-
Notifications
You must be signed in to change notification settings - Fork 48
/
PLCRVoting.sol
324 lines (276 loc) · 13.2 KB
/
PLCRVoting.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
pragma solidity ^0.4.8;
import "./HumanStandardToken.sol";
import "./DLL.sol";
import "./AttributeStore.sol";
/**
@title Partial-Lock-Commit-Reveal Voting scheme with ERC20 tokens
@author Team: Aspyn Palatnick, Cem Ozer, Yorke Rhodes
*/
contract PLCRVoting {
/// maps user's address to voteToken balance
mapping(address => uint) public voteTokenBalance;
struct Poll {
uint commitEndDate; /// expiration date of commit period for poll
uint revealEndDate; /// expiration date of reveal period for poll
uint voteQuorum; /// number of votes required for a proposal to pass
uint votesFor; /// tally of votes supporting proposal
uint votesAgainst; /// tally of votes countering proposal
}
/// maps pollID to Poll struct
mapping(uint => Poll) public pollMap;
uint pollNonce;
event PollCreated(uint pollID);
using DLL for DLL.Data;
mapping(address => DLL.Data) dllMap;
using AttributeStore for AttributeStore.Data;
AttributeStore.Data store;
// ============
// CONSTRUCTOR:
// ============
uint constant INITIAL_POLL_NONCE = 0;
HumanStandardToken public token;
/**
@dev Initializes voteQuorum, commitDuration, revealDuration, and pollNonce in addition to token contract and trusted mapping
@param tokenAddr The address where the ERC20 token contract is deployed
*/
function PLCRVoting(address tokenAddr) {
token = HumanStandardToken(tokenAddr);
pollNonce = INITIAL_POLL_NONCE;
}
// ================
// TOKEN INTERFACE:
// ================
/**
@notice Loads numTokens ERC20 tokens into the voting contract for one-to-one voting rights
@dev Assumes that msg.sender has approved voting contract to spend on their behalf
@param numTokens The number of votingTokens desired in exchange for ERC20 tokens
*/
function requestVotingRights(uint numTokens) external {
require(token.balanceOf(msg.sender) >= numTokens);
require(token.transferFrom(msg.sender, this, numTokens));
voteTokenBalance[msg.sender] += numTokens;
}
/**
@notice Withdraw numTokens ERC20 tokens from the voting contract, revoking these voting rights
@param numTokens The number of ERC20 tokens desired in exchange for voting rights
*/
function withdrawVotingRights(uint numTokens) external {
uint availableTokens = voteTokenBalance[msg.sender] - getLockedTokens(msg.sender);
require(availableTokens >= numTokens);
require(token.transfer(msg.sender, numTokens));
voteTokenBalance[msg.sender] -= numTokens;
}
/**
@dev Unlocks tokens locked in unrevealed vote where poll has ended
@param pollID Integer identifier associated with the target poll
*/
function rescueTokens(uint pollID) external {
require(pollEnded(pollID));
require(!hasBeenRevealed(msg.sender, pollID));
dllMap[msg.sender].remove(pollID);
}
// =================
// VOTING INTERFACE:
// =================
/**
@notice Commits vote using hash of choice and secret salt to conceal vote until reveal
@param pollID Integer identifier associated with target poll
@param secretHash Commit keccak256 hash of voter's choice and salt (tightly packed in this order)
@param numTokens The number of tokens to be committed towards the target poll
@param prevPollID The ID of the poll that the user has voted the maximum number of tokens in which is still less than or equal to numTokens
*/
function commitVote(uint pollID, bytes32 secretHash, uint numTokens, uint prevPollID) external {
require(commitPeriodActive(pollID));
require(voteTokenBalance[msg.sender] >= numTokens); // prevent user from overspending
require(pollID != 0); // prevent user from committing to zero node placerholder
uint nextPollID = dllMap[msg.sender].getNext(prevPollID);
require(validPosition(prevPollID, nextPollID, msg.sender, numTokens));
dllMap[msg.sender].insert(prevPollID, pollID, nextPollID);
bytes32 UUID = attrUUID(msg.sender, pollID);
store.attachAttribute(UUID, "numTokens", numTokens);
store.attachAttribute(UUID, "commitHash", uint(secretHash));
}
/**
@dev Compares previous and next poll's committed tokens for sorting purposes
@param prevID Integer identifier associated with previous poll in sorted order
@param nextID Integer identifier associated with next poll in sorted order
@param voter Address of user to check DLL position for
@param numTokens The number of tokens to be committed towards the poll (used for sorting)
@return valid Boolean indication of if the specified position maintains the sort
*/
function validPosition(uint prevID, uint nextID, address voter, uint numTokens) public constant returns (bool valid) {
bool prevValid = (numTokens >= getNumTokens(voter, prevID));
// if next is zero node, numTokens does not need to be greater
bool nextValid = (numTokens <= getNumTokens(voter, nextID) || nextID == 0);
return prevValid && nextValid;
}
/**
@notice Reveals vote with choice and secret salt used in generating commitHash to attribute committed tokens
@param pollID Integer identifier associated with target poll
@param voteOption Vote choice used to generate commitHash for associated poll
@param salt Secret number used to generate commitHash for associated poll
*/
function revealVote(uint pollID, uint voteOption, uint salt) external {
// Make sure the reveal period is active
require(revealPeriodActive(pollID));
require(!hasBeenRevealed(msg.sender, pollID)); // prevent user from revealing multiple times
require(sha3(voteOption, salt) == getCommitHash(msg.sender, pollID)); // compare resultant hash from inputs to original commitHash
uint numTokens = getNumTokens(msg.sender, pollID);
if (voteOption == 1) // apply numTokens to appropriate poll choice
pollMap[pollID].votesFor += numTokens;
else
pollMap[pollID].votesAgainst += numTokens;
dllMap[msg.sender].remove(pollID); // remove the node referring to this vote upon reveal
}
/**
@param pollID Integer identifier associated with target poll
@param salt Arbitrarily chosen integer used to generate secretHash
@return correctVotes Number of tokens voted for winning option
*/
function getNumPassingTokens(address voter, uint pollID, uint salt) public constant returns (uint correctVotes) {
require(pollEnded(pollID));
require(hasBeenRevealed(voter, pollID));
uint winningChoice = isPassed(pollID) ? 1 : 0;
bytes32 winnerHash = sha3(winningChoice, salt);
bytes32 commitHash = getCommitHash(voter, pollID);
return (winnerHash == commitHash) ? getNumTokens(voter, pollID) : 0;
}
// ==================
// POLLING INTERFACE:
// ==================
/**
@dev Initiates a poll with canonical configured parameters at pollID emitted by PollCreated event
@param _voteQuorum Type of majority (out of 100) that is necessary for poll to be successful
@param _commitDuration Length of desired commit period in seconds
@param _revealDuration Length of desired reveal period in seconds
*/
function startPoll(uint _voteQuorum, uint _commitDuration, uint _revealDuration) public returns (uint pollID) {
pollNonce = pollNonce + 1;
pollMap[pollNonce] = Poll({
voteQuorum: _voteQuorum,
commitEndDate: block.timestamp + _commitDuration,
revealEndDate: block.timestamp + _commitDuration + _revealDuration,
votesFor: 0,
votesAgainst: 0
});
PollCreated(pollNonce);
return pollNonce;
}
/**
@notice Determines if proposal has passed
@dev Check if votesFor out of totalVotes exceeds votesQuorum (requires pollEnded)
@param pollID Integer identifier associated with target poll
*/
function isPassed(uint pollID) constant public returns (bool passed) {
require(pollEnded(pollID));
Poll poll = pollMap[pollID];
return (100 * poll.votesFor) > (poll.voteQuorum * (poll.votesFor + poll.votesAgainst));
}
// ----------------
// POLLING HELPERS:
// ----------------
/**
@dev Gets the total winning votes for reward distribution purposes
@param pollID Integer identifier associated with target poll
@return Total number of votes committed to the winning option for specified poll
*/
function getTotalNumberOfTokensForWinningOption(uint pollID) constant public returns (uint numTokens) {
require(pollEnded(pollID));
if (isPassed(pollID))
return pollMap[pollID].votesFor;
else
return pollMap[pollID].votesAgainst;
}
/**
@notice Determines if poll is over
@dev Checks isExpired for specified poll's revealEndDate
@return Boolean indication of whether polling period is over
*/
function pollEnded(uint pollID) constant public returns (bool ended) {
return isExpired(pollMap[pollID].revealEndDate);
}
/**
@notice Checks if the commit period is still active for the specified poll
@dev Checks isExpired for the specified poll's commitEndDate
@param pollID Integer identifier associated with target poll
@return Boolean indication of isCommitPeriodActive for target poll
*/
function commitPeriodActive(uint pollID) constant public returns (bool active) {
return !isExpired(pollMap[pollID].commitEndDate);
}
/**
@notice Checks if the reveal period is still active for the specified poll
@dev Checks isExpired for the specified poll's revealEndDate
@param pollID Integer identifier associated with target poll
*/
function revealPeriodActive(uint pollID) constant public returns (bool active) {
return !isExpired(pollMap[pollID].revealEndDate) && !commitPeriodActive(pollID);
}
/**
@dev Checks if user has already revealed for specified poll
@param voter Address of user to check against
@param pollID Integer identifier associated with target poll
@return Boolean indication of whether user has already revealed
*/
function hasBeenRevealed(address voter, uint pollID) constant public returns (bool revealed) {
uint prevID = dllMap[voter].getPrev(pollID);
uint nextID = dllMap[voter].getNext(pollID);
return (prevID == pollID) && (nextID == pollID);
}
// ---------------------------
// DOUBLE-LINKED-LIST HELPERS:
// ---------------------------
/**
@dev Gets the bytes32 commitHash property of target poll
@param voter Address of user to check against
@param pollID Integer identifier associated with target poll
@return Bytes32 hash property attached to target poll
*/
function getCommitHash(address voter, uint pollID) constant public returns (bytes32 commitHash) {
return bytes32(store.getAttribute(attrUUID(voter, pollID), "commitHash"));
}
/**
@dev Wrapper for getAttribute with attrName="numTokens"
@param voter Address of user to check against
@param pollID Integer identifier associated with target poll
@return Number of tokens committed to poll in sorted poll-linked-list
*/
function getNumTokens(address voter, uint pollID) constant public returns (uint numTokens) {
return store.getAttribute(attrUUID(voter, pollID), "numTokens");
}
/**
@dev Gets top element of sorted poll-linked-list
@param voter Address of user to check against
@return Integer identifier to poll with maximum number of tokens committed to it
*/
function getLastNode(address voter) constant public returns (uint pollID) {
return dllMap[voter].getPrev(0);
}
/**
@dev Gets the numTokens property of getLastNode
@param voter Address of user to check against
@return Maximum number of tokens committed in poll specified
*/
function getLockedTokens(address voter) constant public returns (uint numTokens) {
return getNumTokens(voter, getLastNode(voter));
}
// ----------------
// GENERAL HELPERS:
// ----------------
/**
@dev Checks if an expiration date has been reached
@param terminationDate Integer timestamp of date to compare current timestamp with
@return expired Boolean indication of whether the terminationDate has passed
*/
function isExpired(uint terminationDate) constant public returns (bool expired) {
return (block.timestamp > terminationDate);
}
/**
@dev Generates an identifier which associates a user and a poll together
@param pollID Integer identifier associated with target poll
@return UUID Hash which is deterministic from user and pollID
*/
function attrUUID(address user, uint pollID) public constant returns (bytes32 UUID) {
return sha3(user, pollID);
}
}