-
Notifications
You must be signed in to change notification settings - Fork 19
/
CoreVoting.sol
372 lines (321 loc) · 13.9 KB
/
CoreVoting.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
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.3;
import "./interfaces/IVotingVault.sol";
import "./libraries/Authorizable.sol";
import "./libraries/ReentrancyBlock.sol";
import "./interfaces/ICoreVoting.sol";
contract CoreVoting is Authorizable, ReentrancyBlock, ICoreVoting {
// if a function selector does not have a set quorum we use this default quorum
uint256 public baseQuorum;
// Assumes avg block time of 13.3 seconds. May be longer or shorter due
// to ice ages or short term changes in hash power.
uint256 public constant DAY_IN_BLOCKS = 6496;
// minimum time a proposal must be active for before executing
// Default to 3 days, this avoids weekend surprise proposals
uint256 public lockDuration = DAY_IN_BLOCKS * 3;
// The number of blocks after the proposal is unlocked during which
// voting can continue. Max vote time = lockDuration + extraVoteTime
// Default to ~5 days of blocks, ie 8 days max vote time
uint256 public extraVoteTime = DAY_IN_BLOCKS * 5;
// minimum amount of voting power required to submit a proposal
uint256 public minProposalPower;
// number of proposals created
uint256 public proposalCount;
// mapping of address and selector to quorum
mapping(address => mapping(bytes4 => uint256)) private _quorums;
/// @notice Override of the getter for the 'quorums' mapping which returns the default
/// quorum when the quorum is not set.
/// @param target the contract for which the quorum is set
/// @param functionSelector the function which is callable
/// @return The quorum needed to pass the function at this point in time
function quorums(address target, bytes4 functionSelector)
public
view
returns (uint256)
{
uint256 storedQuorum = _quorums[target][functionSelector];
if (storedQuorum == 0) {
return baseQuorum;
} else {
return storedQuorum;
}
}
// stores approved voting vaults
mapping(address => bool) public override approvedVaults;
// proposal storage with the proposalID as key
mapping(uint256 => Proposal) public proposals;
// mapping of addresses and proposalIDs to vote struct representing
// the voting actions taken for each proposal
mapping(address => mapping(uint256 => Vote)) public votes;
enum Ballot { YES, NO, MAYBE }
struct Proposal {
// hash of this proposal's intended function calls
bytes32 proposalHash;
// block of the proposal creation
uint128 created;
// timestamp when the proposal can execute
uint128 unlock;
// expiration time of a proposal
uint128 expiration;
// the quorum required for the proposal to execute
uint128 quorum;
// [yes, no, maybe] voting power
uint128[3] votingPower;
// Timestamp after which if the call has not been executed it cannot be executed
uint128 lastCall;
}
struct Vote {
// voting power of the vote
uint128 votingPower;
// direction of the vote
Ballot castBallot;
}
event ProposalCreated(
uint256 proposalId,
uint256 created,
uint256 execution,
uint256 expiration
);
event ProposalExecuted(uint256 proposalId);
event Voted(address indexed voter, uint256 indexed proposalId, Vote vote);
/// @notice constructor
/// @param _timelock Timelock contract.
/// @param _baseQuorum Default quorum for all functions with no set quorum.
/// @param _minProposalPower Minimum voting power needed to submit a proposal.
/// @param _gsc governance steering committee contract.
/// @param votingVaults Initial voting vaults to approve.
constructor(
address _timelock,
uint256 _baseQuorum,
uint256 _minProposalPower,
address _gsc,
address[] memory votingVaults
) Authorizable() {
baseQuorum = _baseQuorum;
minProposalPower = _minProposalPower;
for (uint256 i = 0; i < votingVaults.length; i++) {
approvedVaults[votingVaults[i]] = true;
}
setOwner(address(_timelock));
_authorize(_gsc);
}
/// @notice Create a new proposal
/// @dev all provided votingVaults must be approved vaults `approvedVaults`.
/// @param votingVaults voting vaults to draw voting power from.
/// @param extraVaultData an encoded list of extra data to provide to vaults
/// @param targets list of target addresses the timelock contract will interact with.
/// @param calldatas execution calldata for each target.
/// @param lastCall timestamp after which this cannot be executed, note should be
/// more than the voting time period
/// @param ballot vote direction (yes, no, maybe)
function proposal(
address[] calldata votingVaults,
bytes[] calldata extraVaultData,
address[] calldata targets,
bytes[] calldata calldatas,
uint256 lastCall,
Ballot ballot
) external {
require(targets.length == calldatas.length, "array length mismatch");
require(targets.length != 0, "empty proposal");
// the hash is only used to verify the proposal data, proposals are tracked by ID
// so there is no need to hash with proposalCount nonce.
bytes32 proposalHash = keccak256(abi.encode(targets, calldatas));
// get the quorum requirement for this proposal. The quorum requirement is equal to
// the greatest quorum item in the proposal
uint256 quorum;
for (uint256 i = 0; i < targets.length; i++) {
// function selector should be the first 4 bytes of the calldata
bytes4 selector = _getSelector(calldatas[i]);
uint256 unitQuorum = _quorums[targets[i]][selector];
// don't assume baseQuorum is the highest
unitQuorum = unitQuorum == 0 ? baseQuorum : unitQuorum;
if (unitQuorum > quorum) {
quorum = unitQuorum;
}
}
// We check that the expiration is possibly valid
require(
lastCall > block.number + lockDuration + extraVoteTime,
"expires before voting ends"
);
proposals[proposalCount] = Proposal(
proposalHash,
// Note we use blocknumber - 1 here as a flash loan mitigation.
uint128(block.number - 1),
uint128(block.number + lockDuration),
uint128(block.number + lockDuration + extraVoteTime),
uint128(quorum),
proposals[proposalCount].votingPower,
uint128(lastCall)
);
uint256 votingPower =
vote(votingVaults, extraVaultData, proposalCount, ballot);
// the proposal quorum is the lowest of minProposalPower and the proposal quorum
// because it is awkward for the proposal to require more voting power than
// the execution
uint256 minPower =
quorum <= minProposalPower ? quorum : minProposalPower;
// the GSC (governance steering comity) contract does not have a voting power requirement
// to submit a proposal
if (!isAuthorized(msg.sender)) {
require(votingPower >= minPower, "insufficient voting power");
}
emit ProposalCreated(
proposalCount,
block.number,
block.number + lockDuration,
block.number + lockDuration + extraVoteTime
);
proposalCount += 1;
}
/// @notice Votes for a new proposal.
/// @dev all provided votingVaults must be approved vaults `approvedVaults`.
/// Addresses can re-vote, but the previous vote's effect will be negated.
/// @param votingVaults voting vaults to draw voting power from.
/// @param extraVaultData extra bytes data to give to each vault
/// @param proposalId proposal identifier.
/// @param ballot vote direction (yes, no, maybe)
/// @return the user's voting power
function vote(
address[] memory votingVaults,
bytes[] memory extraVaultData,
uint256 proposalId,
Ballot ballot
) public returns (uint256) {
// No votes after the vote period is over
require(proposals[proposalId].created != 0, "proposal does not exist");
require(block.number <= proposals[proposalId].expiration, "Expired");
uint128 votingPower;
for (uint256 i = 0; i < votingVaults.length; i++) {
// ensure there are no voting vault duplicates
for (uint256 j = i + 1; j < votingVaults.length; j++) {
require(votingVaults[i] != votingVaults[j], "duplicate vault");
}
require(approvedVaults[votingVaults[i]], "unverified vault");
votingPower += uint128(
IVotingVault(votingVaults[i]).queryVotePower(
msg.sender,
proposals[proposalId].created,
extraVaultData[i]
)
);
}
// if a user has already voted, undo their previous vote.
// NOTE: A new vote can have less voting power
if (votes[msg.sender][proposalId].votingPower > 0) {
proposals[proposalId].votingPower[
uint256(votes[msg.sender][proposalId].castBallot)
] -= votes[msg.sender][proposalId].votingPower;
}
votes[msg.sender][proposalId] = Vote(votingPower, ballot);
proposals[proposalId].votingPower[uint256(ballot)] += votingPower;
// Emit an event to track this info
emit Voted(msg.sender, proposalId, votes[msg.sender][proposalId]);
return votingPower;
}
/// @notice Execute a proposal.
/// @param proposalId proposal identifier.
/// @param targets list of target addresses the timelock contract will interact with.
/// @param calldatas execution calldata for each target.
function execute(
uint256 proposalId,
address[] memory targets,
bytes[] memory calldatas
) external nonReentrant {
// We have to execute after min voting period
require(block.number >= proposals[proposalId].unlock, "not unlocked");
// If executed the proposal will be deleted and this will be zero
require(proposals[proposalId].unlock != 0, "Previously executed");
// We cannot execute if the proposal has expired
require(
block.number < proposals[proposalId].lastCall,
"past last call timestamp"
);
// ensure the data matches the hash
require(
keccak256(abi.encode(targets, calldatas)) ==
proposals[proposalId].proposalHash,
"hash mismatch"
);
uint128[3] memory results = proposals[proposalId].votingPower;
// if there are enough votes to meet quorum and there are more yes votes than no votes
// then the proposal is executed
bool passesQuorum =
results[0] + results[1] + results[2] >=
proposals[proposalId].quorum;
bool majorityInFavor = results[0] > results[1];
require(passesQuorum && majorityInFavor, "Cannot execute");
// Execute a package of low level calls
// NOTE - All of them must succeed for the package to succeed.
for (uint256 i = 0; i < targets.length; i++) {
(bool success, ) = targets[i].call(calldatas[i]);
require(success, "Call failed");
}
// Notification of proposal execution
emit ProposalExecuted(proposalId);
// delete proposal for some gas savings,
// Proposals are only deleted when they are actually executed, failed proposals
// are never deleted
delete proposals[proposalId];
}
/// @notice gets the current voting power for a proposal
/// @param proposalId The proposal's ID.
function getProposalVotingPower(uint256 proposalId)
external
view
returns (uint128[3] memory)
{
return proposals[proposalId].votingPower;
}
/// @notice Sets a quorum for a specific address and selector.
/// @param target Target contract address.
/// @param selector Function selector.
/// @param quorum Fraction to set quorum to.
function setCustomQuorum(
address target,
bytes4 selector,
uint256 quorum
) external onlyOwner {
_quorums[target][selector] = quorum;
}
/// @notice Updates the status of a voting vault.
/// @param vault Address of the voting vault.
/// @param isValid True to be valid, false otherwise.
function changeVaultStatus(address vault, bool isValid) external onlyOwner {
approvedVaults[vault] = isValid;
}
/// @notice Updates the default quorum.
/// @param quorum New base quorum.
function setDefaultQuorum(uint256 quorum) external onlyOwner {
baseQuorum = quorum;
}
/// @notice Updates the minimum voting power needed to submit a proposal.
/// @param _minProposalPower Minimum voting power needed to submit a proposal.
function setMinProposalPower(uint256 _minProposalPower) external onlyOwner {
minProposalPower = _minProposalPower;
}
/// @notice Updates the lock duration of a proposal.
/// @param _lockDuration New lock duration.
function setLockDuration(uint256 _lockDuration) external onlyOwner {
lockDuration = _lockDuration;
}
/// @notice Updates the extra voting period
/// @param _extraVoteTime New extra voting time
function changeExtraVotingTime(uint256 _extraVoteTime) external onlyOwner {
extraVoteTime = _extraVoteTime;
}
/// @notice Internal helper function to get the function selector of a calldata string.
function _getSelector(bytes memory _calldata)
internal
pure
returns (bytes4 out)
{
assembly {
out := and(
mload(add(_calldata, 32)),
0xFFFFFFFFF0000000000000000000000000000000000000000000000000000000
)
}
}
}