commit 665fbaf113e6d1bf0a81d4be866dfeb2d0c1319f
parent fa01e69dfd66475bb9e142d1b4600176e56c0296
Author: lash <dev@holbrook.no>
Date: Thu, 4 May 2023 09:47:20 +0100
Replace contract with ERC20 transfer based voting
Diffstat:
M | solidity/Vote.sol | | | 430 | +++++++++++++++++++++++++------------------------------------------------------ |
A | solidity/Vote_fail.sol | | | 319 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
2 files changed, 455 insertions(+), 294 deletions(-)
diff --git a/solidity/Vote.sol b/solidity/Vote.sol
@@ -9,349 +9,191 @@ contract ERC20Vote {
address public token;
struct Proposal {
- bytes32 digest;
- bytes32 state;
- uint256 voterMax;
+ bytes32 description;
+ bytes32 []options;
+ uint256 []optionVotes;
+ uint256 supply;
+ uint256 total;
+ uint256 blockDeadline;
+ uint24 targetVotePpm;
address proposer;
- uint256 ackBlockDeadline;
- uint256 voteBlockDeadline;
- uint256 voteTargetPpm;
- uint256 scanCursor;
- address []voters;
- //bool voterVote;
- uint8 voteMode;
- bool valid;
+ int16 result;
+ uint8 scanCursor;
bool active;
- bool ackScanDone;
- bool voteScanDone;
- bool result;
}
- Proposal emptyProposal;
-
- mapping ( address => uint256 ) voterState;
- address[] public voters;
- mapping ( uint256 => mapping ( address => uint256 ) ) ack;
- mapping ( uint256 => mapping ( address => uint256 ) ) vote;
- mapping ( uint256 => uint256 ) tally;
- mapping ( uint256 => uint256 ) budget;
-
- address newVoter;
Proposal[] public proposals;
- uint256 public proposalCursor;
-
- event ProposalAdded(uint256 indexed _proposalIdx, uint256 indexed _ackBlockDeadline, uint256 indexed voteTargetPpm);
- event VoterProposalAdded(uint256 indexed _proposalIdx, uint256 indexed _ackBlockDeadline, uint8 indexed _voteMode, address _voter);
- event VotesAdded(uint256 indexed _proposalIdx, address indexed _voter, uint256 indexed _total, uint256 _delta);
- event VotesWithdrawn(uint256 indexed _proposalIdx, address indexed _voter, uint256 indexed _total, uint256 _delta);
- event VoteResult(uint256 indexed _proposaldx, bool result);
- event ProposalInvalid(uint256 _proposalIdx);
-
- constructor(address _token, address _secondVoter) {
- token = _token;
- voters.push(msg.sender);
- voterState[msg.sender] = block.number;
- voters.push(_secondVoter);
- voterState[_secondVoter] = block.number;
- }
-
- // bounded sequential control of all proposals, to avoid gas lockout.
- // protects the voter population from changing between a vote has been proposed and it has been processed
- function scanProposal(uint256 _count) public returns (bool) {
- uint256 i;
- uint256 delta;
- Proposal storage proposal;
-
- if (proposalCursor + _count > proposals.length) {
- _count = proposals.length - proposalCursor;
- }
-
- for (i = proposalCursor; i < proposals.length; i++) {
- proposal = proposals[proposalCursor];
- if (!proposal.active) {
- delta += 1;
- }
- }
- proposalCursor += delta;
- if (proposalCursor == proposals.length) {
- return false;
- }
- return true;
- }
-
- // bounded processing of acks for a proposal, to avoid gas lockout.
- // when complete, relevant acks will be committed to the proposal and voting ratification can be possible.
- function scanAck(uint256 _proposalIdx, uint256 _count) public returns (bool) {
- Proposal storage proposal;
- uint256 i;
-
- proposal = getActive(_proposalIdx);
-
- require(!proposal.ackScanDone);
-
- if (proposal.scanCursor + _count > voters.length) {
- _count = voters.length - proposal.scanCursor;
- }
-
- for (i = proposal.scanCursor; i < proposal.scanCursor + _count; i++) {
- address voter;
- if (i == proposal.voterMax) {
- proposal.scanCursor = 0;
- proposal.ackScanDone = true;
- return false;
- }
- voter = voters[i];
- if (voterState[voter] > 0) {
- proposal.voters.push(voter);
- }
- proposal.scanCursor = i;
- }
- return true;
- }
-
- // bounded processing of votes, to avoid gas lockout.
- // can only be called after voting deadline.
- // adds all cast votes to the tally.
- function scanVote(uint256 _proposalIdx, uint256 _count) public returns (bool) {
- Proposal storage proposal;
- uint256 i;
+ uint256 public currentProposal;
- proposal = getActive(_proposalIdx);
- require(proposal.voteBlockDeadline <= block.timestamp);
- require(!proposal.voteScanDone);
+ mapping ( address => uint256 ) public balanceOf;
+ mapping ( address => uint256 ) proposalIdxLock;
- if (proposal.scanCursor + _count > proposal.voters.length) {
- _count = proposal.voters.length - proposal.scanCursor;
- }
-
- for (i = proposal.scanCursor; i < proposal.scanCursor + _count; i++) {
- address voter;
- if (checkProposalBalance(_proposalIdx, voter) == 0) {
- return false;
- }
- if (i == proposal.voters.length) {
- proposal.scanCursor = 0;
- proposal.voteScanDone = true;
- return false;
- }
- voter = voters[i];
- tally[_proposalIdx] += vote[_proposalIdx][voter];
- proposal.scanCursor = i;
- }
- return true;
- }
+ event ProposalAdded(uint256 indexed _blockDeadline, uint256 indexed voteTargetPpm, uint256 indexed _proposalIdx);
- // finalize proposal and result.
- function ratify(uint256 _proposalIdx) public returns (bool) {
- Proposal storage proposal;
- uint256 tallyPpm;
- uint256 budgetPpm;
-
- proposal = getActive(_proposalIdx);
-
- require(proposal.voteScanDone, "ERR_VOTE_SCAN_MISSING");
- if (proposal.voteMode > 0) {
- require(_proposalIdx == proposalCursor, "ERR_PREMATURE_VOTERVOTE");
- }
-
- tallyPpm = tally[_proposalIdx] * 1000000;
- budgetPpm = budget[_proposalIdx] * 1000000;
-
- if (tallyPpm / budgetPpm >= proposal.voteTargetPpm) {
- proposal.result = checkRatify(proposal);
- }
- proposal.active = false;
- emit VoteResult(_proposalIdx, proposal.result);
- return proposal.result;
- }
-
- function checkRatify(Proposal storage _proposal) private returns (bool) {
- if (_proposal.voteMode == 1) {
- if (voterState[newVoter] == 0) {
- voters.push(newVoter);
- } else {
- voterState[newVoter] = block.number;
- }
- newVoter = address(0);
- }
- if (_proposal.voteMode < 2) {
- return true;
- }
- voterState[newVoter] = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff;
- return true;
+ constructor(address _token) {
+ token = _token;
}
- // Common code for propose and proposeVoter.
- function proposeCore(bytes32 _digest, uint256 _ackBlockDeadline, uint256 _voteBlockDeadline, uint256 _voteTargetPpm) private returns (uint256) {
- require(_ackBlockDeadline > block.number);
- require(_voteBlockDeadline > _ackBlockDeadline);
- require(voters.length > 1);
-
- Proposal memory proposal;
- uint256 idx;
-
- proposal.digest = _digest;
- proposal.proposer = msg.sender;
- proposal.voterMax = voters.length - 1;
- proposal.ackBlockDeadline = _ackBlockDeadline;
- proposal.voteBlockDeadline = _voteBlockDeadline;
- proposal.valid = true;
- proposal.active = true;
- proposal.voteTargetPpm = _voteTargetPpm;
-
- idx = proposals.length;
- proposals.push(proposal);
+ function proposeCore(bytes32 _description, bytes32[] calldata _options, uint256 _blockDeadline, uint24 _targetVotePpm) private returns (uint256) {
+ Proposal memory l_proposal;
+ uint256 l_proposalIndex;
- return idx;
+ require(_options.length < 256, "ERR_TOO_MANY_OPTIONS");
+ l_proposal.proposer = msg.sender;
+ l_proposal.description = _description;
+ l_proposal.options = _options;
+ l_proposal.targetVotePpm = _targetVotePpm;
+ l_proposal.blockDeadline = _blockDeadline;
+ l_proposalIndex = proposals.length;
+ proposals.push(l_proposal);
+ l_proposal.supply = checkSupply(proposals[l_proposalIndex]);
+ return l_proposalIndex;
}
// Propose a vote on the subject described by digest.
- function propose(bytes32 _digest, uint256 _ackBlockDeadline, uint256 _voteBlockDeadline, uint256 _voteTargetPpm) public returns (uint256) {
+ function propose(bytes32 _description, bytes32[] calldata _options, uint256 _blockDeadline, uint24 _targetVotePpm) public returns (uint256) {
uint256 r;
- require(newVoter == address(0x0), "ERR_VOTERCHANGE_BLOCK");
-
- r = proposeCore(_digest, _ackBlockDeadline, _voteBlockDeadline, _voteTargetPpm);
- emit ProposalAdded(r, _ackBlockDeadline, _voteTargetPpm);
+ r = proposeCore(_description, _options, _blockDeadline, _targetVotePpm);
+ emit ProposalAdded(_blockDeadline, _targetVotePpm, r);
return r;
}
- // Propose addition of a new voter.
- function proposeAddVoter(address _voter, uint256 _ackBlockDeadline, uint256 _voteBlockDeadline, uint256 _voteTargetPpm) public returns (uint256) {
- return proposeVoterCore(_voter, _ackBlockDeadline, _voteBlockDeadline, _voteTargetPpm, 1);
- }
-
-
- // Propose removal of an existing voter.
- function proposeRemoveVoter(address _voter, uint256 _ackBlockDeadline, uint256 _voteBlockDeadline, uint256 _voteTargetPpm) public returns (uint256) {
- return proposeVoterCore(_voter, _ackBlockDeadline, _voteBlockDeadline, _voteTargetPpm, 2);
- }
-
- function proposeVoterCore(address _voter, uint256 _ackBlockDeadline, uint256 _voteBlockDeadline, uint256 _voteTargetPpm, uint8 _voteMode) public returns (uint256) {
- bytes32 voterDigest;
- bytes memory voterDigestMaterial;
- uint256 proposalIdx;
+ // Cast votes on an option by locking ERC20 token in contract.
+ // Votes may be divided on several options.
+ function vote(uint256 _optionIndex, uint256 _value) public {
+ Proposal storage proposal;
+ bool r;
+ bytes memory v;
- require(newVoter == address(0x0), "ERR_VOTERCHANGE_BLOCK");
+ proposal = proposals[currentProposal];
+ require(proposal.blockDeadline < block.number, "ERR_DEADLINE");
+ require(proposal.active, "ERR_PROPOSAL_INACTIVE");
+ if (proposalIdxLock[msg.sender] > 0) {
+ require(proposalIdxLock[msg.sender] == currentProposal, "ERR_RECOVER_FIRST");
+ }
+ require(_optionIndex < proposal.options.length, "ERR_OPTION_INVALID");
- newVoter = _voter;
- voterDigestMaterial = abi.encodePacked("bytes", bytes20(_voter));
- voterDigest = sha256(voterDigestMaterial);
+ (r, v) = token.call(abi.encodeWithSignature('transferFrom', msg.sender, this, _value));
+ require(r, "ERR_TOKEN");
+ r = abi.decode(v, (bool));
+ require(r, "ERR_TRANSFER");
- proposalIdx = proposeCore(voterDigest, _ackBlockDeadline, _voteBlockDeadline, _voteTargetPpm);
- proposals[proposalIdx].voteMode = _voteMode;
- emit VoterProposalAdded(proposalIdx, _ackBlockDeadline, _voteMode, _voter);
- return proposalIdx;
+ proposalIdxLock[msg.sender] = currentProposal;
+ balanceOf[msg.sender] += _value;
+ proposal.total += _value;
+ proposal.optionVotes[_optionIndex] += _value;
}
- // returns the active state.
- function getActive(uint256 _proposalIdx) private returns (Proposal storage) {
+ function scan(uint8 _count) public returns (int16) {
Proposal storage proposal;
-
- proposal = proposals[_proposalIdx];
- if (!proposal.valid) {
- return emptyProposal;
- }
- if (!proposal.active) {
- return emptyProposal;
+ uint8 i;
+ uint16 lead;
+ uint256 hi;
+ uint256 score;
+ uint8 c;
+
+ proposal = proposals[currentProposal];
+ require(proposal.active, "ERR_INACTIVE");
+ require(proposal.blockDeadline <= block.number, "ERR_PREMATURE");
+ require(proposal.scanCursor < proposal.options.length, "ERR_ALREADY_SCANNED");
+ c = proposal.scanCursor;
+ if (c + _count > proposal.options.length) {
+ _count = uint8(proposal.options.length) - c;
+ }
+
+ _count += c;
+ for (i = c; i < _count; i++) {
+ score = proposal.optionVotes[i];
+ if (score > 0 && score == hi) {
+ proposal.result = -2;
+ } else if (score > hi) {
+ hi = score;
+ lead = i;
+ proposal.result = int16(lead);
+ }
+ c += 1;
}
- if (block.number >= proposal.voteBlockDeadline) {
+ if (c == proposal.options.length) {
proposal.active = false;
}
- return proposal;
+ proposal.scanCursor = c;
+ return proposal.result;
}
- // register ack for a proposal
- function ackProposal(uint256 _proposalIdx) public returns (bool) {
+ function finalize() public returns (bool) {
Proposal storage proposal;
- uint256 balance;
+ uint256 l_total_m;
+ uint256 l_supply_m;
- proposal = getActive(_proposalIdx);
- require(proposal.active, "ERR_PROPOSAL_INACTIVE");
- require(proposal.ackBlockDeadline < block.number, "ERR_ACK_EXPIRE");
+ proposal = proposals[currentProposal];
+ require(proposal.result != 0, "ERR_SCAN_FIRST");
+ require(proposal.active, "ERR_INACTIVE");
- if (ack[_proposalIdx][msg.sender] > 0) {
+ if (proposal.result < 0) {
return false;
}
- balance = getBalance(msg.sender);
- ack[_proposalIdx][msg.sender] = balance;
- budget[_proposalIdx] += balance;
- return true;
- }
- // spend votes on proposal
- function spendVote(uint256 _proposalIdx, uint256 _amount) public returns (uint256) {
- uint256 balance;
- uint256 usedBalance;
- Proposal storage proposal;
+ l_total_m = proposal.total * 1000000;
+ l_supply_m = proposal.supply * 1000000;
- proposal = getActive(_proposalIdx);
- require(proposal.active, "ERR_PROPOSAL_INACTIVE");
-
- balance = checkProposalBalance(_proposalIdx, msg.sender);
- if (balance == 0) {
- return 0;
- }
-
- usedBalance = vote[_proposalIdx][msg.sender];
- require(balance - usedBalance >= _amount);
- usedBalance += _amount;
- vote[_proposalIdx][msg.sender] = usedBalance;
- emit VotesAdded(_proposalIdx, msg.sender, usedBalance, _amount);
+ if (l_supply_m / l_total_m < proposal.targetVotePpm) {
+ proposal.result = -3;
+ return false;
- return usedBalance;
+ }
+ return true;
}
+
+ function checkSupply(Proposal storage proposal) private returns (uint256) {
+ bool r;
+ bytes memory v;
+ uint256 l_supply;
- // withdraw spent votes on proposal
- function withdrawVote(uint256 _proposalIdx, uint256 _amount) public returns (uint256) {
- Proposal storage proposal;
- uint256 balance;
- uint256 usedBalance;
-
- proposal = getActive(_proposalIdx);
- require(proposal.active, "ERR_PROPOSAL_INACTIVE");
+ (r, v) = token.call(abi.encodeWithSignature('totalSupply'));
+ require(r, "ERR_TOKEN");
+ l_supply = abi.decode(v, (uint256));
- balance = checkProposalBalance(_proposalIdx, msg.sender);
- if (balance == 0) {
+ require(l_supply > 0, "ERR_ZERO_SUPPLY");
+ if (proposal.supply == 0) {
+ proposal.supply = l_supply;
+ } else {
+ proposal.active = false;
+ proposal.result = -1;
+ currentProposal += 1;
return 0;
}
-
- usedBalance = vote[_proposalIdx][msg.sender];
- require(usedBalance >= _amount);
- usedBalance -= _amount;
- vote[_proposalIdx][msg.sender] = usedBalance;
- emit VotesWithdrawn(_proposalIdx, msg.sender, usedBalance, _amount);
-
- return usedBalance;
+
+ return l_supply;
}
- // retrieve token balance from backing erc20 token.
- function getBalance(address _voter) private returns (uint256) {
+ // Recover tokens from a finished vote or from an active vote before deadline.
+ function recover() public returns (uint256) {
+ Proposal storage proposal;
+ bool r;
bytes memory v;
- bool ok;
+ uint256 l_value;
- (ok, v) = token.call(abi.encodeWithSignature('balanceOf', _voter));
- require(ok);
- return abi.decode(v, (uint256));
- }
+ proposal = proposals[currentProposal];
+ checkSupply(proposal);
- // invalidate proposal if terms of the vote has changed:
- // * current voter balance does not match voter balance at time of acknowledgement
- function checkProposalBalance(uint256 _proposalIdx, address _voter) private returns (uint256) {
- uint256 balance;
- uint256 origBalance;
-
- balance = getBalance(_voter);
- origBalance = ack[_proposalIdx][_voter];
- if (balance != origBalance) {
- Proposal storage proposal;
- proposal = proposals[_proposalIdx];
- proposal.valid = false;
- proposal.active = false;
- emit ProposalInvalid(_proposalIdx);
- return 0;
+ l_value = balanceOf[msg.sender];
+ if (proposalIdxLock[msg.sender] == currentProposal) {
+ if (proposal.blockDeadline <= block.number) {
+ require(proposal.result == 0, "ERR_PREMATURE");
+ } else {
+ proposal.total -= l_value;
+ }
}
- return balance;
+
+ balanceOf[msg.sender] = 0;
+ proposalIdxLock[msg.sender] = 0;
+ (r, v) = token.call(abi.encodeWithSignature('transfer', msg.sender, l_value));
+ require(r, "ERR_TOKEN");
+ r = abi.decode(v, (bool));
+ require(r, "ERR_TRANSFER");
+
+ return l_value;
}
}
diff --git a/solidity/Vote_fail.sol b/solidity/Vote_fail.sol
@@ -0,0 +1,319 @@
+pragma solidity ^0.8.0;
+
+// Author: Louis Holbrook <dev@holbrook.no> 0826EDA1702D1E87C6E2875121D2E7BB88C2A746
+// SPDX-License-Identifier: AGPL-3.0-or-later
+// File-Version: 1
+// Description: Voting contract using ERC20 tokens as shares
+
+contract ERC20Vote {
+ address public token;
+
+ struct Proposal {
+ bytes32 digest;
+ bytes32 state;
+ uint256 voterMax;
+ address proposer;
+ uint256 ackBlockDeadline;
+ uint256 voteBlockDeadline;
+ uint256 voteTargetPpm;
+ uint256 scanCursor;
+ address []voters;
+ bool valid;
+ bool active;
+ bool ackScanDone;
+ bool voteScanDone;
+ bool result;
+ }
+ Proposal emptyProposal;
+
+ mapping ( address => uint256 ) voterState;
+ address[] voters;
+ mapping ( uint256 => mapping ( address => uint256 ) ) ack;
+ mapping ( uint256 => mapping ( address => uint256 ) ) vote;
+ mapping ( uint256 => uint256 ) tally;
+ mapping ( uint256 => uint256 ) budget;
+
+ Proposal[] public proposals;
+
+ uint256 public proposalCursor;
+
+ event ProposalAdded(uint256 indexed _proposalIdx, uint256 indexed _ackBlockDeadline, uint256 indexed voteTargetPpm);
+ event VotesAdded(uint256 indexed _proposalIdx, address indexed _voter, uint256 indexed _total, uint256 _delta);
+ event VotesWithdrawn(uint256 indexed _proposalIdx, address indexed _voter, uint256 indexed _total, uint256 _delta);
+ event VoteResult(uint256 indexed _proposaldx, bool result);
+ event ProposalInvalid(uint256 _proposalIdx);
+
+ constructor(address _token, address _secondVoter) {
+ token = _token;
+ voters.push(msg.sender);
+ voterState[msg.sender] = block.number;
+ voters.push(_secondVoter);
+ voterState[_secondVoter] = block.number;
+ }
+
+ // Resolve voter address by index voter was added.
+ // Deactivates voter if balance is 0, activates voter if balance is 1.
+ function getVoter(uint256 _voterIdx) public returns (address) {
+ uint256 balance;
+ address voter;
+
+ voter = voters[_voterIdx];
+ balance = getBalance(voter);
+ if (balance == 0) {
+ voterState[voter] = 0;
+ } else if (voterState[voter] == 0) {
+ voterState[voter] = block.number;
+ }
+ return voter;
+ }
+
+ // bounded sequential control of all proposals, to avoid gas lockout.
+ // protects the voter population from changing between a vote has been proposed and it has been processed
+ function scanProposal(uint256 _count) public returns (bool) {
+ uint256 i;
+ uint256 delta;
+ Proposal storage proposal;
+
+ if (proposalCursor + _count > proposals.length) {
+ _count = proposals.length - proposalCursor;
+ }
+
+ for (i = proposalCursor; i < proposals.length; i++) {
+ proposal = proposals[proposalCursor];
+ if (!proposal.active) {
+ delta += 1;
+ }
+ }
+ proposalCursor += delta;
+ if (proposalCursor == proposals.length) {
+ return false;
+ }
+ return true;
+ }
+
+ // bounded processing of acks for a proposal, to avoid gas lockout.
+ // when complete, relevant acks will be committed to the proposal and voting ratification can be possible.
+ function scanAck(uint256 _proposalIdx, uint256 _count) public returns (bool) {
+ Proposal storage proposal;
+ uint256 i;
+
+ proposal = getActive(_proposalIdx);
+
+ require(!proposal.ackScanDone);
+
+ if (proposal.scanCursor + _count > voters.length) {
+ _count = voters.length - proposal.scanCursor;
+ }
+
+ for (i = proposal.scanCursor; i < proposal.scanCursor + _count; i++) {
+ address voter;
+ if (i == proposal.voterMax) {
+ proposal.scanCursor = 0;
+ proposal.ackScanDone = true;
+ return false;
+ }
+ voter = getVoter(i); //voters[i];
+ if (voterState[voter] > 0) {
+ proposal.voters.push(voter);
+ }
+ proposal.scanCursor = i;
+ }
+ return true;
+ }
+
+ // bounded processing of votes, to avoid gas lockout.
+ // can only be called after voting deadline.
+ // adds all cast votes to the tally.
+ function scanVote(uint256 _proposalIdx, uint256 _count) public returns (bool) {
+ Proposal storage proposal;
+ uint256 i;
+
+ proposal = getActive(_proposalIdx);
+ require(proposal.voteBlockDeadline <= block.timestamp);
+ require(!proposal.voteScanDone);
+
+ if (proposal.scanCursor + _count > proposal.voters.length) {
+ _count = proposal.voters.length - proposal.scanCursor;
+ }
+
+ for (i = proposal.scanCursor; i < proposal.scanCursor + _count; i++) {
+ address voter;
+ if (checkProposalBalance(_proposalIdx, voter) == 0) {
+ return false;
+ }
+ if (i == proposal.voters.length) {
+ proposal.scanCursor = 0;
+ proposal.voteScanDone = true;
+ return false;
+ }
+ voter = voters[i];
+ tally[_proposalIdx] += vote[_proposalIdx][voter];
+ proposal.scanCursor = i;
+ }
+ return true;
+ }
+
+ // finalize proposal and result.
+ function ratify(uint256 _proposalIdx) public returns (bool) {
+ Proposal storage proposal;
+ uint256 tallyPpm;
+ uint256 budgetPpm;
+
+ proposal = getActive(_proposalIdx);
+
+ require(proposal.voteScanDone, "ERR_VOTE_SCAN_MISSING");
+
+ tallyPpm = tally[_proposalIdx] * 1000000;
+ budgetPpm = budget[_proposalIdx] * 1000000;
+
+ if (tallyPpm / budgetPpm >= proposal.voteTargetPpm) {
+ proposal.result = true;
+ }
+ proposal.active = false;
+ emit VoteResult(_proposalIdx, proposal.result);
+ return proposal.result;
+ }
+
+ // Common code for propose and proposeVoter.
+ function proposeCore(bytes32 _digest, uint256 _ackBlockDeadline, uint256 _voteBlockDeadline, uint256 _voteTargetPpm) private returns (uint256) {
+ require(_ackBlockDeadline > block.number);
+ require(_voteBlockDeadline > _ackBlockDeadline);
+ require(voters.length > 1);
+
+ Proposal memory proposal;
+ uint256 idx;
+
+ proposal.digest = _digest;
+ proposal.proposer = msg.sender;
+ proposal.voterMax = voters.length - 1;
+ proposal.ackBlockDeadline = _ackBlockDeadline;
+ proposal.voteBlockDeadline = _voteBlockDeadline;
+ proposal.valid = true;
+ proposal.active = true;
+ proposal.voteTargetPpm = _voteTargetPpm;
+
+ idx = proposals.length;
+ proposals.push(proposal);
+
+ return idx;
+ }
+
+ // Propose a vote on the subject described by digest.
+ function propose(bytes32 _digest, uint256 _ackBlockDeadline, uint256 _voteBlockDeadline, uint256 _voteTargetPpm) public returns (uint256) {
+ uint256 r;
+
+ r = proposeCore(_digest, _ackBlockDeadline, _voteBlockDeadline, _voteTargetPpm);
+ emit ProposalAdded(r, _ackBlockDeadline, _voteTargetPpm);
+ return r;
+ }
+
+ // returns the active state.
+ function getActive(uint256 _proposalIdx) private returns (Proposal storage) {
+ Proposal storage proposal;
+
+ proposal = proposals[_proposalIdx];
+ if (!proposal.valid) {
+ return emptyProposal;
+ }
+ if (!proposal.active) {
+ return emptyProposal;
+ }
+ if (block.number >= proposal.voteBlockDeadline) {
+ proposal.active = false;
+ }
+ return proposal;
+ }
+
+ // register ack for a proposal
+ function ackProposal(uint256 _proposalIdx) public returns (bool) {
+ Proposal storage proposal;
+ uint256 balance;
+
+ proposal = getActive(_proposalIdx);
+ require(proposal.active, "ERR_PROPOSAL_INACTIVE");
+ require(proposal.ackBlockDeadline < block.number, "ERR_ACK_EXPIRE");
+
+ if (ack[_proposalIdx][msg.sender] > 0) {
+ return false;
+ }
+ balance = getBalance(msg.sender);
+ ack[_proposalIdx][msg.sender] = balance;
+ budget[_proposalIdx] += balance;
+ return true;
+ }
+
+ // spend votes on proposal
+ function spendVote(uint256 _proposalIdx, uint256 _amount) public returns (uint256) {
+ uint256 balance;
+ uint256 usedBalance;
+ Proposal storage proposal;
+
+ proposal = getActive(_proposalIdx);
+ require(proposal.active, "ERR_PROPOSAL_INACTIVE");
+
+ balance = checkProposalBalance(_proposalIdx, msg.sender);
+ if (balance == 0) {
+ return 0;
+ }
+
+ usedBalance = vote[_proposalIdx][msg.sender];
+ require(balance - usedBalance >= _amount);
+ usedBalance += _amount;
+ vote[_proposalIdx][msg.sender] = usedBalance;
+ emit VotesAdded(_proposalIdx, msg.sender, usedBalance, _amount);
+
+ return usedBalance;
+ }
+
+ // withdraw spent votes on proposal
+ function withdrawVote(uint256 _proposalIdx, uint256 _amount) public returns (uint256) {
+ Proposal storage proposal;
+ uint256 balance;
+ uint256 usedBalance;
+
+ proposal = getActive(_proposalIdx);
+ require(proposal.active, "ERR_PROPOSAL_INACTIVE");
+
+ balance = checkProposalBalance(_proposalIdx, msg.sender);
+ if (balance == 0) {
+ return 0;
+ }
+
+ usedBalance = vote[_proposalIdx][msg.sender];
+ require(usedBalance >= _amount);
+ usedBalance -= _amount;
+ vote[_proposalIdx][msg.sender] = usedBalance;
+ emit VotesWithdrawn(_proposalIdx, msg.sender, usedBalance, _amount);
+
+ return usedBalance;
+ }
+
+ // retrieve token balance from backing erc20 token.
+ function getBalance(address _voter) private returns (uint256) {
+ bytes memory v;
+ bool ok;
+
+ (ok, v) = token.call(abi.encodeWithSignature('balanceOf', _voter));
+ require(ok);
+ return abi.decode(v, (uint256));
+ }
+
+ // invalidate proposal if terms of the vote has changed:
+ // * current voter balance does not match voter balance at time of acknowledgement
+ function checkProposalBalance(uint256 _proposalIdx, address _voter) private returns (uint256) {
+ uint256 balance;
+ uint256 origBalance;
+
+ balance = getBalance(_voter);
+ origBalance = ack[_proposalIdx][_voter];
+ if (balance != origBalance) {
+ Proposal storage proposal;
+ proposal = proposals[_proposalIdx];
+ proposal.valid = false;
+ proposal.active = false;
+ emit ProposalInvalid(_proposalIdx);
+ return 0;
+ }
+ return balance;
+ }
+}