-
Notifications
You must be signed in to change notification settings - Fork 23
/
Molochv2.1.sol
721 lines (566 loc) · 34 KB
/
Molochv2.1.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
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
pragma solidity 0.5.3;
import "./oz/IERC20.sol";
import "./oz/SafeMath.sol";
import "./oz/ReentrancyGuard.sol";
contract Moloch is ReentrancyGuard {
using SafeMath for uint256;
/***************
GLOBAL CONSTANTS
***************/
uint256 public periodDuration; // default = 17280 = 4.8 hours in seconds (5 periods per day)
uint256 public votingPeriodLength; // default = 35 periods (7 days)
uint256 public gracePeriodLength; // default = 35 periods (7 days)
uint256 public proposalDeposit; // default = 10 ETH (~$1,000 worth of ETH at contract deployment)
uint256 public dilutionBound; // default = 3 - maximum multiplier a YES voter will be obligated to pay in case of mass ragequit
uint256 public processingReward; // default = 0.1 - amount of ETH to give to whoever processes a proposal
uint256 public summoningTime; // needed to determine the current period
bool private initialized; // internally tracks deployment under eip-1167 proxy pattern
address public depositToken; // deposit token contract reference; default = wETH
// HARD-CODED LIMITS
// These numbers are quite arbitrary; they are small enough to avoid overflows when doing calculations
// with periods or shares, yet big enough to not limit reasonable use cases.
uint256 constant MAX_VOTING_PERIOD_LENGTH = 10**18; // maximum length of voting period
uint256 constant MAX_GRACE_PERIOD_LENGTH = 10**18; // maximum length of grace period
uint256 constant MAX_DILUTION_BOUND = 10**18; // maximum dilution bound
uint256 constant MAX_NUMBER_OF_SHARES_AND_LOOT = 10**18; // maximum number of shares that can be minted
uint256 constant MAX_TOKEN_WHITELIST_COUNT = 400; // maximum number of whitelisted tokens
uint256 constant MAX_TOKEN_GUILDBANK_COUNT = 200; // maximum number of tokens with non-zero balance in guildbank
// ***************
// EVENTS
// ***************
event SummonComplete(address indexed summoner, address[] tokens, uint256 summoningTime, uint256 periodDuration, uint256 votingPeriodLength, uint256 gracePeriodLength, uint256 proposalDeposit, uint256 dilutionBound, uint256 processingReward);
event SubmitProposal(address indexed applicant, uint256 sharesRequested, uint256 lootRequested, uint256 tributeOffered, address tributeToken, uint256 paymentRequested, address paymentToken, string details, bool[6] flags, uint256 proposalId, address indexed delegateKey, address indexed memberAddress);
event SponsorProposal(address indexed delegateKey, address indexed memberAddress, uint256 proposalId, uint256 proposalIndex, uint256 startingPeriod);
event SubmitVote(uint256 proposalId, uint256 indexed proposalIndex, address indexed delegateKey, address indexed memberAddress, uint8 uintVote);
event ProcessProposal(uint256 indexed proposalIndex, uint256 indexed proposalId, bool didPass);
event ProcessWhitelistProposal(uint256 indexed proposalIndex, uint256 indexed proposalId, bool didPass);
event ProcessGuildKickProposal(uint256 indexed proposalIndex, uint256 indexed proposalId, bool didPass);
event Ragequit(address indexed memberAddress, uint256 sharesToBurn, uint256 lootToBurn);
event TokensCollected(address indexed token, uint256 amountToCollect);
event CancelProposal(uint256 indexed proposalId, address applicantAddress);
event UpdateDelegateKey(address indexed memberAddress, address newDelegateKey);
event Withdraw(address indexed memberAddress, address token, uint256 amount);
// *******************
// INTERNAL ACCOUNTING
// *******************
uint256 public proposalCount = 0; // total proposals submitted
uint256 public totalShares = 0; // total shares across all members
uint256 public totalLoot = 0; // total loot across all members
uint256 public totalGuildBankTokens = 0; // total tokens with non-zero balance in guild bank
address public constant GUILD = address(0xdead);
address public constant ESCROW = address(0xbeef);
address public constant TOTAL = address(0xbabe);
mapping (address => mapping(address => uint256)) public userTokenBalances; // userTokenBalances[userAddress][tokenAddress]
enum Vote {
Null, // default value, counted as abstention
Yes,
No
}
struct Member {
address delegateKey; // the key responsible for submitting proposals and voting - defaults to member address unless updated
uint256 shares; // the # of voting shares assigned to this member
uint256 loot; // the loot amount available to this member (combined with shares on ragequit)
bool exists; // always true once a member has been created
uint256 highestIndexYesVote; // highest proposal index # on which the member voted YES
uint256 jailed; // set to proposalIndex of a passing guild kick proposal for this member, prevents voting on and sponsoring proposals
}
struct Proposal {
address applicant; // the applicant who wishes to become a member - this key will be used for withdrawals (doubles as guild kick target for gkick proposals)
address proposer; // the account that submitted the proposal (can be non-member)
address sponsor; // the member that sponsored the proposal (moving it into the queue)
uint256 sharesRequested; // the # of shares the applicant is requesting
uint256 lootRequested; // the amount of loot the applicant is requesting
uint256 tributeOffered; // amount of tokens offered as tribute
address tributeToken; // tribute token contract reference
uint256 paymentRequested; // amount of tokens requested as payment
address paymentToken; // payment token contract reference
uint256 startingPeriod; // the period in which voting can start for this proposal
uint256 yesVotes; // the total number of YES votes for this proposal
uint256 noVotes; // the total number of NO votes for this proposal
bool[6] flags; // [sponsored, processed, didPass, cancelled, whitelist, guildkick]
string details; // proposal details - could be IPFS hash, plaintext, or JSON
uint256 maxTotalSharesAndLootAtYesVote; // the maximum # of total shares encountered at a yes vote on this proposal
mapping(address => Vote) votesByMember; // the votes on this proposal by each member
}
mapping(address => bool) public tokenWhitelist;
address[] public approvedTokens;
mapping(address => bool) public proposedToWhitelist;
mapping(address => bool) public proposedToKick;
mapping(address => Member) public members;
mapping(address => address) public memberAddressByDelegateKey;
mapping(uint256 => Proposal) public proposals;
uint256[] public proposalQueue;
modifier onlyMember {
require(members[msg.sender].shares > 0 || members[msg.sender].loot > 0, "not a member");
_;
}
modifier onlyShareholder {
require(members[msg.sender].shares > 0, "not a shareholder");
_;
}
modifier onlyDelegate {
require(members[memberAddressByDelegateKey[msg.sender]].shares > 0, "not a delegate");
_;
}
function init(
address[] calldata _summoner,
address[] calldata _approvedTokens,
uint256 _periodDuration,
uint256 _votingPeriodLength,
uint256 _gracePeriodLength,
uint256 _proposalDeposit,
uint256 _dilutionBound,
uint256 _processingReward,
uint256[] calldata _summonerShares
) external {
require(!initialized, "initialized");
require(_summoner.length == _summonerShares.length, "summoner length mismatches summonerShares");
require(_periodDuration > 0, "_periodDuration cannot be 0");
require(_votingPeriodLength > 0, "_votingPeriodLength cannot be 0");
require(_votingPeriodLength <= MAX_VOTING_PERIOD_LENGTH, "_votingPeriodLength exceeds limit");
require(_gracePeriodLength <= MAX_GRACE_PERIOD_LENGTH, "_gracePeriodLength exceeds limit");
require(_dilutionBound > 0, "_dilutionBound cannot be 0");
require(_dilutionBound <= MAX_DILUTION_BOUND, "_dilutionBound exceeds limit");
require(_approvedTokens.length > 0, "need at least one approved token");
require(_approvedTokens.length <= MAX_TOKEN_WHITELIST_COUNT, "too many tokens");
require(_proposalDeposit >= _processingReward, "_proposalDeposit cannot be smaller than _processingReward");
depositToken = _approvedTokens[0];
for (uint256 i = 0; i < _summoner.length; i++) {
require(_summoner[i] != address(0), "summoner cannot be 0");
members[_summoner[i]] = Member(_summoner[i], _summonerShares[i], 0, true, 0, 0);
memberAddressByDelegateKey[_summoner[i]] = _summoner[i];
totalShares = totalShares.add(_summonerShares[i]);
}
require(totalShares <= MAX_NUMBER_OF_SHARES_AND_LOOT, "too many shares requested");
for (uint256 i = 0; i < _approvedTokens.length; i++) {
require(_approvedTokens[i] != address(0), "_approvedToken cannot be 0");
require(!tokenWhitelist[_approvedTokens[i]], "duplicate approved token");
tokenWhitelist[_approvedTokens[i]] = true;
approvedTokens.push(_approvedTokens[i]);
}
periodDuration = _periodDuration;
votingPeriodLength = _votingPeriodLength;
gracePeriodLength = _gracePeriodLength;
proposalDeposit = _proposalDeposit;
dilutionBound = _dilutionBound;
processingReward = _processingReward;
summoningTime = now;
initialized = true;
}
/*****************
PROPOSAL FUNCTIONS
*****************/
function submitProposal(
address applicant,
uint256 sharesRequested,
uint256 lootRequested,
uint256 tributeOffered,
address tributeToken,
uint256 paymentRequested,
address paymentToken,
string memory details
) public nonReentrant returns (uint256 proposalId) {
require(sharesRequested.add(lootRequested) <= MAX_NUMBER_OF_SHARES_AND_LOOT, "too many shares requested");
require(tokenWhitelist[tributeToken], "tributeToken is not whitelisted");
require(tokenWhitelist[paymentToken], "payment is not whitelisted");
require(applicant != address(0), "applicant cannot be 0");
require(applicant != GUILD && applicant != ESCROW && applicant != TOTAL, "applicant address cannot be reserved");
require(members[applicant].jailed == 0, "proposal applicant must not be jailed");
if (tributeOffered > 0 && userTokenBalances[GUILD][tributeToken] == 0) {
require(totalGuildBankTokens < MAX_TOKEN_GUILDBANK_COUNT, 'cannot submit more tribute proposals for new tokens - guildbank is full');
}
// collect tribute from proposer and store it in the Moloch until the proposal is processed
require(IERC20(tributeToken).transferFrom(msg.sender, address(this), tributeOffered), "tribute token transfer failed");
unsafeAddToBalance(ESCROW, tributeToken, tributeOffered);
bool[6] memory flags; // [sponsored, processed, didPass, cancelled, whitelist, guildkick]
_submitProposal(applicant, sharesRequested, lootRequested, tributeOffered, tributeToken, paymentRequested, paymentToken, details, flags);
return proposalCount - 1; // return proposalId - contracts calling submit might want it
}
function submitWhitelistProposal(address tokenToWhitelist, string memory details) public nonReentrant returns (uint256 proposalId) {
require(tokenToWhitelist != address(0), "must provide token address");
require(!tokenWhitelist[tokenToWhitelist], "cannot already have whitelisted the token");
require(approvedTokens.length < MAX_TOKEN_WHITELIST_COUNT, "cannot submit more whitelist proposals");
bool[6] memory flags; // [sponsored, processed, didPass, cancelled, whitelist, guildkick]
flags[4] = true; // whitelist
_submitProposal(address(0), 0, 0, 0, tokenToWhitelist, 0, address(0), details, flags);
return proposalCount - 1;
}
function submitGuildKickProposal(address memberToKick, string memory details) public nonReentrant returns (uint256 proposalId) {
Member memory member = members[memberToKick];
require(member.shares > 0 || member.loot > 0, "member must have at least one share or one loot");
require(members[memberToKick].jailed == 0, "member must not already be jailed");
bool[6] memory flags; // [sponsored, processed, didPass, cancelled, whitelist, guildkick]
flags[5] = true; // guild kick
_submitProposal(memberToKick, 0, 0, 0, address(0), 0, address(0), details, flags);
return proposalCount - 1;
}
function _submitProposal(
address applicant,
uint256 sharesRequested,
uint256 lootRequested,
uint256 tributeOffered,
address tributeToken,
uint256 paymentRequested,
address paymentToken,
string memory details,
bool[6] memory flags
) internal {
Proposal memory proposal = Proposal({
applicant : applicant,
proposer : msg.sender,
sponsor : address(0),
sharesRequested : sharesRequested,
lootRequested : lootRequested,
tributeOffered : tributeOffered,
tributeToken : tributeToken,
paymentRequested : paymentRequested,
paymentToken : paymentToken,
startingPeriod : 0,
yesVotes : 0,
noVotes : 0,
flags : flags,
details : details,
maxTotalSharesAndLootAtYesVote : 0
});
proposals[proposalCount] = proposal;
address memberAddress = memberAddressByDelegateKey[msg.sender];
// NOTE: argument order matters, avoid stack too deep
emit SubmitProposal(applicant, sharesRequested, lootRequested, tributeOffered, tributeToken, paymentRequested, paymentToken, details, flags, proposalCount, msg.sender, memberAddress);
proposalCount += 1;
}
function sponsorProposal(uint256 proposalId) public nonReentrant onlyDelegate {
// collect proposal deposit from sponsor and store it in the Moloch until the proposal is processed
require(IERC20(depositToken).transferFrom(msg.sender, address(this), proposalDeposit), "proposal deposit token transfer failed");
unsafeAddToBalance(ESCROW, depositToken, proposalDeposit);
Proposal storage proposal = proposals[proposalId];
require(proposal.proposer != address(0), 'proposal must have been proposed');
require(!proposal.flags[0], "proposal has already been sponsored");
require(!proposal.flags[3], "proposal has been cancelled");
require(members[proposal.applicant].jailed == 0, "proposal applicant must not be jailed");
if (proposal.tributeOffered > 0 && userTokenBalances[GUILD][proposal.tributeToken] == 0) {
require(totalGuildBankTokens < MAX_TOKEN_GUILDBANK_COUNT, 'cannot sponsor more tribute proposals for new tokens - guildbank is full');
}
// whitelist proposal
if (proposal.flags[4]) {
require(!tokenWhitelist[address(proposal.tributeToken)], "cannot already have whitelisted the token");
require(!proposedToWhitelist[address(proposal.tributeToken)], 'already proposed to whitelist');
require(approvedTokens.length < MAX_TOKEN_WHITELIST_COUNT, "cannot sponsor more whitelist proposals");
proposedToWhitelist[address(proposal.tributeToken)] = true;
// guild kick proposal
} else if (proposal.flags[5]) {
require(!proposedToKick[proposal.applicant], 'already proposed to kick');
proposedToKick[proposal.applicant] = true;
}
// compute startingPeriod for proposal
uint256 startingPeriod = max(
getCurrentPeriod(),
proposalQueue.length == 0 ? 0 : proposals[proposalQueue[proposalQueue.length.sub(1)]].startingPeriod
).add(1);
proposal.startingPeriod = startingPeriod;
address memberAddress = memberAddressByDelegateKey[msg.sender];
proposal.sponsor = memberAddress;
proposal.flags[0] = true; // sponsored
// append proposal to the queue
proposalQueue.push(proposalId);
emit SponsorProposal(msg.sender, memberAddress, proposalId, proposalQueue.length.sub(1), startingPeriod);
}
// NOTE: In MolochV2 proposalIndex !== proposalId
function submitVote(uint256 proposalIndex, uint8 uintVote) public nonReentrant onlyDelegate {
address memberAddress = memberAddressByDelegateKey[msg.sender];
Member storage member = members[memberAddress];
require(proposalIndex < proposalQueue.length, "proposal does not exist");
Proposal storage proposal = proposals[proposalQueue[proposalIndex]];
require(uintVote < 3, "must be less than 3");
Vote vote = Vote(uintVote);
require(getCurrentPeriod() >= proposal.startingPeriod, "voting period has not started");
require(!hasVotingPeriodExpired(proposal.startingPeriod), "proposal voting period has expired");
require(proposal.votesByMember[memberAddress] == Vote.Null, "member has already voted");
require(vote == Vote.Yes || vote == Vote.No, "vote must be either Yes or No");
proposal.votesByMember[memberAddress] = vote;
if (vote == Vote.Yes) {
proposal.yesVotes = proposal.yesVotes.add(member.shares);
// set highest index (latest) yes vote - must be processed for member to ragequit
if (proposalIndex > member.highestIndexYesVote) {
member.highestIndexYesVote = proposalIndex;
}
// set maximum of total shares encountered at a yes vote - used to bound dilution for yes voters
if (totalShares.add(totalLoot) > proposal.maxTotalSharesAndLootAtYesVote) {
proposal.maxTotalSharesAndLootAtYesVote = totalShares.add(totalLoot);
}
} else if (vote == Vote.No) {
proposal.noVotes = proposal.noVotes.add(member.shares);
}
// NOTE: subgraph indexes by proposalId not proposalIndex since proposalIndex isn't set untill it's been sponsored but proposal is created on submission
emit SubmitVote(proposalQueue[proposalIndex], proposalIndex, msg.sender, memberAddress, uintVote);
}
function processProposal(uint256 proposalIndex) public nonReentrant {
_validateProposalForProcessing(proposalIndex);
uint256 proposalId = proposalQueue[proposalIndex];
Proposal storage proposal = proposals[proposalId];
require(!proposal.flags[4] && !proposal.flags[5], "must be a standard proposal");
proposal.flags[1] = true; // processed
bool didPass = _didPass(proposalIndex);
// Make the proposal fail if the new total number of shares and loot exceeds the limit
if (totalShares.add(totalLoot).add(proposal.sharesRequested).add(proposal.lootRequested) > MAX_NUMBER_OF_SHARES_AND_LOOT) {
didPass = false;
}
// Make the proposal fail if it is requesting more tokens as payment than the available guild bank balance
if (proposal.paymentRequested > userTokenBalances[GUILD][proposal.paymentToken]) {
didPass = false;
}
// Make the proposal fail if it would result in too many tokens with non-zero balance in guild bank
if (proposal.tributeOffered > 0 && userTokenBalances[GUILD][proposal.tributeToken] == 0 && totalGuildBankTokens >= MAX_TOKEN_GUILDBANK_COUNT) {
didPass = false;
}
// PROPOSAL PASSED
if (didPass) {
proposal.flags[2] = true; // didPass
// if the applicant is already a member, add to their existing shares & loot
if (members[proposal.applicant].exists) {
members[proposal.applicant].shares = members[proposal.applicant].shares.add(proposal.sharesRequested);
members[proposal.applicant].loot = members[proposal.applicant].loot.add(proposal.lootRequested);
// the applicant is a new member, create a new record for them
} else {
// if the applicant address is already taken by a member's delegateKey, reset it to their member address
if (members[memberAddressByDelegateKey[proposal.applicant]].exists) {
address memberToOverride = memberAddressByDelegateKey[proposal.applicant];
memberAddressByDelegateKey[memberToOverride] = memberToOverride;
members[memberToOverride].delegateKey = memberToOverride;
}
// use applicant address as delegateKey by default
members[proposal.applicant] = Member(proposal.applicant, proposal.sharesRequested, proposal.lootRequested, true, 0, 0);
memberAddressByDelegateKey[proposal.applicant] = proposal.applicant;
}
// mint new shares & loot
totalShares = totalShares.add(proposal.sharesRequested);
totalLoot = totalLoot.add(proposal.lootRequested);
// if the proposal tribute is the first tokens of its kind to make it into the guild bank, increment total guild bank tokens
if (userTokenBalances[GUILD][proposal.tributeToken] == 0 && proposal.tributeOffered > 0) {
totalGuildBankTokens += 1;
}
unsafeInternalTransfer(ESCROW, GUILD, proposal.tributeToken, proposal.tributeOffered);
unsafeInternalTransfer(GUILD, proposal.applicant, proposal.paymentToken, proposal.paymentRequested);
// if the proposal spends 100% of guild bank balance for a token, decrement total guild bank tokens
if (userTokenBalances[GUILD][proposal.paymentToken] == 0 && proposal.paymentRequested > 0) {
totalGuildBankTokens -= 1;
}
// PROPOSAL FAILED
} else {
// return all tokens to the proposer (not the applicant, because funds come from proposer)
unsafeInternalTransfer(ESCROW, proposal.proposer, proposal.tributeToken, proposal.tributeOffered);
}
_returnDeposit(proposal.sponsor);
emit ProcessProposal(proposalIndex, proposalId, didPass);
}
function processWhitelistProposal(uint256 proposalIndex) public nonReentrant {
_validateProposalForProcessing(proposalIndex);
uint256 proposalId = proposalQueue[proposalIndex];
Proposal storage proposal = proposals[proposalId];
require(proposal.flags[4], "must be a whitelist proposal");
proposal.flags[1] = true; // processed
bool didPass = _didPass(proposalIndex);
if (approvedTokens.length >= MAX_TOKEN_WHITELIST_COUNT) {
didPass = false;
}
if (didPass) {
proposal.flags[2] = true; // didPass
tokenWhitelist[address(proposal.tributeToken)] = true;
approvedTokens.push(proposal.tributeToken);
}
proposedToWhitelist[address(proposal.tributeToken)] = false;
_returnDeposit(proposal.sponsor);
emit ProcessWhitelistProposal(proposalIndex, proposalId, didPass);
}
function processGuildKickProposal(uint256 proposalIndex) public nonReentrant {
_validateProposalForProcessing(proposalIndex);
uint256 proposalId = proposalQueue[proposalIndex];
Proposal storage proposal = proposals[proposalId];
require(proposal.flags[5], "must be a guild kick proposal");
proposal.flags[1] = true; // processed
bool didPass = _didPass(proposalIndex);
if (didPass) {
proposal.flags[2] = true; // didPass
Member storage member = members[proposal.applicant];
member.jailed = proposalIndex;
// transfer shares to loot
member.loot = member.loot.add(member.shares);
totalShares = totalShares.sub(member.shares);
totalLoot = totalLoot.add(member.shares);
member.shares = 0; // revoke all shares
}
proposedToKick[proposal.applicant] = false;
_returnDeposit(proposal.sponsor);
emit ProcessGuildKickProposal(proposalIndex, proposalId, didPass);
}
function _didPass(uint256 proposalIndex) internal returns (bool didPass) {
Proposal memory proposal = proposals[proposalQueue[proposalIndex]];
didPass = proposal.yesVotes > proposal.noVotes;
// Make the proposal fail if the dilutionBound is exceeded
if ((totalShares.add(totalLoot)).mul(dilutionBound) < proposal.maxTotalSharesAndLootAtYesVote) {
didPass = false;
}
// Make the proposal fail if the applicant is jailed
// - for standard proposals, we don't want the applicant to get any shares/loot/payment
// - for guild kick proposals, we should never be able to propose to kick a jailed member (or have two kick proposals active), so it doesn't matter
if (members[proposal.applicant].jailed != 0) {
didPass = false;
}
return didPass;
}
function _validateProposalForProcessing(uint256 proposalIndex) internal view {
require(proposalIndex < proposalQueue.length, "proposal does not exist");
Proposal memory proposal = proposals[proposalQueue[proposalIndex]];
require(getCurrentPeriod() >= proposal.startingPeriod.add(votingPeriodLength).add(gracePeriodLength), "proposal is not ready to be processed");
require(proposal.flags[1] == false, "proposal has already been processed");
require(proposalIndex == 0 || proposals[proposalQueue[proposalIndex.sub(1)]].flags[1], "previous proposal must be processed");
}
function _returnDeposit(address sponsor) internal {
unsafeInternalTransfer(ESCROW, msg.sender, depositToken, processingReward);
unsafeInternalTransfer(ESCROW, sponsor, depositToken, proposalDeposit.sub(processingReward));
}
function ragequit(uint256 sharesToBurn, uint256 lootToBurn) public nonReentrant onlyMember {
_ragequit(msg.sender, sharesToBurn, lootToBurn);
}
function _ragequit(address memberAddress, uint256 sharesToBurn, uint256 lootToBurn) internal {
uint256 initialTotalSharesAndLoot = totalShares.add(totalLoot);
Member storage member = members[memberAddress];
require(member.shares >= sharesToBurn, "insufficient shares");
require(member.loot >= lootToBurn, "insufficient loot");
require(canRagequit(member.highestIndexYesVote), "cannot ragequit until highest index proposal member voted YES on is processed");
uint256 sharesAndLootToBurn = sharesToBurn.add(lootToBurn);
// burn shares and loot
member.shares = member.shares.sub(sharesToBurn);
member.loot = member.loot.sub(lootToBurn);
totalShares = totalShares.sub(sharesToBurn);
totalLoot = totalLoot.sub(lootToBurn);
for (uint256 i = 0; i < approvedTokens.length; i++) {
uint256 amountToRagequit = fairShare(userTokenBalances[GUILD][approvedTokens[i]], sharesAndLootToBurn, initialTotalSharesAndLoot);
if (amountToRagequit > 0) { // gas optimization to allow a higher maximum token limit
// deliberately not using safemath here to keep overflows from preventing the function execution (which would break ragekicks)
// if a token overflows, it is because the supply was artificially inflated to oblivion, so we probably don't care about it anyways
userTokenBalances[GUILD][approvedTokens[i]] -= amountToRagequit;
userTokenBalances[memberAddress][approvedTokens[i]] += amountToRagequit;
}
}
emit Ragequit(memberAddress, sharesToBurn, lootToBurn);
}
function ragekick(address memberToKick) public nonReentrant {
Member storage member = members[memberToKick];
require(member.jailed != 0, "member must be in jail");
require(member.loot > 0, "member must have some loot"); // note - should be impossible for jailed member to have shares
require(canRagequit(member.highestIndexYesVote), "cannot ragequit until highest index proposal member voted YES on is processed");
_ragequit(memberToKick, 0, member.loot);
}
function withdrawBalance(address token, uint256 amount) public nonReentrant {
_withdrawBalance(token, amount);
}
function withdrawBalances(address[] memory tokens, uint256[] memory amounts, bool max) public nonReentrant {
require(tokens.length == amounts.length, "tokens and amounts arrays must be matching lengths");
for (uint256 i=0; i < tokens.length; i++) {
uint256 withdrawAmount = amounts[i];
if (max) { // withdraw the maximum balance
withdrawAmount = userTokenBalances[msg.sender][tokens[i]];
}
_withdrawBalance(tokens[i], withdrawAmount);
}
}
function _withdrawBalance(address token, uint256 amount) internal {
require(userTokenBalances[msg.sender][token] >= amount, "insufficient balance");
unsafeSubtractFromBalance(msg.sender, token, amount);
require(IERC20(token).transfer(msg.sender, amount), "transfer failed");
emit Withdraw(msg.sender, token, amount);
}
function collectTokens(address token) public onlyDelegate nonReentrant {
uint256 amountToCollect = IERC20(token).balanceOf(address(this)).sub(userTokenBalances[TOTAL][token]);
// only collect if 1) there are tokens to collect 2) token is whitelisted 3) token has non-zero balance
require(amountToCollect > 0, 'no tokens to collect');
require(tokenWhitelist[token], 'token to collect must be whitelisted');
require(userTokenBalances[GUILD][token] > 0 || totalGuildBankTokens < MAX_TOKEN_GUILDBANK_COUNT, 'token to collect must have non-zero guild bank balance');
if (userTokenBalances[GUILD][token] == 0){
totalGuildBankTokens += 1;
}
unsafeAddToBalance(GUILD, token, amountToCollect);
emit TokensCollected(token, amountToCollect);
}
// NOTE: requires that delegate key which sent the original proposal cancels, msg.sender == proposal.proposer
function cancelProposal(uint256 proposalId) public nonReentrant {
Proposal storage proposal = proposals[proposalId];
require(!proposal.flags[0], "proposal has already been sponsored");
require(!proposal.flags[3], "proposal has already been cancelled");
require(msg.sender == proposal.proposer, "solely the proposer can cancel");
proposal.flags[3] = true; // cancelled
unsafeInternalTransfer(ESCROW, proposal.proposer, proposal.tributeToken, proposal.tributeOffered);
emit CancelProposal(proposalId, msg.sender);
}
function updateDelegateKey(address newDelegateKey) public nonReentrant onlyShareholder {
require(newDelegateKey != address(0), "newDelegateKey cannot be 0");
// skip checks if member is setting the delegate key to their member address
if (newDelegateKey != msg.sender) {
require(!members[newDelegateKey].exists, "cannot overwrite existing members");
require(!members[memberAddressByDelegateKey[newDelegateKey]].exists, "cannot overwrite existing delegate keys");
}
Member storage member = members[msg.sender];
memberAddressByDelegateKey[member.delegateKey] = address(0);
memberAddressByDelegateKey[newDelegateKey] = msg.sender;
member.delegateKey = newDelegateKey;
emit UpdateDelegateKey(msg.sender, newDelegateKey);
}
// can only ragequit if the latest proposal you voted YES on has been processed
function canRagequit(uint256 highestIndexYesVote) public view returns (bool) {
require(highestIndexYesVote < proposalQueue.length, "proposal does not exist");
return proposals[proposalQueue[highestIndexYesVote]].flags[1];
}
function hasVotingPeriodExpired(uint256 startingPeriod) public view returns (bool) {
return getCurrentPeriod() >= startingPeriod.add(votingPeriodLength);
}
/***************
GETTER FUNCTIONS
***************/
function max(uint256 x, uint256 y) internal pure returns (uint256) {
return x >= y ? x : y;
}
function getCurrentPeriod() public view returns (uint256) {
return now.sub(summoningTime).div(periodDuration);
}
function getProposalQueueLength() public view returns (uint256) {
return proposalQueue.length;
}
function getProposalFlags(uint256 proposalId) public view returns (bool[6] memory) {
return proposals[proposalId].flags;
}
function getUserTokenBalance(address user, address token) public view returns (uint256) {
return userTokenBalances[user][token];
}
function getMemberProposalVote(address memberAddress, uint256 proposalIndex) public view returns (Vote) {
require(members[memberAddress].exists, "member does not exist");
require(proposalIndex < proposalQueue.length, "proposal does not exist");
return proposals[proposalQueue[proposalIndex]].votesByMember[memberAddress];
}
function getTokenCount() public view returns (uint256) {
return approvedTokens.length;
}
/***************
HELPER FUNCTIONS
***************/
function unsafeAddToBalance(address user, address token, uint256 amount) internal {
userTokenBalances[user][token] += amount;
userTokenBalances[TOTAL][token] += amount;
}
function unsafeSubtractFromBalance(address user, address token, uint256 amount) internal {
userTokenBalances[user][token] -= amount;
userTokenBalances[TOTAL][token] -= amount;
}
function unsafeInternalTransfer(address from, address to, address token, uint256 amount) internal {
unsafeSubtractFromBalance(from, token, amount);
unsafeAddToBalance(to, token, amount);
}
function fairShare(uint256 balance, uint256 shares, uint256 totalShares) internal pure returns (uint256) {
require(totalShares != 0);
if (balance == 0) { return 0; }
uint256 prod = balance * shares;
if (prod / balance == shares) { // no overflow in multiplication above?
return prod / totalShares;
}
return (balance / totalShares) * shares;
}
}