evm-tokenvote

Voting machine using ERC20 tokens as votes.
Log | Files | Refs | README

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 }