Voter.sol (14260B)
1 pragma solidity ^0.8.0; 2 3 // Author: Louis Holbrook <dev@holbrook.no> 0826EDA1702D1E87C6E2875121D2E7BB88C2A746 4 // SPDX-License-Identifier: AGPL-3.0-or-later 5 // File-Version: 1 6 // Description: Voting contract using ERC20 tokens as shares 7 8 contract ERC20Vote { 9 uint8 constant STATE_INIT = 1; // proposal has been initiated. 10 uint8 constant STATE_FINAL = 2; // proposal has been finalized. 11 uint8 constant STATE_SCANNED = 4; // proposal votes have been scanned (this can be done after finalization). 12 uint8 constant STATE_INSUFFICIENT = 8; // proposal did not attract minimum participation before deadline. 13 uint8 constant STATE_TIED = 16; // two or more proposal options have the same amount of votes. 14 uint8 constant STATE_SUPPLYCHANGE = 32; // supply changed while voting was underway. 15 uint8 constant STATE_IMMEDIATE = 64; // minimum participation was attained before deadline. 16 uint8 constant STATE_CANCELLED = 128; // vote to cancel the proposal has the majority. 17 //uint16 constant STATE_DUE = 256; // votes are ready to be tallied. 18 19 bytes32 constant INTERNALS_BLOCK_WAIT_LIMIT = 0x67ca084db32598c571e2ad2dc8b95679c3fa14c63213935dfd8f0a158ff65c57; 20 21 address public token; 22 23 struct Proposal { 24 bytes32 description; 25 bytes32 []options; 26 uint256 []optionVotes; 27 uint256 cancelVotes; 28 uint256 supply; 29 uint256 total; 30 uint256 blockDeadline; 31 uint24 targetVotePpm; 32 address proposer; 33 uint8 state; 34 uint8 scanCursor; 35 bool internals; // vote to govern internal mechanics of the contract. May not contain options. 36 } 37 38 // sequential index of all added proposals. 39 Proposal[] proposals; 40 41 // optional access control registry of which addresses to allow voting. 42 address voterRegistry; 43 44 // optional access control registry of which addresses to allow adding proposals. 45 address proposerRegistry; 46 47 // proposal currently being voted on (provided the proposal has INIT set). 48 uint256 currentProposal; 49 50 // if set, the proposal will be cancelled with supply has been changed. 51 // The proposal will be marked accordingly to disambiguate the cancellation from a cancel vote. 52 bool protectSupply; 53 54 // the maximum amount of block waits for a vote 55 uint256 public blockWaitLimit; 56 57 // the deadline of the last added proposal 58 uint256 lastBlockDeadline; 59 60 // value of tokens held in escrow per account. 61 mapping ( address => uint256 ) public balanceOf; 62 63 // links escow to specific proposal, controls whether tokens can be withdrawn. 64 mapping ( address => uint256 ) proposalIdxLock; 65 66 // a new proposal has been added to the proposals index. 67 event ProposalAdded(uint256 indexed _blockDeadline, uint256 indexed voteTargetPpm, uint256 indexed _proposalIdx); 68 69 // the current proposal has been finalized; whether successful, cancelled or insufficient vote. 70 event ProposalCompleted(uint256 indexed _proposalIdx, bool indexed _cancelled, bool indexed _insufficient, uint256 _totalVote); 71 72 // token must be specified. it is the caller's responsibility to ensure that the token has a value interface. 73 // if a registry is the zero-address, it will be deactivated. 74 constructor(address _token, bool _protectSupply, address _voterRegistry, address _proposerRegistry) { 75 Proposal memory l_proposal; 76 token = _token; 77 voterRegistry = _voterRegistry; 78 proposerRegistry = _proposerRegistry; 79 proposals.push(l_proposal); 80 currentProposal = 1; 81 protectSupply = _protectSupply; 82 } 83 84 // create new proposal 85 function propose(bytes32 _description, uint256 _blockWait, uint24 _targetVotePpm) public returns (uint256) { 86 return proposeCore(_description, _blockWait, _targetVotePpm, false); 87 } 88 89 // create new proposal to change internal settings in contract 90 function proposeInternal(bytes32 _description, bytes32 _option, uint256 _blockWait, uint24 _targetVotePpm) public returns (uint256) { 91 bool l_descriptionValid; 92 uint256 l_proposalIndex; 93 94 if (_description == INTERNALS_BLOCK_WAIT_LIMIT) { 95 l_descriptionValid = true; 96 } 97 require(l_descriptionValid, "ERR_INVALID_INTERNAL"); 98 l_proposalIndex = proposeCore(_description, _blockWait, _targetVotePpm, true); 99 addOption(l_proposalIndex, _option); 100 return l_proposalIndex; 101 } 102 103 // common code for proposal creation 104 function proposeCore(bytes32 _description, uint256 _blockWait, uint24 _targetVotePpm, bool _internals) private returns (uint256) { 105 Proposal memory l_proposal; 106 uint256 l_proposalIndex; 107 uint256 l_blockDeadline; 108 109 if (blockWaitLimit > 0) { 110 require(_blockWait <= blockWaitLimit, "ERR_WAIT"); 111 } 112 mustAccount(msg.sender, proposerRegistry); 113 114 l_proposalIndex = proposals.length - 1; 115 l_proposal.proposer = msg.sender; 116 l_proposal.description = _description; 117 l_proposal.targetVotePpm = _targetVotePpm; 118 l_blockDeadline = block.number + _blockWait; 119 l_proposal.blockDeadline = l_blockDeadline; 120 l_proposal.state = STATE_INIT; 121 l_proposal.internals = _internals; 122 proposals.push(l_proposal); 123 l_proposal.supply = checkSupply(proposals[l_proposalIndex + 1]); 124 125 emit ProposalAdded(l_blockDeadline, _targetVotePpm, l_proposalIndex); 126 return l_proposalIndex; 127 } 128 129 // Add a voting option to proposal 130 function addOption(uint256 _proposalIdx, bytes32 _optionDescription) public { 131 Proposal storage l_proposal; 132 133 l_proposal = proposals[_proposalIdx + 1]; 134 l_proposal.options.push(_optionDescription); 135 l_proposal.optionVotes.push(0); 136 } 137 138 // get proposal by index 139 function getProposal(uint256 _proposalIdx) public view returns(Proposal memory) { 140 return proposals[_proposalIdx + 1]; 141 } 142 143 // get currently active proposal 144 function getCurrentProposal() public view returns(Proposal memory) { 145 Proposal storage proposal; 146 147 proposal = proposals[currentProposal]; 148 require(proposal.state & STATE_INIT > 0, "ERR_NO_CURRENT_PROPOSAL"); 149 return proposal; 150 } 151 152 // get description for option 153 function getOption(uint256 _proposalIdx, uint256 _optionIdx) public view returns (bytes32) { 154 Proposal storage proposal; 155 156 proposal = proposals[_proposalIdx + 1]; 157 return proposal.options[_optionIdx]; 158 } 159 160 // number of options in proposal 161 function optionCount(uint256 _proposalIdx) public view returns(uint256) { 162 Proposal storage proposal; 163 164 proposal = proposals[_proposalIdx + 1]; 165 return proposal.options.length; 166 } 167 168 // total number of votes (across all options) 169 function voteCount(uint256 _proposalIdx, uint256 _optionIdx) public view returns(uint256) { 170 Proposal storage proposal; 171 172 proposal = proposals[_proposalIdx + 1]; 173 if (proposal.options.length == 0) { 174 require(_optionIdx == 0, "ERR_NO_OPTIONS"); 175 return proposal.total; 176 } 177 return proposal.optionVotes[_optionIdx]; 178 } 179 180 // reverts on unregistered account if an accounts registry has been added. 181 function mustAccount(address _account, address _registry) private { 182 bool r; 183 bytes memory v; 184 185 if (_registry == address(0)) { 186 return; 187 } 188 189 (r, v) = _registry.call(abi.encodeWithSignature('have(address)', _account)); 190 require(r, "ERR_REGISTRY"); 191 r = abi.decode(v, (bool)); 192 require(r, "ERR_UNAUTH_ACCOUNT"); 193 } 194 195 // Cast votes on an option by locking ERC20 token in contract. 196 // Votes may be divided on several options as long as balance is sufficient. 197 // If false is returned, proposal has been invalidated. 198 function voteOption(uint256 _optionIndex, uint256 _value) public returns (bool) { 199 Proposal storage proposal; 200 201 mustAccount(msg.sender, voterRegistry); 202 proposal = proposals[currentProposal]; 203 if (!voteable(proposal)) { 204 return false; 205 } 206 if (proposal.options.length > 0) { 207 require(_optionIndex < proposal.options.length, "ERR_OPTION_INVALID"); 208 } 209 voteCore(proposal, _value); 210 if (proposal.options.length > 0) { 211 proposal.optionVotes[_optionIndex] += _value; 212 } 213 return true; 214 } 215 216 // common code for all vote methods 217 // executes the token transfer, updates total and sets immediate flag if target vote has been met 218 function voteCore(Proposal storage proposal, uint256 _value) private { 219 bool r; 220 bytes memory v; 221 222 (r, v) = token.call(abi.encodeWithSignature('transferFrom(address,address,uint256)', msg.sender, this, _value)); 223 require(r, "ERR_TOKEN"); 224 r = abi.decode(v, (bool)); 225 require(r, "ERR_TRANSFER"); 226 227 proposalIdxLock[msg.sender] = currentProposal; 228 balanceOf[msg.sender] += _value; 229 proposal.total += _value; 230 if (haveQuotaFor(proposal, proposal.total)) { 231 if (haveQuotaFor(proposal, proposal.cancelVotes)) { 232 proposal.state |= STATE_CANCELLED | STATE_IMMEDIATE; 233 } 234 if (proposal.options.length < 2) { 235 proposal.state |= STATE_IMMEDIATE; 236 } 237 238 } 239 } 240 241 // Cast vote for a proposal without options 242 // Can be called multiple times as long as balance is sufficient. 243 // If false is returned, proposal has been invalidated. 244 function vote(uint256 _value) public returns (bool) { 245 Proposal storage proposal; 246 247 mustAccount(msg.sender, voterRegistry); 248 proposal = proposals[currentProposal]; 249 require(proposal.options.length < 2); // allow both no options and single option. 250 return voteOption(0, _value); 251 } 252 253 // cast vote to cancel proposal 254 // will set immediate termination and cancelled flag if has target vote majority 255 function voteCancel(uint256 _value) public returns (bool) { 256 Proposal storage proposal; 257 258 mustAccount(msg.sender, voterRegistry); 259 proposal = proposals[currentProposal]; 260 if (!voteable(proposal)) { 261 return false; 262 } 263 proposal.cancelVotes += _value; 264 voteCore(proposal, _value); 265 266 return true; 267 } 268 269 // proposal is voteable if: 270 // * has been initialized 271 // * within deadline 272 // * voter released tokens from previous vote 273 function voteable(Proposal storage proposal) private returns(bool) { 274 require(proposal.state & STATE_INIT > 0, "ERR_PROPOSAL_INACTIVE"); 275 if (checkSupply(proposal) == 0) { 276 return false; 277 } 278 require(proposal.blockDeadline > block.number, "ERR_DEADLINE"); 279 if (proposalIdxLock[msg.sender] > 0) { 280 require(proposalIdxLock[msg.sender] == currentProposal, "ERR_WITHDRAW_FIRST"); 281 } 282 return true; 283 } 284 285 286 // Optionally scan the results for a proposal to make result visible. 287 // Returns false as long as there are more options to scan. 288 function scan(uint256 _proposalIndex, uint8 _count) public returns (bool) { 289 Proposal storage proposal; 290 uint8 i; 291 uint16 lead; 292 uint256 hi; 293 uint256 score; 294 uint8 c; 295 uint8 state; 296 297 proposal = proposals[_proposalIndex + 1]; 298 if (proposal.state & STATE_IMMEDIATE == 0) { 299 require(proposal.blockDeadline <= block.number, "ERR_PREMATURE"); 300 } 301 if (proposal.state & STATE_SCANNED > 0) { 302 return false; 303 } 304 305 if (proposal.options.length == 0) { 306 proposal.state |= STATE_SCANNED; 307 return true; 308 } 309 310 c = proposal.scanCursor; 311 if (c + _count > proposal.options.length) { 312 _count = uint8(proposal.options.length) - c; 313 } 314 315 _count += c; 316 state = proposal.state; 317 for (i = c; i < _count; i++) { 318 score = proposal.optionVotes[i]; 319 if (score > 0 && score == hi) { 320 state |= STATE_TIED; 321 } else if (score > hi) { 322 hi = score; 323 lead = i; 324 state &= ~STATE_TIED; 325 } 326 c += 1; 327 } 328 proposal.scanCursor = c; 329 proposal.state = state; 330 if (proposal.scanCursor >= proposal.options.length) { 331 proposal.state |= STATE_SCANNED; 332 } 333 return proposal.state & STATE_SCANNED > 0; 334 } 335 336 // finalize the results after scanning for winning result. 337 // will record and return whether voting participation was insufficient. 338 function finalize() public returns (bool) { 339 Proposal storage proposal; 340 bool r; 341 342 proposal = proposals[currentProposal]; 343 require(proposal.state & STATE_FINAL == 0, "ERR_ALREADY_STATE_FINAL"); 344 if (checkSupply(proposal) == 0) { 345 return false; 346 } 347 if (block.number > proposal.blockDeadline) { 348 require(proposal.state & STATE_CANCELLED == 0, "ERR_PREMATURE"); 349 } 350 if (!haveQuotaFor(proposal, proposal.total)) { 351 proposal.state |= STATE_INSUFFICIENT; 352 r = true; 353 } 354 proposal.state |= STATE_FINAL; 355 356 if (proposal.internals) { 357 finalizeInternal(proposal.description, proposal.options[0]); 358 } 359 emit ProposalCompleted(currentProposal - 1, proposal.state & STATE_CANCELLED > 0, r, proposal.total); 360 361 currentProposal += 1; 362 return !r; 363 } 364 365 // execute state changes for internals proposals 366 function finalizeInternal(bytes32 _description, bytes32 _optionDescription) private { 367 if (_description == INTERNALS_BLOCK_WAIT_LIMIT) { 368 blockWaitLimit = uint256(_optionDescription); 369 } 370 } 371 372 // check if target vote count has been met 373 function haveQuotaFor(Proposal storage proposal, uint256 _value) private view returns (bool) { 374 uint256 l_total_m; 375 l_total_m = _value * 1000000; 376 return l_total_m / proposal.supply >= proposal.targetVotePpm; 377 } 378 379 // should be checked for proposal creation, each recorded vote and finalization. 380 function checkSupply(Proposal storage proposal) private returns (uint256) { 381 bool r; 382 bytes memory v; 383 uint256 l_supply; 384 385 (r, v) = token.call(abi.encodeWithSignature('totalSupply()')); 386 require(r, "ERR_TOKEN"); 387 l_supply = abi.decode(v, (uint256)); 388 389 require(l_supply > 0, "ERR_ZERO_SUPPLY"); 390 if (proposal.supply == 0) { 391 proposal.supply = l_supply; 392 } else if (l_supply != proposal.supply) { 393 proposal.state |= STATE_SUPPLYCHANGE; 394 proposal.state |= STATE_FINAL; 395 if (protectSupply) { 396 currentProposal += 1; 397 proposal.state |= STATE_CANCELLED; 398 return 0; 399 } 400 } 401 402 return l_supply; 403 } 404 405 // Implements Escrow 406 // Can only be called with the full balance held by the contract. Use withdraw() instead. 407 function withdraw(uint256 _value) public returns (uint256) { 408 require(_value == balanceOf[msg.sender], "ERR_MUST_WITHDRAW_ALL"); 409 return withdraw(); 410 } 411 412 // Implements Escrow 413 // Recover tokens from a finished vote or from an active vote before deadline. 414 function withdraw() public returns (uint256) { 415 Proposal storage proposal; 416 bool r; 417 bytes memory v; 418 uint256 l_value; 419 420 l_value = balanceOf[msg.sender]; 421 if (proposalIdxLock[msg.sender] == currentProposal) { 422 proposal = proposals[currentProposal]; 423 require(proposal.state & STATE_FINAL > 0, "ERR_PREMATURE"); 424 } 425 426 balanceOf[msg.sender] = 0; 427 proposalIdxLock[msg.sender] = 0; 428 (r, v) = token.call(abi.encodeWithSignature('transfer(address,uint256)', msg.sender, l_value)); 429 require(r, "ERR_TOKEN"); 430 r = abi.decode(v, (bool)); 431 require(r, "ERR_TRANSFER"); 432 433 return l_value; 434 } 435 436 // supportsInterface TokenVoter f2e0bfeb 437 }