-
Notifications
You must be signed in to change notification settings - Fork 212
/
Copy pathVoting.sol
421 lines (358 loc) · 15.8 KB
/
Voting.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
/*
* SPDX-License-Identitifer: GPL-3.0-or-later
*/
pragma solidity 0.4.24;
import "@aragon/os/contracts/apps/AragonApp.sol";
import "@aragon/os/contracts/common/IForwarder.sol";
import "@aragon/os/contracts/lib/math/SafeMath.sol";
import "@aragon/os/contracts/lib/math/SafeMath64.sol";
import "@aragon/minime/contracts/MiniMeToken.sol";
contract Voting is IForwarder, AragonApp {
using SafeMath for uint256;
using SafeMath64 for uint64;
bytes32 public constant CREATE_VOTES_ROLE = keccak256("CREATE_VOTES_ROLE");
bytes32 public constant MODIFY_SUPPORT_ROLE = keccak256("MODIFY_SUPPORT_ROLE");
bytes32 public constant MODIFY_QUORUM_ROLE = keccak256("MODIFY_QUORUM_ROLE");
uint64 public constant PCT_BASE = 10 ** 18; // 0% = 0; 1% = 10^16; 100% = 10^18
string private constant ERROR_NO_VOTE = "VOTING_NO_VOTE";
string private constant ERROR_INIT_PCTS = "VOTING_INIT_PCTS";
string private constant ERROR_CHANGE_SUPPORT_PCTS = "VOTING_CHANGE_SUPPORT_PCTS";
string private constant ERROR_CHANGE_QUORUM_PCTS = "VOTING_CHANGE_QUORUM_PCTS";
string private constant ERROR_INIT_SUPPORT_TOO_BIG = "VOTING_INIT_SUPPORT_TOO_BIG";
string private constant ERROR_CHANGE_SUPPORT_TOO_BIG = "VOTING_CHANGE_SUPP_TOO_BIG";
string private constant ERROR_CAN_NOT_VOTE = "VOTING_CAN_NOT_VOTE";
string private constant ERROR_CAN_NOT_EXECUTE = "VOTING_CAN_NOT_EXECUTE";
string private constant ERROR_CAN_NOT_FORWARD = "VOTING_CAN_NOT_FORWARD";
string private constant ERROR_NO_VOTING_POWER = "VOTING_NO_VOTING_POWER";
enum VoterState { Absent, Yea, Nay }
struct Vote {
bool executed;
uint64 startDate;
uint64 snapshotBlock;
uint64 supportRequiredPct;
uint64 minAcceptQuorumPct;
uint256 yea;
uint256 nay;
uint256 votingPower;
bytes executionScript;
mapping (address => VoterState) voters;
}
MiniMeToken public token;
uint64 public supportRequiredPct;
uint64 public minAcceptQuorumPct;
uint64 public voteTime;
// We are mimicing an array, we use a mapping instead to make app upgrade more graceful
mapping (uint256 => Vote) internal votes;
uint256 public votesLength;
event StartVote(uint256 indexed voteId, address indexed creator, string metadata);
event CastVote(uint256 indexed voteId, address indexed voter, bool supports, uint256 stake);
event ExecuteVote(uint256 indexed voteId);
event ChangeSupportRequired(uint64 supportRequiredPct);
event ChangeMinQuorum(uint64 minAcceptQuorumPct);
modifier voteExists(uint256 _voteId) {
require(_voteId < votesLength, ERROR_NO_VOTE);
_;
}
/**
* @notice Initialize Voting app with `_token.symbol(): string` for governance, minimum support of `@formatPct(_supportRequiredPct)`%, minimum acceptance quorum of `@formatPct(_minAcceptQuorumPct)`%, and a voting duration of `@transformTime(_voteTime)`
* @param _token MiniMeToken Address that will be used as governance token
* @param _supportRequiredPct Percentage of yeas in casted votes for a vote to succeed (expressed as a percentage of 10^18; eg. 10^16 = 1%, 10^18 = 100%)
* @param _minAcceptQuorumPct Percentage of yeas in total possible votes for a vote to succeed (expressed as a percentage of 10^18; eg. 10^16 = 1%, 10^18 = 100%)
* @param _voteTime Seconds that a vote will be open for token holders to vote (unless enough yeas or nays have been cast to make an early decision)
*/
function initialize(MiniMeToken _token, uint64 _supportRequiredPct, uint64 _minAcceptQuorumPct, uint64 _voteTime) external onlyInit {
initialized();
require(_minAcceptQuorumPct <= _supportRequiredPct, ERROR_INIT_PCTS);
require(_supportRequiredPct < PCT_BASE, ERROR_INIT_SUPPORT_TOO_BIG);
token = _token;
supportRequiredPct = _supportRequiredPct;
minAcceptQuorumPct = _minAcceptQuorumPct;
voteTime = _voteTime;
}
/**
* @notice Change required support to `@formatPct(_supportRequiredPct)`%
* @param _supportRequiredPct New required support
*/
function changeSupportRequiredPct(uint64 _supportRequiredPct)
external
authP(MODIFY_SUPPORT_ROLE, arr(uint256(_supportRequiredPct), uint256(supportRequiredPct)))
{
require(minAcceptQuorumPct <= _supportRequiredPct, ERROR_CHANGE_SUPPORT_PCTS);
require(_supportRequiredPct < PCT_BASE, ERROR_CHANGE_SUPPORT_TOO_BIG);
supportRequiredPct = _supportRequiredPct;
emit ChangeSupportRequired(_supportRequiredPct);
}
/**
* @notice Change minimum acceptance quorum to `@formatPct(_minAcceptQuorumPct)`%
* @param _minAcceptQuorumPct New acceptance quorum
*/
function changeMinAcceptQuorumPct(uint64 _minAcceptQuorumPct)
external
authP(MODIFY_QUORUM_ROLE, arr(uint256(_minAcceptQuorumPct), uint256(minAcceptQuorumPct)))
{
require(_minAcceptQuorumPct <= supportRequiredPct, ERROR_CHANGE_QUORUM_PCTS);
minAcceptQuorumPct = _minAcceptQuorumPct;
emit ChangeMinQuorum(_minAcceptQuorumPct);
}
/**
* @notice Create a new vote about "`_metadata`"
* @param _executionScript EVM script to be executed on approval
* @param _metadata Vote metadata
* @return voteId Id for newly created vote
*/
function newVote(bytes _executionScript, string _metadata) external auth(CREATE_VOTES_ROLE) returns (uint256 voteId) {
return _newVote(_executionScript, _metadata, true, true);
}
/**
* @notice Create a new vote about "`_metadata`"
* @param _executionScript EVM script to be executed on approval
* @param _metadata Vote metadata
* @param _castVote Whether to also cast newly created vote
* @param _executesIfDecided Whether to also immediately execute newly created vote if decided
* @return voteId id for newly created vote
*/
function newVote(bytes _executionScript, string _metadata, bool _castVote, bool _executesIfDecided)
external
auth(CREATE_VOTES_ROLE)
returns (uint256 voteId)
{
return _newVote(_executionScript, _metadata, _castVote, _executesIfDecided);
}
/**
* @notice Vote `_supports ? 'yes' : 'no'` in vote #`_voteId`
* @dev Initialization check is implicitly provided by `voteExists()` as new votes can only be
* created via `newVote(),` which requires initialization
* @param _voteId Id for vote
* @param _supports Whether voter supports the vote
* @param _executesIfDecided Whether the vote should execute its action if it becomes decided
*/
function vote(uint256 _voteId, bool _supports, bool _executesIfDecided) external voteExists(_voteId) {
require(_canVote(_voteId, msg.sender), ERROR_CAN_NOT_VOTE);
_vote(_voteId, _supports, msg.sender, _executesIfDecided);
}
/**
* @notice Execute vote #`_voteId`
* @dev Initialization check is implicitly provided by `voteExists()` as new votes can only be
* created via `newVote(),` which requires initialization
* @param _voteId Id for vote
*/
function executeVote(uint256 _voteId) external voteExists(_voteId) {
_executeVote(_voteId);
}
// Forwarding fns
/**
* @notice Tells whether the Voting app is a forwarder or not
* @dev IForwarder interface conformance
* @return Always true
*/
function isForwarder() external pure returns (bool) {
return true;
}
/**
* @notice Creates a vote to execute the desired action, and casts a support vote if possible
* @dev IForwarder interface conformance
* @param _evmScript Start vote with script
*/
function forward(bytes _evmScript) public {
require(canForward(msg.sender, _evmScript), ERROR_CAN_NOT_FORWARD);
_newVote(_evmScript, "", true, true);
}
/**
* @notice Tells whether `_sender` can forward actions or not
* @dev IForwarder interface conformance
* @param _sender Address of the account intending to forward an action
* @return True if the given address can create votes, false otherwise
*/
function canForward(address _sender, bytes) public view returns (bool) {
// Note that `canPerform()` implicitly does an initialization check itself
return canPerform(_sender, CREATE_VOTES_ROLE, arr());
}
// Getter fns
/**
* @notice Tells whether a vote #`_voteId` can be executed or not
* @dev Initialization check is implicitly provided by `voteExists()` as new votes can only be
* created via `newVote(),` which requires initialization
* @return True if the given vote can be executed, false otherwise
*/
function canExecute(uint256 _voteId) public view voteExists(_voteId) returns (bool) {
return _canExecute(_voteId);
}
/**
* @notice Tells whether `_sender` can participate in the vote #`_voteId` or not
* @dev Initialization check is implicitly provided by `voteExists()` as new votes can only be
* created via `newVote(),` which requires initialization
* @return True if the given voter can participate a certain vote, false otherwise
*/
function canVote(uint256 _voteId, address _voter) public view voteExists(_voteId) returns (bool) {
return _canVote(_voteId, _voter);
}
/**
* @dev Return all information for a vote by its ID
* @param _voteId Vote identifier
* @return Vote open status
* @return Vote executed status
* @return Vote start date
* @return Vote snapshot block
* @return Vote support required
* @return Vote minimum acceptance quorum
* @return Vote yeas amount
* @return Vote nays amount
* @return Vote power
* @return Vote script
*/
function getVote(uint256 _voteId)
public
view
voteExists(_voteId)
returns (
bool open,
bool executed,
uint64 startDate,
uint64 snapshotBlock,
uint64 supportRequired,
uint64 minAcceptQuorum,
uint256 yea,
uint256 nay,
uint256 votingPower,
bytes script
)
{
Vote storage vote_ = votes[_voteId];
open = _isVoteOpen(vote_);
executed = vote_.executed;
startDate = vote_.startDate;
snapshotBlock = vote_.snapshotBlock;
supportRequired = vote_.supportRequiredPct;
minAcceptQuorum = vote_.minAcceptQuorumPct;
yea = vote_.yea;
nay = vote_.nay;
votingPower = vote_.votingPower;
script = vote_.executionScript;
}
/**
* @dev Return the state of a voter for a given vote by its ID
* @param _voteId Vote identifier
* @return VoterState of the requested voter for a certain vote
*/
function getVoterState(uint256 _voteId, address _voter) public view voteExists(_voteId) returns (VoterState) {
return votes[_voteId].voters[_voter];
}
// Internal fns
/**
* @dev Internal function to create a new vote
* @return voteId id for newly created vote
*/
function _newVote(bytes _executionScript, string _metadata, bool _castVote, bool _executesIfDecided) internal returns (uint256 voteId) {
uint64 snapshotBlock = getBlockNumber64() - 1; // avoid double voting in this very block
uint256 votingPower = token.totalSupplyAt(snapshotBlock);
require(votingPower > 0, ERROR_NO_VOTING_POWER);
voteId = votesLength++;
Vote storage vote_ = votes[voteId];
vote_.startDate = getTimestamp64();
vote_.snapshotBlock = snapshotBlock;
vote_.supportRequiredPct = supportRequiredPct;
vote_.minAcceptQuorumPct = minAcceptQuorumPct;
vote_.votingPower = votingPower;
vote_.executionScript = _executionScript;
emit StartVote(voteId, msg.sender, _metadata);
if (_castVote && _canVote(voteId, msg.sender)) {
_vote(voteId, true, msg.sender, _executesIfDecided);
}
}
/**
* @dev Internal function to cast a vote. It assumes the queried vote exists.
*/
function _vote(uint256 _voteId, bool _supports, address _voter, bool _executesIfDecided) internal {
Vote storage vote_ = votes[_voteId];
// This could re-enter, though we can assume the governance token is not malicious
uint256 voterStake = token.balanceOfAt(_voter, vote_.snapshotBlock);
VoterState state = vote_.voters[_voter];
// If voter had previously voted, decrease count
if (state == VoterState.Yea) {
vote_.yea = vote_.yea.sub(voterStake);
} else if (state == VoterState.Nay) {
vote_.nay = vote_.nay.sub(voterStake);
}
if (_supports) {
vote_.yea = vote_.yea.add(voterStake);
} else {
vote_.nay = vote_.nay.add(voterStake);
}
vote_.voters[_voter] = _supports ? VoterState.Yea : VoterState.Nay;
emit CastVote(_voteId, _voter, _supports, voterStake);
if (_executesIfDecided && _canExecute(_voteId)) {
// We've already checked if the vote can be executed with `_canExecute()`
_unsafeExecuteVote(_voteId);
}
}
/**
* @dev Internal function to execute a vote. It assumes the queried vote exists.
*/
function _executeVote(uint256 _voteId) internal {
require(_canExecute(_voteId), ERROR_CAN_NOT_EXECUTE);
_unsafeExecuteVote(_voteId);
}
/**
* @dev Unsafe version of _executeVote that assumes you have already checked if the vote can be executed and exists
*/
function _unsafeExecuteVote(uint256 _voteId) internal {
Vote storage vote_ = votes[_voteId];
vote_.executed = true;
bytes memory input = new bytes(0); // TODO: Consider input for voting scripts
runScript(vote_.executionScript, input, new address[](0));
emit ExecuteVote(_voteId);
}
/**
* @dev Internal function to check if a vote can be executed. It assumes the queried vote exists.
* @return True if the given vote can be executed, false otherwise
*/
function _canExecute(uint256 _voteId) internal view returns (bool) {
Vote storage vote_ = votes[_voteId];
if (vote_.executed) {
return false;
}
// Voting is already decided
if (_isValuePct(vote_.yea, vote_.votingPower, vote_.supportRequiredPct)) {
return true;
}
// Vote ended?
if (_isVoteOpen(vote_)) {
return false;
}
// Has enough support?
uint256 totalVotes = vote_.yea.add(vote_.nay);
if (!_isValuePct(vote_.yea, totalVotes, vote_.supportRequiredPct)) {
return false;
}
// Has min quorum?
if (!_isValuePct(vote_.yea, vote_.votingPower, vote_.minAcceptQuorumPct)) {
return false;
}
return true;
}
/**
* @dev Internal function to check if a voter can participate on a vote. It assumes the queried vote exists.
* @return True if the given voter can participate a certain vote, false otherwise
*/
function _canVote(uint256 _voteId, address _voter) internal view returns (bool) {
Vote storage vote_ = votes[_voteId];
return _isVoteOpen(vote_) && token.balanceOfAt(_voter, vote_.snapshotBlock) > 0;
}
/**
* @dev Internal function to check if a vote is still open
* @return True if the given vote is open, false otherwise
*/
function _isVoteOpen(Vote storage vote_) internal view returns (bool) {
return getTimestamp64() < vote_.startDate.add(voteTime) && !vote_.executed;
}
/**
* @dev Calculates whether `_value` is more than a percentage `_pct` of `_total`
*/
function _isValuePct(uint256 _value, uint256 _total, uint256 _pct) internal pure returns (bool) {
if (_total == 0) {
return false;
}
uint256 computedPct = _value.mul(PCT_BASE) / _total;
return computedPct > _pct;
}
}