forked from alcuadrado/moloch
-
Notifications
You must be signed in to change notification settings - Fork 0
/
flattened.sol
612 lines (484 loc) · 24.2 KB
/
flattened.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
// File: flatten/Ownable.sol
pragma solidity ^0.5.2;
/**
* @title Ownable
* @dev The Ownable contract has an owner address, and provides basic authorization control
* functions, this simplifies the implementation of "user permissions".
*/
contract Ownable {
address private _owner;
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
/**
* @dev The Ownable constructor sets the original `owner` of the contract to the sender
* account.
*/
constructor () internal {
_owner = msg.sender;
emit OwnershipTransferred(address(0), _owner);
}
/**
* @return the address of the owner.
*/
function owner() public view returns (address) {
return _owner;
}
/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
require(isOwner());
_;
}
/**
* @return true if `msg.sender` is the owner of the contract.
*/
function isOwner() public view returns (bool) {
return msg.sender == _owner;
}
/**
* @dev Allows the current owner to relinquish control of the contract.
* @notice Renouncing to ownership will leave the contract without an owner.
* It will not be possible to call the functions with the `onlyOwner`
* modifier anymore.
*/
function renounceOwnership() public onlyOwner {
emit OwnershipTransferred(_owner, address(0));
_owner = address(0);
}
/**
* @dev Allows the current owner to transfer control of the contract to a newOwner.
* @param newOwner The address to transfer ownership to.
*/
function transferOwnership(address newOwner) public onlyOwner {
_transferOwnership(newOwner);
}
/**
* @dev Transfers control of the contract to a newOwner.
* @param newOwner The address to transfer ownership to.
*/
function _transferOwnership(address newOwner) internal {
require(newOwner != address(0));
emit OwnershipTransferred(_owner, newOwner);
_owner = newOwner;
}
}
// File: flatten/IERC20.sol
pragma solidity ^0.5.2;
/**
* @title ERC20 interface
* @dev see https://github.com/ethereum/EIPs/issues/20
*/
interface IERC20 {
function transfer(address to, uint256 value) external returns (bool);
function approve(address spender, uint256 value) external returns (bool);
function transferFrom(address from, address to, uint256 value) external returns (bool);
function totalSupply() external view returns (uint256);
function balanceOf(address who) external view returns (uint256);
function allowance(address owner, address spender) external view returns (uint256);
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
}
// File: flatten/SafeMath.sol
pragma solidity ^0.5.2;
/**
* @title SafeMath
* @dev Unsigned math operations with safety checks that revert on error
*/
library SafeMath {
/**
* @dev Multiplies two unsigned integers, reverts on overflow.
*/
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
// Gas optimization: this is cheaper than requiring 'a' not being zero, but the
// benefit is lost if 'b' is also tested.
// See: https://github.com/OpenZeppelin/openzeppelin-solidity/pull/522
if (a == 0) {
return 0;
}
uint256 c = a * b;
require(c / a == b);
return c;
}
/**
* @dev Integer division of two unsigned integers truncating the quotient, reverts on division by zero.
*/
function div(uint256 a, uint256 b) internal pure returns (uint256) {
// Solidity only automatically asserts when dividing by 0
require(b > 0);
uint256 c = a / b;
// assert(a == b * c + a % b); // There is no case in which this doesn't hold
return c;
}
/**
* @dev Subtracts two unsigned integers, reverts on overflow (i.e. if subtrahend is greater than minuend).
*/
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
require(b <= a);
uint256 c = a - b;
return c;
}
/**
* @dev Adds two unsigned integers, reverts on overflow.
*/
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
require(c >= a);
return c;
}
/**
* @dev Divides two unsigned integers and returns the remainder (unsigned integer modulo),
* reverts when dividing by zero.
*/
function mod(uint256 a, uint256 b) internal pure returns (uint256) {
require(b != 0);
return a % b;
}
}
// File: flatten/GuildBank.sol
pragma solidity 0.5.3;
contract GuildBank is Ownable {
using SafeMath for uint256;
IERC20 public approvedToken; // approved token contract reference
event Withdrawal(address indexed receiver, uint256 amount);
constructor(address approvedTokenAddress) public {
approvedToken = IERC20(approvedTokenAddress);
}
function withdraw(address receiver, uint256 shares, uint256 totalShares) public onlyOwner returns (bool) {
uint256 amount = approvedToken.balanceOf(address(this)).mul(shares).div(totalShares);
emit Withdrawal(receiver, amount);
return approvedToken.transfer(receiver, amount);
}
}
// File: flatten/Moloch.sol
pragma solidity 0.5.3;
contract Moloch {
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 abortWindow; // default = 5 periods (1 day)
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
IERC20 public approvedToken; // approved token contract reference; default = wETH
GuildBank public guildBank; // guild bank contract reference
// 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 = 10**18; // maximum number of shares that can be minted
/***************
EVENTS
***************/
event SubmitProposal(uint256 proposalIndex, address indexed delegateKey, address indexed memberAddress, address indexed applicant, uint256 tokenTribute, uint256 sharesRequested);
event SubmitVote(uint256 indexed proposalIndex, address indexed delegateKey, address indexed memberAddress, uint8 uintVote);
event ProcessProposal(uint256 indexed proposalIndex, address indexed applicant, address indexed memberAddress, uint256 tokenTribute, uint256 sharesRequested, bool didPass);
event Ragequit(address indexed memberAddress, uint256 sharesToBurn);
event Abort(uint256 indexed proposalIndex, address applicantAddress);
event UpdateDelegateKey(address indexed memberAddress, address newDelegateKey);
event SummonComplete(address indexed summoner, uint256 shares);
/******************
INTERNAL ACCOUNTING
******************/
uint256 public totalShares = 0; // total shares across all members
uint256 public totalSharesRequested = 0; // total shares that have been requested in unprocessed proposals
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 shares assigned to this member
bool exists; // always true once a member has been created
uint256 highestIndexYesVote; // highest proposal index # on which the member voted YES
}
struct Proposal {
address proposer; // the member who submitted the proposal
address applicant; // the applicant who wishes to become a member - this key will be used for withdrawals
uint256 sharesRequested; // the # of shares the applicant is requesting
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 processed; // true only if the proposal has been processed
bool didPass; // true only if the proposal passed
bool aborted; // true only if applicant calls "abort" fn before end of voting period
uint256 tokenTribute; // amount of tokens offered as tribute
string details; // proposal details - could be IPFS hash, plaintext, or JSON
uint256 maxTotalSharesAtYesVote; // 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 => Member) public members;
mapping (address => address) public memberAddressByDelegateKey;
Proposal[] public proposalQueue;
/********
MODIFIERS
********/
modifier onlyMember {
require(members[msg.sender].shares > 0, "Moloch::onlyMember - not a member");
_;
}
modifier onlyDelegate {
require(members[memberAddressByDelegateKey[msg.sender]].shares > 0, "Moloch::onlyDelegate - not a delegate");
_;
}
/********
FUNCTIONS
********/
constructor(
address summoner,
address _approvedToken,
uint256 _periodDuration,
uint256 _votingPeriodLength,
uint256 _gracePeriodLength,
uint256 _abortWindow,
uint256 _proposalDeposit,
uint256 _dilutionBound,
uint256 _processingReward
) public {
require(summoner != address(0), "Moloch::constructor - summoner cannot be 0");
require(_approvedToken != address(0), "Moloch::constructor - _approvedToken cannot be 0");
require(_periodDuration > 0, "Moloch::constructor - _periodDuration cannot be 0");
require(_votingPeriodLength > 0, "Moloch::constructor - _votingPeriodLength cannot be 0");
require(_votingPeriodLength <= MAX_VOTING_PERIOD_LENGTH, "Moloch::constructor - _votingPeriodLength exceeds limit");
require(_gracePeriodLength <= MAX_GRACE_PERIOD_LENGTH, "Moloch::constructor - _gracePeriodLength exceeds limit");
require(_abortWindow > 0, "Moloch::constructor - _abortWindow cannot be 0");
require(_abortWindow <= _votingPeriodLength, "Moloch::constructor - _abortWindow must be smaller than or equal to _votingPeriodLength");
require(_dilutionBound > 0, "Moloch::constructor - _dilutionBound cannot be 0");
require(_dilutionBound <= MAX_DILUTION_BOUND, "Moloch::constructor - _dilutionBound exceeds limit");
require(_proposalDeposit >= _processingReward, "Moloch::constructor - _proposalDeposit cannot be smaller than _processingReward");
approvedToken = IERC20(_approvedToken);
guildBank = new GuildBank(_approvedToken);
periodDuration = _periodDuration;
votingPeriodLength = _votingPeriodLength;
gracePeriodLength = _gracePeriodLength;
abortWindow = _abortWindow;
proposalDeposit = _proposalDeposit;
dilutionBound = _dilutionBound;
processingReward = _processingReward;
summoningTime = now;
members[summoner] = Member(summoner, 1, true, 0);
memberAddressByDelegateKey[summoner] = summoner;
totalShares = 1;
emit SummonComplete(summoner, 1);
}
/*****************
PROPOSAL FUNCTIONS
*****************/
function submitProposal(
address applicant,
uint256 tokenTribute,
uint256 sharesRequested,
string memory details
)
public
onlyDelegate
{
require(applicant != address(0), "Moloch::submitProposal - applicant cannot be 0");
// Make sure we won't run into overflows when doing calculations with shares.
// Note that totalShares + totalSharesRequested + sharesRequested is an upper bound
// on the number of shares that can exist until this proposal has been processed.
require(totalShares.add(totalSharesRequested).add(sharesRequested) <= MAX_NUMBER_OF_SHARES, "Moloch::submitProposal - too many shares requested");
totalSharesRequested = totalSharesRequested.add(sharesRequested);
address memberAddress = memberAddressByDelegateKey[msg.sender];
// collect proposal deposit from proposer and store it in the Moloch until the proposal is processed
require(approvedToken.transferFrom(msg.sender, address(this), proposalDeposit), "Moloch::submitProposal - proposal deposit token transfer failed");
// collect tribute from applicant and store it in the Moloch until the proposal is processed
require(approvedToken.transferFrom(applicant, address(this), tokenTribute), "Moloch::submitProposal - tribute token transfer failed");
// compute startingPeriod for proposal
uint256 startingPeriod = max(
getCurrentPeriod(),
proposalQueue.length == 0 ? 0 : proposalQueue[proposalQueue.length.sub(1)].startingPeriod
).add(1);
// create proposal ...
Proposal memory proposal = Proposal({
proposer: memberAddress,
applicant: applicant,
sharesRequested: sharesRequested,
startingPeriod: startingPeriod,
yesVotes: 0,
noVotes: 0,
processed: false,
didPass: false,
aborted: false,
tokenTribute: tokenTribute,
details: details,
maxTotalSharesAtYesVote: 0
});
// ... and append it to the queue
proposalQueue.push(proposal);
uint256 proposalIndex = proposalQueue.length.sub(1);
emit SubmitProposal(proposalIndex, msg.sender, memberAddress, applicant, tokenTribute, sharesRequested);
}
function submitVote(uint256 proposalIndex, uint8 uintVote) public onlyDelegate {
address memberAddress = memberAddressByDelegateKey[msg.sender];
Member storage member = members[memberAddress];
require(proposalIndex < proposalQueue.length, "Moloch::submitVote - proposal does not exist");
Proposal storage proposal = proposalQueue[proposalIndex];
require(uintVote < 3, "Moloch::submitVote - uintVote must be less than 3");
Vote vote = Vote(uintVote);
require(getCurrentPeriod() >= proposal.startingPeriod, "Moloch::submitVote - voting period has not started");
require(!hasVotingPeriodExpired(proposal.startingPeriod), "Moloch::submitVote - proposal voting period has expired");
require(proposal.votesByMember[memberAddress] == Vote.Null, "Moloch::submitVote - member has already voted on this proposal");
require(vote == Vote.Yes || vote == Vote.No, "Moloch::submitVote - vote must be either Yes or No");
require(!proposal.aborted, "Moloch::submitVote - proposal has been aborted");
// store vote
proposal.votesByMember[memberAddress] = vote;
// count 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 > proposal.maxTotalSharesAtYesVote) {
proposal.maxTotalSharesAtYesVote = totalShares;
}
} else if (vote == Vote.No) {
proposal.noVotes = proposal.noVotes.add(member.shares);
}
emit SubmitVote(proposalIndex, msg.sender, memberAddress, uintVote);
}
function processProposal(uint256 proposalIndex) public {
require(proposalIndex < proposalQueue.length, "Moloch::processProposal - proposal does not exist");
Proposal storage proposal = proposalQueue[proposalIndex];
require(getCurrentPeriod() >= proposal.startingPeriod.add(votingPeriodLength).add(gracePeriodLength), "Moloch::processProposal - proposal is not ready to be processed");
require(proposal.processed == false, "Moloch::processProposal - proposal has already been processed");
require(proposalIndex == 0 || proposalQueue[proposalIndex.sub(1)].processed, "Moloch::processProposal - previous proposal must be processed");
proposal.processed = true;
totalSharesRequested = totalSharesRequested.sub(proposal.sharesRequested);
bool didPass = proposal.yesVotes > proposal.noVotes;
// Make the proposal fail if the dilutionBound is exceeded
if (totalShares.mul(dilutionBound) < proposal.maxTotalSharesAtYesVote) {
didPass = false;
}
// PROPOSAL PASSED
if (didPass && !proposal.aborted) {
proposal.didPass = true;
// if the applicant is already a member, add to their existing shares
if (members[proposal.applicant].exists) {
members[proposal.applicant].shares = members[proposal.applicant].shares.add(proposal.sharesRequested);
// 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, true, 0);
memberAddressByDelegateKey[proposal.applicant] = proposal.applicant;
}
// mint new shares
totalShares = totalShares.add(proposal.sharesRequested);
// transfer tokens to guild bank
require(
approvedToken.transfer(address(guildBank), proposal.tokenTribute),
"Moloch::processProposal - token transfer to guild bank failed"
);
// PROPOSAL FAILED OR ABORTED
} else {
// return all tokens to the applicant
require(
approvedToken.transfer(proposal.applicant, proposal.tokenTribute),
"Moloch::processProposal - failing vote token transfer failed"
);
}
// send msg.sender the processingReward
require(
approvedToken.transfer(msg.sender, processingReward),
"Moloch::processProposal - failed to send processing reward to msg.sender"
);
// return deposit to proposer (subtract processing reward)
require(
approvedToken.transfer(proposal.proposer, proposalDeposit.sub(processingReward)),
"Moloch::processProposal - failed to return proposal deposit to proposer"
);
emit ProcessProposal(
proposalIndex,
proposal.applicant,
proposal.proposer,
proposal.tokenTribute,
proposal.sharesRequested,
didPass
);
}
function ragequit(uint256 sharesToBurn) public onlyMember {
uint256 initialTotalShares = totalShares;
Member storage member = members[msg.sender];
require(member.shares >= sharesToBurn, "Moloch::ragequit - insufficient shares");
require(canRagequit(member.highestIndexYesVote), "Moloch::ragequit - cant ragequit until highest index proposal member voted YES on is processed");
// burn shares
member.shares = member.shares.sub(sharesToBurn);
totalShares = totalShares.sub(sharesToBurn);
// instruct guildBank to transfer fair share of tokens to the ragequitter
require(
guildBank.withdraw(msg.sender, sharesToBurn, initialTotalShares),
"Moloch::ragequit - withdrawal of tokens from guildBank failed"
);
emit Ragequit(msg.sender, sharesToBurn);
}
function abort(uint256 proposalIndex) public {
require(proposalIndex < proposalQueue.length, "Moloch::abort - proposal does not exist");
Proposal storage proposal = proposalQueue[proposalIndex];
require(msg.sender == proposal.applicant, "Moloch::abort - msg.sender must be applicant");
require(getCurrentPeriod() < proposal.startingPeriod.add(abortWindow), "Moloch::abort - abort window must not have passed");
require(!proposal.aborted, "Moloch::abort - proposal must not have already been aborted");
uint256 tokensToAbort = proposal.tokenTribute;
proposal.tokenTribute = 0;
proposal.aborted = true;
// return all tokens to the applicant
require(
approvedToken.transfer(proposal.applicant, tokensToAbort),
"Moloch::processProposal - failed to return tribute to applicant"
);
emit Abort(proposalIndex, msg.sender);
}
function updateDelegateKey(address newDelegateKey) public onlyMember {
require(newDelegateKey != address(0), "Moloch::updateDelegateKey - 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, "Moloch::updateDelegateKey - cant overwrite existing members");
require(!members[memberAddressByDelegateKey[newDelegateKey]].exists, "Moloch::updateDelegateKey - cant 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);
}
/***************
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;
}
// 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, "Moloch::canRagequit - proposal does not exist");
return proposalQueue[highestIndexYesVote].processed;
}
function hasVotingPeriodExpired(uint256 startingPeriod) public view returns (bool) {
return getCurrentPeriod() >= startingPeriod.add(votingPeriodLength);
}
function getMemberProposalVote(address memberAddress, uint256 proposalIndex) public view returns (Vote) {
require(members[memberAddress].exists, "Moloch::getMemberProposalVote - member doesn't exist");
require(proposalIndex < proposalQueue.length, "Moloch::getMemberProposalVote - proposal doesn't exist");
return proposalQueue[proposalIndex].votesByMember[memberAddress];
}
}
// "0x512E07A093aAA20Ba288392EaDF03838C7a4e522", "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", 17280, 35, 35, 5, 10000000000000000000, 3, 100000000000000000