-
Notifications
You must be signed in to change notification settings - Fork 18
/
Copy pathAdminVoting.sol
257 lines (226 loc) · 9.54 KB
/
AdminVoting.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
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import "@openzeppelin/contracts/utils/Address.sol";
import "../dependencies/DelegatedOps.sol";
import "../dependencies/SystemStart.sol";
import "../interfaces/ITokenLocker.sol";
/**
@title Prisma DAO Admin Voter
@notice Primary ownership contract for all Prisma contracts. Allows executing
arbitrary function calls only after a required percentage of PRISMA
lockers have signalled in favor of performing the action.
*/
contract AdminVoting is DelegatedOps, SystemStart {
using Address for address;
event ProposalCreated(address indexed account, Action[] payload, uint256 week, uint256 requiredWeight);
event ProposalExecuted(uint256 proposalId);
event ProposalCancelled(uint256 proposalId);
event VoteCast(address indexed account, uint256 id, uint256 weight, uint256 proposalCurrentWeight);
event ProposalCreationMinWeightSet(uint256 weight);
event ProposalPassingPctSet(uint256 pct);
struct Proposal {
uint16 week; // week which vote weights are based upon
uint32 createdAt; // timestamp when the proposal was created
uint40 currentWeight; // amount of weight currently voting in favor
uint40 requiredWeight; // amount of weight required for the proposal to be executed
bool processed; // set to true once the proposal is processed
}
struct Action {
address target;
bytes data;
}
uint256 public constant VOTING_PERIOD = 1 weeks;
uint256 public constant MIN_TIME_TO_EXECUTION = 86400;
ITokenLocker public immutable tokenLocker;
IPrismaCore public immutable prismaCore;
Proposal[] proposalData;
mapping(uint256 => Action[]) proposalPayloads;
// account -> ID -> amount of weight voted in favor
mapping(address => mapping(uint256 => uint256)) public accountVoteWeights;
// absolute amount of weight required to create a new proposal
uint256 public minCreateProposalWeight;
// percent of total weight that must vote for a proposal before it can be executed
uint256 public passingPct;
constructor(
address _prismaCore,
ITokenLocker _tokenLocker,
uint256 _minCreateProposalWeight,
uint256 _passingPct
) SystemStart(_prismaCore) {
tokenLocker = _tokenLocker;
prismaCore = IPrismaCore(_prismaCore);
minCreateProposalWeight = _minCreateProposalWeight;
passingPct = _passingPct;
}
/**
@notice The total number of votes created
*/
function getProposalCount() external view returns (uint256) {
return proposalData.length;
}
/**
@notice Gets information on a specific proposal
*/
function getProposalData(
uint256 id
)
external
view
returns (
uint256 week,
uint256 createdAt,
uint256 currentWeight,
uint256 requiredWeight,
bool executed,
bool canExecute,
Action[] memory payload
)
{
Proposal memory proposal = proposalData[id];
payload = proposalPayloads[id];
canExecute = (!proposal.processed &&
proposal.currentWeight >= proposal.requiredWeight &&
proposal.createdAt + MIN_TIME_TO_EXECUTION < block.timestamp);
return (
proposal.week,
proposal.createdAt,
proposal.currentWeight,
proposal.requiredWeight,
proposal.processed,
canExecute,
payload
);
}
/**
@notice Create a new proposal
@param payload Tuple of [(target address, calldata), ... ] to be
executed if the proposal is passed.
*/
function createNewProposal(address account, Action[] calldata payload) external callerOrDelegated(account) {
require(payload.length > 0, "Empty payload");
// week is set at -1 to the active week so that weights are finalized
uint256 week = getWeek();
require(week > 0, "No proposals in first week");
week -= 1;
uint256 accountWeight = tokenLocker.getAccountWeightAt(account, week);
require(accountWeight >= minCreateProposalWeight, "Not enough weight to propose");
uint256 totalWeight = tokenLocker.getTotalWeightAt(week);
uint40 requiredWeight = uint40((totalWeight * passingPct) / 100);
uint256 idx = proposalData.length;
proposalData.push(
Proposal({
week: uint16(week),
createdAt: uint32(block.timestamp),
currentWeight: 0,
requiredWeight: requiredWeight,
processed: false
})
);
for (uint256 i = 0; i < payload.length; i++) {
proposalPayloads[idx].push(payload[i]);
}
emit ProposalCreated(account, payload, week, requiredWeight);
}
/**
@notice Vote in favor of a proposal
@dev Each account can vote once per proposal
@param id Proposal ID
@param weight Weight to allocate to this action. If set to zero, the full available
account weight is used. Integrating protocols may wish to use partial
weight to reflect partial support from their own users.
*/
function voteForProposal(address account, uint256 id, uint256 weight) external callerOrDelegated(account) {
require(id < proposalData.length, "Invalid ID");
require(accountVoteWeights[account][id] == 0, "Already voted");
Proposal memory proposal = proposalData[id];
require(!proposal.processed, "Proposal already processed");
require(proposal.createdAt + VOTING_PERIOD > block.timestamp, "Voting period has closed");
uint256 accountWeight = tokenLocker.getAccountWeightAt(account, proposal.week);
if (weight == 0) {
weight = accountWeight;
require(weight > 0, "No vote weight");
} else {
require(weight <= accountWeight, "Weight exceeds account weight");
}
accountVoteWeights[account][id] = weight;
uint40 updatedWeight = uint40(proposal.currentWeight + weight);
proposalData[id].currentWeight = updatedWeight;
emit VoteCast(account, id, weight, updatedWeight);
}
/**
@notice Cancels a pending proposal
@dev Can only be called by the guardian to avoid malicious proposals
Guardians cannot cancel a proposal for their replacement
@param id Proposal ID
*/
function cancelProposal(uint256 id) external {
require(msg.sender == prismaCore.guardian(), "Only guardian can cancel proposals");
require(id < proposalData.length, "Invalid ID");
// We make sure guardians cannot cancel proposals for their replacement
Action[] storage payload = proposalPayloads[id];
Action memory firstAction = payload[0];
bytes memory data = firstAction.data;
// Extract the call sig from payload data
bytes4 sig;
assembly {
sig := mload(add(data, 0x20))
}
require(
firstAction.target != address(prismaCore) || sig != IPrismaCore.setGuardian.selector,
"Guardian replacement not cancellable"
);
proposalData[id].processed = true;
emit ProposalCancelled(id);
}
/**
@notice Execute a proposal's payload
@dev Can only be called if the proposal has received sufficient vote weight,
and has been active for at least `MIN_TIME_TO_EXECUTION`
@param id Proposal ID
*/
function executeProposal(uint256 id) external {
require(id < proposalData.length, "Invalid ID");
Proposal memory proposal = proposalData[id];
require(proposal.currentWeight >= proposal.requiredWeight, "Not passed");
require(proposal.createdAt + MIN_TIME_TO_EXECUTION < block.timestamp, "MIN_TIME_TO_EXECUTION");
require(!proposal.processed, "Already processed");
proposalData[id].processed = true;
Action[] storage payload = proposalPayloads[id];
uint256 payloadLength = payload.length;
for (uint256 i = 0; i < payloadLength; i++) {
payload[i].target.functionCall(payload[i].data);
}
emit ProposalExecuted(id);
}
/**
@notice Set the minimum absolute weight required to create a new proposal
@dev Only callable via a passing proposal that includes a call
to this contract and function within it's payload
*/
function setMinCreateProposalWeight(uint256 weight) external returns (bool) {
require(msg.sender == address(this), "Only callable via proposal");
minCreateProposalWeight = weight;
emit ProposalCreationMinWeightSet(weight);
return true;
}
/**
@notice Set the required % of the total weight that must vote
for a proposal prior to being able to execute it
@dev Only callable via a passing proposal that includes a call
to this contract and function within it's payload
*/
function setPassingPct(uint256 pct) external returns (bool) {
require(msg.sender == address(this), "Only callable via proposal");
require(pct <= 100, "Invalid value");
passingPct = pct;
emit ProposalPassingPctSet(pct);
return true;
}
/**
@dev Unguarded method to allow accepting ownership transfer of `PrismaCore`
at the end of the deployment sequence
*/
function acceptTransferOwnership() external {
prismaCore.acceptTransferOwnership();
}
}