diff --git a/contracts/extensions/VotingBase.sol b/contracts/extensions/VotingBase.sol index 63b7451f5b..aa29613937 100644 --- a/contracts/extensions/VotingBase.sol +++ b/contracts/extensions/VotingBase.sol @@ -38,9 +38,11 @@ contract VotingBase is DSMath { } // Data structures - enum PollState { Open, Reveal, Closed } + enum PollState { Pending, Open, Reveal, Closed, Executed } struct Poll { + bool executed; + address creator; uint256 pollCloses; uint256[] voteCounts; } @@ -48,8 +50,46 @@ contract VotingBase is DSMath { // Storage uint256 pollCount; mapping (uint256 => Poll) polls; + mapping (uint256 => mapping (uint256 => bytes)) pollActions; + + // Modifiers + modifier onlyCreator(uint256 _pollId) { + require(polls[_pollId].creator == msg.sender, "voting-base-only-creator"); + _; + } + + modifier pollPending(uint256 _pollId) { + require(getPollState(_pollId) == PollState.Pending, "voting-base-poll-not-pending"); + _; + } // Functions + function createPoll() public { + pollCount += 1; + polls[pollCount].creator = msg.sender; + } + + function addPollAction(uint256 _pollId, bytes memory _action) public onlyCreator(_pollId) pollPending(_pollId) { + pollActions[_pollId][polls[_pollId].voteCounts.length] = _action; + polls[_pollId].voteCounts.push(0); + } + + function openPoll(uint256 _pollId, uint256 _duration) public onlyCreator(_pollId) pollPending(_pollId) { + require(polls[_pollId].voteCounts.length > 1, "voting-base-insufficient-poll-actions"); + polls[_pollId].pollCloses = add(now, _duration); + } + + function executePoll(uint256 _pollId) public returns (bool) { + require(getPollState(_pollId) != PollState.Executed, "voting-base-poll-already-executed"); + require(getPollState(_pollId) == PollState.Closed, "voting-base-poll-not-closed"); + + polls[_pollId].executed = true; + + uint256 winner = getPollWinner(_pollId); + bytes storage action = pollActions[_pollId][winner]; + return executeCall(address(colony), action); + } + function getPollCount() public view returns (uint256) { return pollCount; } @@ -58,13 +98,40 @@ contract VotingBase is DSMath { poll = polls[_pollId]; } - function getPollState(uint256 _pollId) internal view returns (PollState) { - if (now < polls[_pollId].pollCloses) { + function getPollState(uint256 _pollId) public view returns (PollState) { + Poll storage poll = polls[_pollId]; + if (poll.pollCloses == 0) { + return PollState.Pending; + } else if (now < poll.pollCloses) { return PollState.Open; - } else if (now < add(polls[_pollId].pollCloses, REVEAL_PERIOD)) { + } else if (now < add(poll.pollCloses, REVEAL_PERIOD)) { return PollState.Reveal; - } else { + } else if (!poll.executed) { return PollState.Closed; + } else { + return PollState.Executed; + } + } + + function getPollWinner(uint256 _pollId) public view returns (uint256 winner) { + uint256[] storage voteCounts = polls[_pollId].voteCounts; + + // TODO: Handle ties! + for (uint256 i; i < voteCounts.length; i += 1) { + if (voteCounts[i] > voteCounts[winner]) { + winner = i; + } + } + } + + function executeCall(address to, bytes memory data) internal returns (bool success) { + assembly { + // call contract at address a with input mem[in…(in+insize)) + // providing g gas and v wei and output area mem[out…(out+outsize)) + // returning 0 on error (eg. out of gas) and 1 on success + + // call(g, a, v, in, insize, out, outsize) + success := call(gas, to, 0, add(data, 0x20), mload(data), 0, 0) } } diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index 6abdcd29d0..c5c59c6054 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -26,34 +26,32 @@ contract VotingReputation is VotingBase, PatriciaTreeProofs { constructor(address _colony) public VotingBase(_colony) {} - struct RepDatum { + struct RepInfo { bytes32 rootHash; uint256 skillId; } - mapping (uint256 => RepDatum) repData; + mapping (uint256 => RepInfo) repInfo; // The UserVote type here is just the bytes32 voteSecret mapping (address => mapping (uint256 => bytes32)) userVotes; - function createPoll(uint256 _numOutcomes, uint256 _duration, uint256 _skillId) public { - pollCount += 1; - - polls[pollCount] = Poll({ - pollCloses: add(now, _duration), - voteCounts: new uint256[](_numOutcomes) - }); - - repData[pollCount] = RepDatum({ + function setPollRepInfo(uint256 _pollId, uint256 _skillId) public onlyCreator(_pollId) pollPending(_pollId) { + repInfo[pollCount] = RepInfo({ rootHash: colonyNetwork.getReputationRootHash(), skillId: _skillId }); } - function submitVote(uint256 _pollId, bytes32 _voteSecret) public { - require(getPollState(_pollId) == PollState.Open, "colony-rep-voting-poll-not-open"); + // Override the base function + function openPoll(uint256 _pollId, uint256 _duration) public onlyCreator(_pollId) pollPending(_pollId) { + require(repInfo[_pollId].rootHash != 0x0, "voting-rep-poll-no-root-hash"); + super.openPoll(_pollId, _duration); + } + function submitVote(uint256 _pollId, bytes32 _voteSecret) public { + require(getPollState(_pollId) == PollState.Open, "voting-rep-poll-not-open"); userVotes[msg.sender][_pollId] = _voteSecret; } @@ -68,12 +66,11 @@ contract VotingReputation is VotingBase, PatriciaTreeProofs { ) public { - uint256 pollCloses = polls[_pollId].pollCloses; - require(getPollState(_pollId) != PollState.Open, "colony-rep-voting-poll-still-open"); + require(getPollState(_pollId) != PollState.Open, "voting-rep-poll-still-open"); bytes32 voteSecret = userVotes[msg.sender][_pollId]; - require(voteSecret == getVoteSecret(_salt, _vote), "colony-rep-voting-secret-no-match"); - require(_vote < polls[_pollId].voteCounts.length, "colony-rep-voting-invalid-vote"); + require(voteSecret == getVoteSecret(_salt, _vote), "voting-rep-secret-no-match"); + require(_vote < polls[_pollId].voteCounts.length, "voting-rep-invalid-vote"); // Validate proof and get reputation value uint256 userReputation = checkReputation(_pollId, _key, _value, _branchMask, _siblings); @@ -98,7 +95,7 @@ contract VotingReputation is VotingBase, PatriciaTreeProofs { internal view returns (uint256) { bytes32 impliedRoot = getImpliedRootHashKey(_key, _value, _branchMask, _siblings); - require(repData[_pollId].rootHash == impliedRoot, "colony-rep-voting-invalid-root-hash"); + require(repInfo[_pollId].rootHash == impliedRoot, "voting-rep-invalid-root-hash"); uint256 reputationValue; address keyColonyAddress; @@ -112,9 +109,9 @@ contract VotingReputation is VotingBase, PatriciaTreeProofs { keyUserAddress := mload(add(_key, 72)) } - require(keyColonyAddress == address(colony), "colony-rep-voting-invalid-colony-address"); - require(keySkill == repData[_pollId].skillId, "colony-rep-voting-invalid-skill-id"); - require(keyUserAddress == msg.sender, "colony-rep-voting-invalid-user-address"); + require(keyColonyAddress == address(colony), "voting-rep-invalid-colony-address"); + require(keySkill == repInfo[_pollId].skillId, "voting-rep-invalid-skill-id"); + require(keyUserAddress == msg.sender, "voting-rep-invalid-user-address"); return reputationValue; } diff --git a/contracts/extensions/VotingToken.sol b/contracts/extensions/VotingToken.sol index dcce40b37d..fcd6464b4e 100644 --- a/contracts/extensions/VotingToken.sol +++ b/contracts/extensions/VotingToken.sol @@ -35,15 +35,6 @@ contract VotingToken is VotingBase { mapping (address => mapping (uint256 => UserVote)) userVotes; - function createPoll(uint256 _numOutcomes, uint256 _duration) public { - pollCount += 1; - - polls[pollCount] = Poll({ - pollCloses: add(now, _duration), - voteCounts: new uint256[](_numOutcomes) - }); - } - // TODO: Implement inner linked list function submitVote(uint256 _pollId, bytes32 _voteSecret, uint256 _prevPollCloses) public { require(getPollState(_pollId) == PollState.Open, "colony-token-voting-poll-not-open"); diff --git a/helpers/task-review-signing.js b/helpers/task-review-signing.js index 69e9a2e8cc..071a4c6b34 100644 --- a/helpers/task-review-signing.js +++ b/helpers/task-review-signing.js @@ -1,8 +1,8 @@ -import { soliditySha3, padLeft, isBN } from "web3-utils"; +import { soliditySha3, padLeft } from "web3-utils"; import { hashPersonalMessage, ecsign } from "ethereumjs-util"; import fs from "fs"; import { ethers } from "ethers"; -import { BigNumber } from "bignumber.js"; +import { encodeTxData } from "./test-helper"; export async function executeSignedTaskChange({ colony, taskId, functionName, signers, privKeys, sigTypes, args }) { const { sigV, sigR, sigS, txData } = await getSigsAndTransactionData({ colony, taskId, functionName, signers, privKeys, sigTypes, args }); @@ -18,21 +18,7 @@ export async function getSigsAndTransactionData({ colony, taskId, functionName, // We have to pass in an ethers BN because of https://github.com/ethereum/web3.js/issues/1920 // and https://github.com/ethereum/web3.js/issues/2077 const ethersBNTaskId = ethers.utils.bigNumberify(taskId.toString()); - const convertedArgs = []; - args.forEach(arg => { - if (Number.isInteger(arg)) { - const convertedArg = ethers.utils.bigNumberify(arg); - convertedArgs.push(convertedArg); - } else if (isBN(arg) || BigNumber.isBigNumber(arg)) { - // Can use isBigNumber from utils once https://github.com/ethereum/web3.js/issues/2835 sorted - const convertedArg = ethers.utils.bigNumberify(arg.toString()); - convertedArgs.push(convertedArg); - } else { - convertedArgs.push(arg); - } - }); - - const txData = await colony.contract.methods[functionName](...convertedArgs).encodeABI(); + const txData = await encodeTxData(colony, functionName, args); const sigsPromises = sigTypes.map((type, i) => { let privKey = []; if (privKeys) { diff --git a/helpers/test-helper.js b/helpers/test-helper.js index 0fdf532d4d..6faa667734 100644 --- a/helpers/test-helper.js +++ b/helpers/test-helper.js @@ -2,8 +2,10 @@ /* eslint-disable no-console */ import shortid from "shortid"; import chai from "chai"; -import { asciiToHex } from "web3-utils"; +import { asciiToHex, isBN } from "web3-utils"; import BN from "bn.js"; +import { ethers } from "ethers"; +import { BigNumber } from "bignumber.js"; import { UINT256_MAX, MIN_STAKE, MINING_CYCLE_DURATION, DEFAULT_STAKE } from "./constants"; @@ -710,3 +712,22 @@ export async function getColonyEditable(colony, colonyNetwork) { const colonyUnderRecovery = await ContractEditing.at(colony.address); return colonyUnderRecovery; } + +export async function encodeTxData(colony, functionName, args) { + const convertedArgs = []; + args.forEach(arg => { + if (Number.isInteger(arg)) { + const convertedArg = ethers.utils.bigNumberify(arg); + convertedArgs.push(convertedArg); + } else if (isBN(arg) || BigNumber.isBigNumber(arg)) { + // Can use isBigNumber from utils once https://github.com/ethereum/web3.js/issues/2835 sorted + const convertedArg = ethers.utils.bigNumberify(arg.toString()); + convertedArgs.push(convertedArg); + } else { + convertedArgs.push(arg); + } + }); + + const txData = await colony.contract.methods[functionName](...convertedArgs).encodeABI(); + return txData; +} diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index 2ea50ad53b..20bc9f125d 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -6,7 +6,7 @@ import shortid from "shortid"; import { soliditySha3 } from "web3-utils"; import { WAD, MINING_CYCLE_DURATION, SECONDS_PER_DAY, DEFAULT_STAKE } from "../../helpers/constants"; -import { checkErrorRevert, makeReputationKey, makeReputationValue, getActiveRepCycle, forwardTime } from "../../helpers/test-helper"; +import { checkErrorRevert, makeReputationKey, makeReputationValue, getActiveRepCycle, forwardTime, encodeTxData } from "../../helpers/test-helper"; import { setupColonyNetwork, @@ -26,7 +26,7 @@ contract("Voting Reputation", accounts => { let colony; let metaColony; let colonyNetwork; - let votingReputation; + let voting; let reputationTree; const USER0 = accounts[0]; @@ -34,8 +34,8 @@ contract("Voting Reputation", accounts => { const MINER = accounts[5]; const SALT = soliditySha3(shortid.generate()); + const FAKE = soliditySha3(shortid.generate()); const WAD2 = WAD.muln(2); - const FAKE = soliditySha3(""); before(async () => { colonyNetwork = await setupColonyNetwork(); @@ -47,12 +47,12 @@ contract("Voting Reputation", accounts => { beforeEach(async () => { ({ colony } = await setupRandomColony(colonyNetwork)); - votingReputation = await VotingReputation.new(colony.address); + voting = await VotingReputation.new(colony.address); reputationTree = new PatriciaTree(); await reputationTree.insert( makeReputationKey(colony.address, 1, USER0), // All good - makeReputationValue(WAD2, 1) + makeReputationValue(WAD, 1) ); await reputationTree.insert( makeReputationKey(metaColony.address, 1, USER0), // Wrong colony @@ -63,8 +63,8 @@ contract("Voting Reputation", accounts => { makeReputationValue(WAD, 3) ); await reputationTree.insert( - makeReputationKey(colony.address, 1, USER1), // Wrong user - makeReputationValue(WAD, 4) + makeReputationKey(colony.address, 1, USER1), // Wrong user (and 2x value) + makeReputationValue(WAD2, 4) ); const rootHash = await reputationTree.getRootHash(); @@ -74,125 +74,170 @@ contract("Voting Reputation", accounts => { await repCycle.confirmNewHash(0); }); - describe.only("happy paths", async () => { + describe.only("creating and editing polls", async () => { it("can create a new poll", async () => { - let pollCount = await votingReputation.getPollCount(); - expect(pollCount).to.be.zero; + let pollId = await voting.getPollCount(); + expect(pollId).to.be.zero; - await votingReputation.createPoll(2, SECONDS_PER_DAY, 1); - pollCount = await votingReputation.getPollCount(); - expect(pollCount).to.eq.BN(1); + await voting.createPoll(); + pollId = await voting.getPollCount(); + expect(pollId).to.eq.BN(1); + + await voting.setPollRepInfo(pollId, 1); + await voting.addPollAction(pollId, FAKE); + await voting.addPollAction(pollId, FAKE); + await voting.openPoll(pollId, SECONDS_PER_DAY); }); - it("can rate and reveal for a poll", async () => { - await votingReputation.createPoll(2, SECONDS_PER_DAY, 1); - const pollId = await votingReputation.getPollCount(); - await votingReputation.submitVote(pollId, soliditySha3(SALT, 0), { from: USER0 }); + it("cannot open a poll with fewer than two actions", async () => { + await voting.createPoll(); + const pollId = await voting.getPollCount(); + await voting.setPollRepInfo(pollId, 1); + await checkErrorRevert(voting.openPoll(pollId, SECONDS_PER_DAY), "voting-base-insufficient-poll-actions"); - await forwardTime(SECONDS_PER_DAY, this); - const key = makeReputationKey(colony.address, 1, USER0); - const value = makeReputationValue(WAD2, 1); - const [mask, siblings] = await reputationTree.getProof(key); - await votingReputation.revealVote(pollId, SALT, 0, key, value, mask, siblings, { from: USER0 }); + await voting.addPollAction(pollId, FAKE); + await checkErrorRevert(voting.openPoll(pollId, SECONDS_PER_DAY), "voting-base-insufficient-poll-actions"); + + await voting.addPollAction(pollId, FAKE); + await voting.openPoll(pollId, SECONDS_PER_DAY); }); - it("can tally votes for a poll", async () => { - await votingReputation.createPoll(3, SECONDS_PER_DAY, 1); - const pollId = await votingReputation.getPollCount(); + it("cannot add an option once a poll is open", async () => { + await voting.createPoll(); + const pollId = await voting.getPollCount(); - await votingReputation.submitVote(pollId, soliditySha3(SALT, 0), { from: USER0 }); - await votingReputation.submitVote(pollId, soliditySha3(SALT, 1), { from: USER1 }); + await voting.setPollRepInfo(pollId, 1); + await voting.addPollAction(pollId, FAKE); + await voting.addPollAction(pollId, FAKE); + await voting.openPoll(pollId, SECONDS_PER_DAY); - await forwardTime(SECONDS_PER_DAY, this); + await checkErrorRevert(voting.addPollAction(pollId, FAKE), "voting-base-poll-not-pending"); + }); + + it("cannot edit a poll unless creator", async () => { + await voting.createPoll(); + const pollId = await voting.getPollCount(); + + await checkErrorRevert(voting.addPollAction(pollId, FAKE, { from: USER1 }), "voting-base-only-creator"); + await checkErrorRevert(voting.setPollRepInfo(pollId, 1, { from: USER1 }), "voting-base-only-creator"); + await checkErrorRevert(voting.openPoll(pollId, SECONDS_PER_DAY, { from: USER1 }), "voting-base-only-creator"); + }); + + // VotingReputation specific + it("cannot open a reputation poll without a root hash", async () => { + await voting.createPoll(); + const pollId = await voting.getPollCount(); + await checkErrorRevert(voting.openPoll(pollId, SECONDS_PER_DAY), "voting-rep-poll-no-root-hash"); + }); + + // VotingReputation specific + it("cannot set the root hash on an open reputation poll", async () => { + await voting.createPoll(); + const pollId = await voting.getPollCount(); + await voting.setPollRepInfo(pollId, 1); + await voting.addPollAction(pollId, FAKE); + await voting.addPollAction(pollId, FAKE); + await voting.openPoll(pollId, SECONDS_PER_DAY); + + await checkErrorRevert(voting.setPollRepInfo(pollId, 1), "voting-base-poll-not-pending"); + }); + }); + + describe.only("voting on polls", async () => { + let key, value, mask, siblings, pollId; // eslint-disable-line one-var + + beforeEach(async () => { + await voting.createPoll(); + pollId = await voting.getPollCount(); + + await voting.setPollRepInfo(pollId, 1); + await voting.addPollAction(pollId, FAKE); + await voting.addPollAction(pollId, FAKE); + await voting.addPollAction(pollId, FAKE); + await voting.openPoll(pollId, SECONDS_PER_DAY); - let key, value, mask, siblings; // eslint-disable-line one-var key = makeReputationKey(colony.address, 1, USER0); - value = makeReputationValue(WAD2, 1); + value = makeReputationValue(WAD, 1); [mask, siblings] = await reputationTree.getProof(key); - await votingReputation.revealVote(pollId, SALT, 0, key, value, mask, siblings, { from: USER0 }); + }); + + it("can rate and reveal for a poll", async () => { + await voting.submitVote(pollId, soliditySha3(SALT, 0), { from: USER0 }); + await forwardTime(SECONDS_PER_DAY, this); + await voting.revealVote(pollId, SALT, 0, key, value, mask, siblings, { from: USER0 }); + }); + + it("can tally votes from two users", async () => { + // USER0 votes for option 2 this time to demonstrate `getPollWinner` + await voting.submitVote(pollId, soliditySha3(SALT, 2), { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, 1), { from: USER1 }); + + // Returns first option when tied + let pollWinner = await voting.getPollWinner(pollId); + expect(pollWinner).to.be.zero; + + await forwardTime(SECONDS_PER_DAY, this); + await voting.revealVote(pollId, SALT, 2, key, value, mask, siblings, { from: USER0 }); + + // Third option in the lead + pollWinner = await voting.getPollWinner(pollId); + expect(pollWinner).to.eq.BN(2); key = makeReputationKey(colony.address, 1, USER1); - value = makeReputationValue(WAD, 4); + value = makeReputationValue(WAD2, 4); [mask, siblings] = await reputationTree.getProof(key); - await votingReputation.revealVote(pollId, SALT, 1, key, value, mask, siblings, { from: USER1 }); + await voting.revealVote(pollId, SALT, 1, key, value, mask, siblings, { from: USER1 }); - const { voteCounts } = await votingReputation.getPollInfo(pollId); - expect(voteCounts[0]).to.eq.BN(WAD2); - expect(voteCounts[1]).to.eq.BN(WAD); - expect(voteCounts[2]).to.be.zero; + // Second option wins + pollWinner = await voting.getPollWinner(pollId); + expect(pollWinner).to.eq.BN(1); + + // See final counts + const { voteCounts } = await voting.getPollInfo(pollId); + expect(voteCounts[0]).to.be.zero; + expect(voteCounts[1]).to.eq.BN(WAD2); + expect(voteCounts[2]).to.eq.BN(WAD); }); it("can update votes, but only last one counts", async () => { - await votingReputation.createPoll(2, SECONDS_PER_DAY, 1); - const pollId = await votingReputation.getPollCount(); - - await votingReputation.submitVote(pollId, soliditySha3(SALT, 0), { from: USER0 }); - await votingReputation.submitVote(pollId, soliditySha3(SALT, 1), { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, 0), { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, 1), { from: USER0 }); await forwardTime(SECONDS_PER_DAY, this); - const key = makeReputationKey(colony.address, 1, USER0); - const value = makeReputationValue(WAD2, 1); - const [mask, siblings] = await reputationTree.getProof(key); - // Revealing first vote fails - await checkErrorRevert( - votingReputation.revealVote(pollId, SALT, 0, key, value, mask, siblings, { from: USER0 }), - "colony-rep-voting-secret-no-match" - ); + await checkErrorRevert(voting.revealVote(pollId, SALT, 0, key, value, mask, siblings, { from: USER0 }), "voting-rep-secret-no-match"); // Revealing second succeeds - await votingReputation.revealVote(pollId, SALT, 1, key, value, mask, siblings, { from: USER0 }); + await voting.revealVote(pollId, SALT, 1, key, value, mask, siblings, { from: USER0 }); }); it("can reveal votes after poll closes, but doesn't count", async () => { - await votingReputation.createPoll(2, SECONDS_PER_DAY, 1); - const pollId = await votingReputation.getPollCount(); - - await votingReputation.submitVote(pollId, soliditySha3(SALT, 0), { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, 0), { from: USER0 }); // Close the poll (1 day voting, 2 day reveal) await forwardTime(SECONDS_PER_DAY * 3, this); - const key = makeReputationKey(colony.address, 1, USER0); - const value = makeReputationValue(WAD2, 1); - const [mask, siblings] = await reputationTree.getProof(key); - - await votingReputation.revealVote(pollId, SALT, 0, key, value, mask, siblings, { from: USER0 }); + await voting.revealVote(pollId, SALT, 0, key, value, mask, siblings, { from: USER0 }); // Vote didn't count - const { voteCounts } = await votingReputation.getPollInfo(pollId); + const { voteCounts } = await voting.getPollInfo(pollId); expect(voteCounts[0]).to.be.zero; expect(voteCounts[1]).to.be.zero; expect(voteCounts[2]).to.be.zero; }); it("cannot reveal a vote twice, and so cannot vote twice", async () => { - await votingReputation.createPoll(2, SECONDS_PER_DAY, 1); - const pollId = await votingReputation.getPollCount(); - await votingReputation.submitVote(pollId, soliditySha3(SALT, 0), { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, 0), { from: USER0 }); await forwardTime(SECONDS_PER_DAY, this); - const key = makeReputationKey(colony.address, 1, USER0); - const value = makeReputationValue(WAD2, 1); - const [mask, siblings] = await reputationTree.getProof(key); - - await votingReputation.revealVote(pollId, SALT, 0, key, value, mask, siblings, { from: USER0 }); - await checkErrorRevert( - votingReputation.revealVote(pollId, SALT, 0, key, value, mask, siblings, { from: USER0 }), - "colony-rep-voting-secret-no-match" - ); + await voting.revealVote(pollId, SALT, 0, key, value, mask, siblings, { from: USER0 }); + await checkErrorRevert(voting.revealVote(pollId, SALT, 0, key, value, mask, siblings, { from: USER0 }), "voting-rep-secret-no-match"); }); it("can vote in two polls with two reputation states, with different proofs", async () => { - await votingReputation.createPoll(2, SECONDS_PER_DAY, 1); - const pollId1 = await votingReputation.getPollCount(); - await votingReputation.submitVote(pollId1, soliditySha3(SALT, 0), { from: USER0 }); - - const key = makeReputationKey(colony.address, 1, USER0); - const value1 = makeReputationValue(WAD2, 1); - const [mask1, siblings1] = await reputationTree.getProof(key); + await voting.submitVote(pollId, soliditySha3(SALT, 0), { from: USER0 }); // Update reputation state const value2 = makeReputationValue(WAD.muln(3), 1); @@ -205,15 +250,48 @@ contract("Voting Reputation", accounts => { await repCycle.submitRootHash(rootHash, 0, "0x00", 10, { from: MINER }); await repCycle.confirmNewHash(0); - await votingReputation.createPoll(2, SECONDS_PER_DAY, 1); - const pollId2 = await votingReputation.getPollCount(); - await votingReputation.submitVote(pollId2, soliditySha3(SALT, 0), { from: USER0 }); + // Create new poll with new reputation state + await voting.createPoll(); + const pollId2 = await voting.getPollCount(); + await voting.setPollRepInfo(pollId2, 1); + await voting.addPollAction(pollId2, FAKE); + await voting.addPollAction(pollId2, FAKE); + await voting.openPoll(pollId2, SECONDS_PER_DAY); + + await voting.submitVote(pollId2, soliditySha3(SALT, 0), { from: USER0 }); await forwardTime(SECONDS_PER_DAY, this); const [mask2, siblings2] = await reputationTree.getProof(key); - await votingReputation.revealVote(pollId1, SALT, 0, key, value1, mask1, siblings1, { from: USER0 }); - await votingReputation.revealVote(pollId2, SALT, 0, key, value2, mask2, siblings2, { from: USER0 }); + await voting.revealVote(pollId, SALT, 0, key, value, mask, siblings, { from: USER0 }); + await voting.revealVote(pollId2, SALT, 0, key, value2, mask2, siblings2, { from: USER0 }); + }); + + it("can take an action based on the result of a poll", async () => { + await colony.setAdministrationRole(1, 0, voting.address, 1, true); + const action = await encodeTxData(colony, "makeTask", [1, 0, FAKE, 1, 0, 0]); + + await voting.createPoll(); + pollId = await voting.getPollCount(); + await voting.setPollRepInfo(pollId, 1); + await voting.addPollAction(pollId, FAKE); + await voting.addPollAction(pollId, action); + await voting.openPoll(pollId, SECONDS_PER_DAY); + + await voting.submitVote(pollId, soliditySha3(SALT, 1), { from: USER0 }); + + await forwardTime(SECONDS_PER_DAY, this); + await voting.revealVote(pollId, SALT, 1, key, value, mask, siblings, { from: USER0 }); + + await checkErrorRevert(voting.executePoll(pollId), "voting-base-poll-not-closed"); + + await forwardTime(SECONDS_PER_DAY * 2, this); + const taskCountPrev = await colony.getTaskCount(); + await voting.executePoll(pollId); + const taskCountPost = await colony.getTaskCount(); + expect(taskCountPost).to.eq.BN(taskCountPrev.addn(1)); + + await checkErrorRevert(voting.executePoll(pollId), "voting-base-poll-already-executed"); }); }); @@ -221,72 +299,68 @@ contract("Voting Reputation", accounts => { let pollId; beforeEach(async () => { - await votingReputation.createPoll(2, SECONDS_PER_DAY, 1); - pollId = await votingReputation.getPollCount(); + await voting.createPoll(); + pollId = await voting.getPollCount(); + await voting.setPollRepInfo(pollId, 1); + await voting.addPollAction(pollId, FAKE); + await voting.addPollAction(pollId, FAKE); + await voting.openPoll(pollId, SECONDS_PER_DAY); + }); + + it("cannot submit a vote if poll is pending", async () => { + await voting.createPoll(); + pollId = await voting.getPollCount(); + await checkErrorRevert(voting.submitVote(pollId, soliditySha3(SALT, 0)), "voting-rep-poll-not-open"); }); it("cannot submit a vote if voting is closed", async () => { await forwardTime(SECONDS_PER_DAY * 2, this); - await checkErrorRevert(votingReputation.submitVote(pollId, soliditySha3(SALT, 0)), "colony-rep-voting-poll-not-open"); + await checkErrorRevert(voting.submitVote(pollId, soliditySha3(SALT, 0)), "voting-rep-poll-not-open"); }); it("cannot reveal a vote if voting is open", async () => { - await votingReputation.submitVote(pollId, soliditySha3(SALT, 0)); - await checkErrorRevert(votingReputation.revealVote(pollId, SALT, 1, FAKE, FAKE, 0, []), "colony-rep-voting-poll-still-open"); + await voting.submitVote(pollId, soliditySha3(SALT, 0)); + await checkErrorRevert(voting.revealVote(pollId, SALT, 1, FAKE, FAKE, 0, []), "voting-rep-poll-still-open"); }); it("cannot reveal a vote with a bad secret", async () => { - await votingReputation.submitVote(pollId, soliditySha3(SALT, 0)); + await voting.submitVote(pollId, soliditySha3(SALT, 0)); await forwardTime(SECONDS_PER_DAY, this); - await checkErrorRevert(votingReputation.revealVote(pollId, SALT, 1, FAKE, FAKE, 0, []), "colony-rep-voting-secret-no-match"); + await checkErrorRevert(voting.revealVote(pollId, SALT, 1, FAKE, FAKE, 0, []), "voting-rep-secret-no-match"); }); it("cannot reveal an invalid vote", async () => { - await votingReputation.submitVote(pollId, soliditySha3(SALT, 2)); + await voting.submitVote(pollId, soliditySha3(SALT, 2)); await forwardTime(SECONDS_PER_DAY, this); - await checkErrorRevert(votingReputation.revealVote(pollId, SALT, 2, FAKE, FAKE, 0, []), "colony-rep-voting-invalid-vote"); + await checkErrorRevert(voting.revealVote(pollId, SALT, 2, FAKE, FAKE, 0, []), "voting-rep-invalid-vote"); }); + // VotingReputation specific it("cannot reveal a vote with a bad proof", async () => { - await votingReputation.submitVote(pollId, soliditySha3(SALT, 0)); + await voting.submitVote(pollId, soliditySha3(SALT, 0), { from: USER0 }); await forwardTime(SECONDS_PER_DAY, this); - await checkErrorRevert(votingReputation.revealVote(pollId, SALT, 0, FAKE, FAKE, 0, []), "colony-rep-voting-invalid-root-hash"); - }); - it("cannot submit a proof with the wrong colony", async () => { - await votingReputation.submitVote(pollId, soliditySha3(SALT, 0)); - const key = makeReputationKey(metaColony.address, 1, USER0); - const value = makeReputationValue(WAD, 2); - const [mask, siblings] = await reputationTree.getProof(key); - await forwardTime(SECONDS_PER_DAY, this); - await checkErrorRevert( - votingReputation.revealVote(pollId, SALT, 0, key, value, mask, siblings, { from: USER0 }), - "colony-rep-voting-invalid-colony-address" - ); - }); + // Invalid proof (wrong root hash) + await checkErrorRevert(voting.revealVote(pollId, SALT, 0, FAKE, FAKE, 0, [], { from: USER0 }), "voting-rep-invalid-root-hash"); - it("cannot submit a proof with the wrong skill", async () => { - await votingReputation.submitVote(pollId, soliditySha3(SALT, 0)); - const key = makeReputationKey(colony.address, 2, USER0); - const value = makeReputationValue(WAD, 3); - const [mask, siblings] = await reputationTree.getProof(key); - await forwardTime(SECONDS_PER_DAY, this); - await checkErrorRevert( - votingReputation.revealVote(pollId, SALT, 0, key, value, mask, siblings, { from: USER0 }), - "colony-rep-voting-invalid-skill-id" - ); - }); + // Invalid colony address + let key, value, mask, siblings; // eslint-disable-line one-var + key = makeReputationKey(metaColony.address, 1, USER0); + value = makeReputationValue(WAD, 2); + [mask, siblings] = await reputationTree.getProof(key); + await checkErrorRevert(voting.revealVote(pollId, SALT, 0, key, value, mask, siblings, { from: USER0 }), "voting-rep-invalid-colony-address"); - it("cannot submit a proof with the wrong user", async () => { - await votingReputation.submitVote(pollId, soliditySha3(SALT, 0)); - const key = makeReputationKey(colony.address, 1, USER1); - const value = makeReputationValue(WAD, 4); - const [mask, siblings] = await reputationTree.getProof(key); - await forwardTime(SECONDS_PER_DAY, this); - await checkErrorRevert( - votingReputation.revealVote(pollId, SALT, 0, key, value, mask, siblings, { from: USER0 }), - "colony-rep-voting-invalid-user-address" - ); + // Invalid skill id + key = makeReputationKey(colony.address, 2, USER0); + value = makeReputationValue(WAD, 3); + [mask, siblings] = await reputationTree.getProof(key); + await checkErrorRevert(voting.revealVote(pollId, SALT, 0, key, value, mask, siblings, { from: USER0 }), "voting-rep-invalid-skill-id"); + + // Invalid user address + key = makeReputationKey(colony.address, 1, USER1); + value = makeReputationValue(WAD2, 4); + [mask, siblings] = await reputationTree.getProof(key); + await checkErrorRevert(voting.revealVote(pollId, SALT, 0, key, value, mask, siblings, { from: USER0 }), "voting-rep-invalid-user-address"); }); }); }); diff --git a/test/extensions/voting-token.js b/test/extensions/voting-token.js index e0723ba9ef..297733671e 100644 --- a/test/extensions/voting-token.js +++ b/test/extensions/voting-token.js @@ -22,12 +22,13 @@ contract("Voting Token", accounts => { let token; let colonyNetwork; let tokenLocking; - let votingToken; + let voting; const USER0 = accounts[0]; const USER1 = accounts[1]; const SALT = soliditySha3(shortid.generate()); + const FAKE = soliditySha3(shortid.generate()); const WAD2 = WAD.muln(2); before(async () => { @@ -44,51 +45,61 @@ contract("Voting Token", accounts => { const tokenAuthority = await TokenAuthority.new(token.address, colony.address, [tokenLocking.address]); await token.setAuthority(tokenAuthority.address); - await token.mint(USER0, WAD2); - await token.approve(tokenLocking.address, WAD2, { from: USER0 }); - await tokenLocking.deposit(token.address, WAD2, { from: USER0 }); + await token.mint(USER0, WAD); + await token.approve(tokenLocking.address, WAD, { from: USER0 }); + await tokenLocking.deposit(token.address, WAD, { from: USER0 }); - await token.mint(USER1, WAD); - await token.approve(tokenLocking.address, WAD, { from: USER1 }); - await tokenLocking.deposit(token.address, WAD, { from: USER1 }); + await token.mint(USER1, WAD2); + await token.approve(tokenLocking.address, WAD2, { from: USER1 }); + await tokenLocking.deposit(token.address, WAD2, { from: USER1 }); - votingToken = await VotingToken.new(colony.address); + voting = await VotingToken.new(colony.address); }); - describe.only("token voting", async () => { + describe.only("creating polls", async () => { it("can create a new poll", async () => { - let pollCount = await votingToken.getPollCount(); + let pollCount = await voting.getPollCount(); expect(pollCount).to.be.zero; - await votingToken.createPoll(2, SECONDS_PER_DAY); - pollCount = await votingToken.getPollCount(); + await voting.createPoll(); + pollCount = await voting.getPollCount(); expect(pollCount).to.eq.BN(1); }); + }); + + describe.only("voting on polls", async () => { + let pollId; + + beforeEach(async () => { + await voting.createPoll(); + pollId = await voting.getPollCount(); + + await voting.addPollAction(pollId, FAKE); + await voting.addPollAction(pollId, FAKE); + await voting.addPollAction(pollId, FAKE); + await voting.openPoll(pollId, SECONDS_PER_DAY); + }); it("can rate and reveal for a poll", async () => { - await votingToken.createPoll(2, SECONDS_PER_DAY); - const pollId = await votingToken.getPollCount(); - await votingToken.submitVote(pollId, soliditySha3(SALT, 0), 0, { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, 0), 0, { from: USER0 }); await forwardTime(SECONDS_PER_DAY, this); - await votingToken.revealVote(pollId, SALT, 0, { from: USER0 }); + + await voting.revealVote(pollId, SALT, 0, { from: USER0 }); }); it("can tally votes for a poll", async () => { - await votingToken.createPoll(3, SECONDS_PER_DAY); - const pollId = await votingToken.getPollCount(); - - await votingToken.submitVote(pollId, soliditySha3(SALT, 0), 0, { from: USER0 }); - await votingToken.submitVote(pollId, soliditySha3(SALT, 1), 0, { from: USER1 }); + await voting.submitVote(pollId, soliditySha3(SALT, 0), 0, { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, 1), 0, { from: USER1 }); await forwardTime(SECONDS_PER_DAY, this); - await votingToken.revealVote(pollId, SALT, 0, { from: USER0 }); - await votingToken.revealVote(pollId, SALT, 1, { from: USER1 }); + await voting.revealVote(pollId, SALT, 0, { from: USER0 }); + await voting.revealVote(pollId, SALT, 1, { from: USER1 }); - const { voteCounts } = await votingToken.getPollInfo(pollId); - expect(voteCounts[0]).to.eq.BN(WAD2); - expect(voteCounts[1]).to.eq.BN(WAD); + const { voteCounts } = await voting.getPollInfo(pollId); + expect(voteCounts[0]).to.eq.BN(WAD); + expect(voteCounts[1]).to.eq.BN(WAD2); expect(voteCounts[2]).to.be.zero; }); });