/
InvitesV1.sol
298 lines (261 loc) · 7.47 KB
/
InvitesV1.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
// SPDX-License-Identifier: MIT
pragma solidity >=0.8;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/utils/math/SafeMathUpgradeable.sol";
import "../Interfaces.sol";
// import "hardhat/console.sol";
/**
* @title InvitesV1 contract that handles invites with pre allocated bounty pool
* 1.1 adds invitee bonus
*/
contract InvitesV1 is Initializable {
using SafeMathUpgradeable for uint256;
struct Stats {
uint256 totalApprovedInvites;
uint256 totalBountiesPaid;
uint256 totalInvited;
uint256[5] __reserevedSpace;
}
struct User {
address invitedBy;
bytes32 inviteCode;
bool bountyPaid;
address[] invitees;
address[] pending;
uint256 level;
uint256 levelStarted;
uint256 totalApprovedInvites;
uint256 totalEarned;
uint256 joinedAt;
uint256[5] __reserevedSpace;
}
struct Level {
uint256 toNext;
uint256 bounty; //in G$ cents ie 2 decimals
uint256 daysToComplete;
uint256[5] __reserevedSpace;
}
mapping(bytes32 => address) public codeToUser;
mapping(address => User) public users;
address payable public avatar;
mapping(uint256 => Level) public levels;
address public owner;
IIdentity public identity;
cERC20 public goodDollar;
bool public active;
Stats public stats;
bool public levelExpirationEnabled;
event InviteeJoined(address indexed inviter, address indexed invitee);
event InviterBounty(
address indexed inviter,
address indexed invitee,
uint256 bountyPaid,
uint256 inviterLevel,
bool earnedLevel
);
modifier ownerOrAvatar() {
require(
msg.sender == owner || msg.sender == avatar,
"Only owner or avatar can perform this action"
);
_;
}
modifier isActive() {
require(active, "not active");
_;
}
function initialize(
address payable _avatar,
address _identity,
address _gd,
uint256 level0Bounty
) public initializer {
owner = msg.sender;
identity = IIdentity(_identity);
active = true;
Level storage lvl = levels[0];
lvl.bounty = level0Bounty;
goodDollar = cERC20(_gd);
avatar = _avatar;
levelExpirationEnabled = false;
}
function setLevelExpirationEnabled(bool _isEnabled) public ownerOrAvatar {
levelExpirationEnabled = _isEnabled;
}
function join(bytes32 _myCode, bytes32 _inviterCode) public isActive {
require(
codeToUser[_myCode] == address(0) || codeToUser[_myCode] == msg.sender,
"invite code already in use"
);
User storage user = users[msg.sender]; // this is not expensive as user is new
address inviter = codeToUser[_inviterCode];
//allow user to set inviter if doesnt have one
require(
user.inviteCode == 0x0 ||
(user.invitedBy == address(0) && inviter != address(0)),
"user already joined"
);
if (user.inviteCode == 0x0) {
user.inviteCode = _myCode;
user.levelStarted = block.timestamp;
user.joinedAt = block.timestamp;
codeToUser[_myCode] = msg.sender;
}
if (inviter != address(0)) {
user.invitedBy = inviter;
users[inviter].invitees.push(msg.sender);
users[inviter].pending.push(msg.sender);
stats.totalInvited += 1;
}
emit InviteeJoined(inviter, msg.sender);
}
function canCollectBountyFor(address _invitee) public view returns (bool) {
address invitedBy = users[_invitee].invitedBy;
uint256 daysToComplete = levels[users[invitedBy].level].daysToComplete;
bool isLevelExpired = levelExpirationEnabled == true &&
daysToComplete > 0 &&
users[_invitee].joinedAt > users[invitedBy].levelStarted &&
daysToComplete <
users[_invitee].joinedAt.sub(users[invitedBy].levelStarted).div(1 days);
return
invitedBy != address(0) &&
!users[_invitee].bountyPaid &&
identity.isWhitelisted(_invitee) &&
identity.isWhitelisted(invitedBy) &&
isLevelExpired == false;
}
function getInvitees(address _inviter)
public
view
returns (address[] memory)
{
return users[_inviter].invitees;
}
function getPendingInvitees(address _inviter)
public
view
returns (address[] memory)
{
address[] memory pending = users[_inviter].pending;
uint256 cur = 0;
uint256 total = 0;
for (uint256 i; i < pending.length; i++) {
if (!users[pending[i]].bountyPaid) {
total++;
}
}
address[] memory result = new address[](total);
for (uint256 i; i < pending.length; i++) {
if (!users[pending[i]].bountyPaid) {
result[cur] = pending[i];
cur++;
}
}
return result;
}
function getPendingBounties(address _inviter) public view returns (uint256) {
address[] memory pending = users[_inviter].pending;
uint256 total = 0;
for (uint256 i; i < pending.length; i++) {
if (canCollectBountyFor(pending[i])) {
total++;
}
}
return total;
}
/**
* @dev pay bounty for the inviter of _invitee
* invitee need to be whitelisted
*/
function bountyFor(address _invitee)
public
isActive
returns (uint256 bounty)
{
require(canCollectBountyFor(_invitee), "user not elligble for bounty yet");
return _bountyFor(_invitee, true);
}
function _bountyFor(address _invitee, bool isSingleBounty)
internal
returns (uint256 bounty)
{
address invitedBy = users[_invitee].invitedBy;
uint256 joinedAt = users[_invitee].joinedAt;
Level memory level = levels[users[invitedBy].level];
bool isLevelExpired = level.daysToComplete > 0 &&
joinedAt > users[invitedBy].levelStarted && //prevent overflow in subtraction
level.daysToComplete <
joinedAt.sub(users[invitedBy].levelStarted).div(1 days); //how long after level started did invitee join
users[_invitee].bountyPaid = true;
users[invitedBy].totalApprovedInvites += 1;
users[invitedBy].totalEarned += level.bounty;
stats.totalApprovedInvites += 1;
stats.totalBountiesPaid += level.bounty;
bool earnedLevel = false;
if (
level.toNext > 0 &&
users[invitedBy].totalApprovedInvites >= level.toNext &&
isLevelExpired == false
) {
users[invitedBy].level += 1;
users[invitedBy].levelStarted = block.timestamp;
earnedLevel = true;
}
if (isSingleBounty) goodDollar.transfer(invitedBy, level.bounty);
goodDollar.transfer(_invitee, level.bounty.div(2)); //pay invitee half the bounty
emit InviterBounty(
invitedBy,
_invitee,
level.bounty,
users[invitedBy].level,
earnedLevel
);
return level.bounty;
}
/**
@dev collect bounties for invitees by msg.sender that are now whitelisted
*/
function collectBounties() public isActive {
address[] storage pendings = users[msg.sender].pending;
uint256 totalBounties = 0;
for (int256 i = int256(pendings.length) - 1; i >= 0; i--) {
if (gasleft() < 100000) break;
address pending = pendings[uint256(i)];
if (canCollectBountyFor(pending)) {
totalBounties += _bountyFor(pending, false);
pendings.pop();
}
}
if (totalBounties > 0) goodDollar.transfer(msg.sender, totalBounties);
}
function setLevel(
uint256 _lvl,
uint256 _toNext,
uint256 _bounty,
uint256 _daysToComplete
) public ownerOrAvatar {
Level storage lvl = levels[_lvl];
lvl.toNext = _toNext;
lvl.daysToComplete = _daysToComplete;
lvl.bounty = _bounty;
}
function setActive(bool _active) public ownerOrAvatar {
active = _active;
}
function end() public ownerOrAvatar isActive {
uint256 gdBalance = goodDollar.balanceOf(address(this));
goodDollar.transfer(avatar, gdBalance);
avatar.transfer(address(this).balance);
active = false;
}
/**
* @dev
* 1.2.0 - final changes before release
* 1.3.0 - allow to set inviter later
* 1.4.0 - improve gas for bounty collection
* 1.5.0 - more gas improvements
*/
function version() public pure returns (string memory) {
return "1.5.0";
}
}