From 7c2bc22a3eb9785a586f394d126270a1f8a3c89e Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Wed, 12 Jun 2019 17:03:05 +0300 Subject: [PATCH 01/61] Create VotingBase and VotingReputation --- contracts/extensions/VotingBase.sol | 74 ++++++ contracts/extensions/VotingReputation.sol | 122 +++++++++ contracts/extensions/VotingToken.sol | 110 ++++++++ scripts/check-recovery.js | 2 + test/extensions/voting-rep.js | 292 ++++++++++++++++++++++ test/extensions/voting-token.js | 95 +++++++ 6 files changed, 695 insertions(+) create mode 100644 contracts/extensions/VotingBase.sol create mode 100644 contracts/extensions/VotingReputation.sol create mode 100644 contracts/extensions/VotingToken.sol create mode 100644 test/extensions/voting-rep.js create mode 100644 test/extensions/voting-token.js diff --git a/contracts/extensions/VotingBase.sol b/contracts/extensions/VotingBase.sol new file mode 100644 index 0000000000..63b7451f5b --- /dev/null +++ b/contracts/extensions/VotingBase.sol @@ -0,0 +1,74 @@ +/* + This file is part of The Colony Network. + + The Colony Network is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + The Colony Network is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with The Colony Network. If not, see . +*/ + +pragma solidity 0.5.8; +pragma experimental ABIEncoderV2; + +import "../../lib/dappsys/math.sol"; +import "../IColony.sol"; +import "../IColonyNetwork.sol"; + + +contract VotingBase is DSMath { + + // Constants + uint256 constant REVEAL_PERIOD = 2 days; + + // Initialization data + IColony colony; + IColonyNetwork colonyNetwork; + + constructor(address _colony) public { + colony = IColony(_colony); + colonyNetwork = IColonyNetwork(colony.getColonyNetwork()); + } + + // Data structures + enum PollState { Open, Reveal, Closed } + + struct Poll { + uint256 pollCloses; + uint256[] voteCounts; + } + + // Storage + uint256 pollCount; + mapping (uint256 => Poll) polls; + + // Functions + function getPollCount() public view returns (uint256) { + return pollCount; + } + + function getPollInfo(uint256 _pollId) public view returns (Poll memory poll) { + poll = polls[_pollId]; + } + + function getPollState(uint256 _pollId) internal view returns (PollState) { + if (now < polls[_pollId].pollCloses) { + return PollState.Open; + } else if (now < add(polls[_pollId].pollCloses, REVEAL_PERIOD)) { + return PollState.Reveal; + } else { + return PollState.Closed; + } + } + + function getVoteSecret(bytes32 _salt, uint256 _vote) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(_salt, _vote)); + } +} diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol new file mode 100644 index 0000000000..6abdcd29d0 --- /dev/null +++ b/contracts/extensions/VotingReputation.sol @@ -0,0 +1,122 @@ +/* + This file is part of The Colony Network. + + The Colony Network is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + The Colony Network is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with The Colony Network. If not, see . +*/ + +pragma solidity 0.5.8; +pragma experimental ABIEncoderV2; + +import "../PatriciaTree/PatriciaTreeProofs.sol"; +import "./VotingBase.sol"; + + +contract VotingReputation is VotingBase, PatriciaTreeProofs { + + constructor(address _colony) public VotingBase(_colony) {} + + struct RepDatum { + bytes32 rootHash; + uint256 skillId; + } + + mapping (uint256 => RepDatum) repData; + + // 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({ + rootHash: colonyNetwork.getReputationRootHash(), + skillId: _skillId + }); + } + + function submitVote(uint256 _pollId, bytes32 _voteSecret) public { + require(getPollState(_pollId) == PollState.Open, "colony-rep-voting-poll-not-open"); + + userVotes[msg.sender][_pollId] = _voteSecret; + } + + function revealVote( + uint256 _pollId, + bytes32 _salt, + uint256 _vote, + bytes memory _key, + bytes memory _value, + uint256 _branchMask, + bytes32[] memory _siblings + ) + public + { + uint256 pollCloses = polls[_pollId].pollCloses; + require(getPollState(_pollId) != PollState.Open, "colony-rep-voting-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"); + + // Validate proof and get reputation value + uint256 userReputation = checkReputation(_pollId, _key, _value, _branchMask, _siblings); + + // Remove the secret + delete userVotes[msg.sender][_pollId]; + + // Increment the vote if poll in reveal, otherwise skip + // NOTE: since there's no locking, we could just `require` PollState.Reveal + if (getPollState(_pollId) == PollState.Reveal) { + polls[_pollId].voteCounts[_vote] += userReputation; + } + } + + function checkReputation( + uint256 _pollId, + bytes memory _key, + bytes memory _value, + uint256 _branchMask, + bytes32[] memory _siblings + ) + internal view returns (uint256) + { + bytes32 impliedRoot = getImpliedRootHashKey(_key, _value, _branchMask, _siblings); + require(repData[_pollId].rootHash == impliedRoot, "colony-rep-voting-invalid-root-hash"); + + uint256 reputationValue; + address keyColonyAddress; + uint256 keySkill; + address keyUserAddress; + + assembly { + reputationValue := mload(add(_value, 32)) + keyColonyAddress := mload(add(_key, 20)) + keySkill := mload(add(_key, 52)) + 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"); + + return reputationValue; + } + +} diff --git a/contracts/extensions/VotingToken.sol b/contracts/extensions/VotingToken.sol new file mode 100644 index 0000000000..dcce40b37d --- /dev/null +++ b/contracts/extensions/VotingToken.sol @@ -0,0 +1,110 @@ +/* + This file is part of The Colony Network. + + The Colony Network is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + The Colony Network is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with The Colony Network. If not, see . +*/ + +pragma solidity 0.5.8; +pragma experimental ABIEncoderV2; + +import "../ITokenLocking.sol"; +import "./VotingBase.sol"; + + +contract VotingToken is VotingBase { + + constructor(address _colony) public VotingBase(_colony) {} + + struct UserVote { + uint256 pollId; + bytes32 voteSecret; + uint256 prevPollCloses; + uint256 nextPollCloses; + } + + 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"); + + UserVote storage prev = userVotes[msg.sender][_prevPollCloses]; + UserVote storage next = userVotes[msg.sender][prev.nextPollCloses]; + + // Check we are inserting at the correct location + uint256 pollCloses = polls[_pollId].pollCloses; + require(pollCloses > _prevPollCloses, "colony-token-voting-insert-too-soon"); + require(pollCloses < prev.nextPollCloses || prev.nextPollCloses == 0, "colony-token-voting-insert-too-late"); + + userVotes[msg.sender][pollCloses] = UserVote({ + pollId: _pollId, + voteSecret: _voteSecret, + prevPollCloses: _prevPollCloses, + nextPollCloses: prev.nextPollCloses + }); + + prev.nextPollCloses = pollCloses; + next.prevPollCloses = pollCloses; + } + + function revealVote(uint256 _pollId, bytes32 _salt, uint256 _vote) public { + require(getPollState(_pollId) != PollState.Open, "colony-token-voting-poll-still-open"); + + uint256 pollCloses = polls[_pollId].pollCloses; + UserVote storage curr = userVotes[msg.sender][pollCloses]; + UserVote storage prev = userVotes[msg.sender][curr.prevPollCloses]; + UserVote storage next = userVotes[msg.sender][curr.nextPollCloses]; + + require(curr.voteSecret == getVoteSecret(_salt, _vote), "colony-token-voting-secret-no-match"); + require(_vote < polls[_pollId].voteCounts.length, "colony-token-voting-invalid-vote"); + + // Remove the secret + prev.nextPollCloses = curr.nextPollCloses; + next.prevPollCloses = curr.prevPollCloses; + delete userVotes[msg.sender][pollCloses]; + + // Increment the vote if poll in reveal + if (getPollState(_pollId) == PollState.Reveal) { + address token = colony.getToken(); + address tokenLocking = colonyNetwork.getTokenLocking(); + uint256 userBalance = ITokenLocking(tokenLocking).getUserLock(token, msg.sender).balance; + polls[_pollId].voteCounts[_vote] += userBalance; + } + } + + function isAddressLocked(address _address) public view returns (bool) { + uint256 nextPollCloses = userVotes[_address][0].nextPollCloses; + if (nextPollCloses == 0) { + // The list is empty, no unrevealed votes for this address + return false; + } else if (now < nextPollCloses) { + // The poll is still open for voting and tokens transfer + return false; + } else { + // The poll is closed for voting and is in the reveal period, during which all votes' tokens are locked until reveal + // Note: even after the poll is resolved, tokens remain locked until reveal + return true; + } + } + +} diff --git a/scripts/check-recovery.js b/scripts/check-recovery.js index 774f43690b..5d3a8f2346 100644 --- a/scripts/check-recovery.js +++ b/scripts/check-recovery.js @@ -46,6 +46,8 @@ walkSync("./contracts/").forEach((contractName) => { "contracts/extensions/FundingQueueFactory.sol", "contracts/extensions/OneTxPayment.sol", "contracts/extensions/OneTxPaymentFactory.sol", + "contracts/extensions/VotingBase.sol", + "contracts/extensions/VotingReputation.sol", "contracts/gnosis/MultiSigWallet.sol", "contracts/patriciaTree/Bits.sol", "contracts/patriciaTree/Data.sol", diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js new file mode 100644 index 0000000000..2ea50ad53b --- /dev/null +++ b/test/extensions/voting-rep.js @@ -0,0 +1,292 @@ +/* globals artifacts */ + +import chai from "chai"; +import bnChai from "bn-chai"; +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 { + setupColonyNetwork, + setupMetaColonyWithLockedCLNYToken, + setupRandomColony, + giveUserCLNYTokensAndStake +} from "../../helpers/test-data-generator"; + +import PatriciaTree from "../../packages/reputation-miner/patricia"; + +const { expect } = chai; +chai.use(bnChai(web3.utils.BN)); + +const VotingReputation = artifacts.require("VotingReputation"); + +contract("Voting Reputation", accounts => { + let colony; + let metaColony; + let colonyNetwork; + let votingReputation; + let reputationTree; + + const USER0 = accounts[0]; + const USER1 = accounts[1]; + const MINER = accounts[5]; + + const SALT = soliditySha3(shortid.generate()); + const WAD2 = WAD.muln(2); + const FAKE = soliditySha3(""); + + before(async () => { + colonyNetwork = await setupColonyNetwork(); + ({ metaColony } = await setupMetaColonyWithLockedCLNYToken(colonyNetwork)); + await giveUserCLNYTokensAndStake(colonyNetwork, MINER, DEFAULT_STAKE); + await colonyNetwork.initialiseReputationMining(); + await colonyNetwork.startNextCycle(); + }); + + beforeEach(async () => { + ({ colony } = await setupRandomColony(colonyNetwork)); + votingReputation = await VotingReputation.new(colony.address); + + reputationTree = new PatriciaTree(); + await reputationTree.insert( + makeReputationKey(colony.address, 1, USER0), // All good + makeReputationValue(WAD2, 1) + ); + await reputationTree.insert( + makeReputationKey(metaColony.address, 1, USER0), // Wrong colony + makeReputationValue(WAD, 2) + ); + await reputationTree.insert( + makeReputationKey(colony.address, 2, USER0), // Wrong skill + makeReputationValue(WAD, 3) + ); + await reputationTree.insert( + makeReputationKey(colony.address, 1, USER1), // Wrong user + makeReputationValue(WAD, 4) + ); + + const rootHash = await reputationTree.getRootHash(); + const repCycle = await getActiveRepCycle(colonyNetwork); + await forwardTime(MINING_CYCLE_DURATION, this); + await repCycle.submitRootHash(rootHash, 0, "0x00", 10, { from: MINER }); + await repCycle.confirmNewHash(0); + }); + + describe.only("happy paths", async () => { + it("can create a new poll", async () => { + let pollCount = await votingReputation.getPollCount(); + expect(pollCount).to.be.zero; + + await votingReputation.createPoll(2, SECONDS_PER_DAY, 1); + pollCount = await votingReputation.getPollCount(); + expect(pollCount).to.eq.BN(1); + }); + + 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 }); + + 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 }); + }); + + it("can tally votes for a poll", async () => { + await votingReputation.createPoll(3, 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: USER1 }); + + await forwardTime(SECONDS_PER_DAY, this); + + let key, value, mask, siblings; // eslint-disable-line one-var + key = makeReputationKey(colony.address, 1, USER0); + value = makeReputationValue(WAD2, 1); + [mask, siblings] = await reputationTree.getProof(key); + await votingReputation.revealVote(pollId, SALT, 0, key, value, mask, siblings, { from: USER0 }); + + key = makeReputationKey(colony.address, 1, USER1); + value = makeReputationValue(WAD, 4); + [mask, siblings] = await reputationTree.getProof(key); + await votingReputation.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; + }); + + 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 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" + ); + + // Revealing second succeeds + await votingReputation.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 }); + + // 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 }); + + // Vote didn't count + const { voteCounts } = await votingReputation.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 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" + ); + }); + + 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); + + // Update reputation state + const value2 = makeReputationValue(WAD.muln(3), 1); + await reputationTree.insert(key, value2); + + // Set new rootHash + const rootHash = await reputationTree.getRootHash(); + const repCycle = await getActiveRepCycle(colonyNetwork); + await forwardTime(MINING_CYCLE_DURATION, this); + 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 }); + + 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 }); + }); + }); + + describe.only("simple exceptions", async () => { + let pollId; + + beforeEach(async () => { + await votingReputation.createPoll(2, SECONDS_PER_DAY, 1); + pollId = await votingReputation.getPollCount(); + }); + + 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"); + }); + + 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"); + }); + + it("cannot reveal a vote with a bad secret", async () => { + await votingReputation.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"); + }); + + it("cannot reveal an invalid vote", async () => { + await votingReputation.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"); + }); + + it("cannot reveal a vote with a bad proof", async () => { + await votingReputation.submitVote(pollId, soliditySha3(SALT, 0)); + 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" + ); + }); + + 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" + ); + }); + + 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" + ); + }); + }); +}); diff --git a/test/extensions/voting-token.js b/test/extensions/voting-token.js new file mode 100644 index 0000000000..e0723ba9ef --- /dev/null +++ b/test/extensions/voting-token.js @@ -0,0 +1,95 @@ +/* globals artifacts */ + +import chai from "chai"; +import bnChai from "bn-chai"; +import shortid from "shortid"; +import { soliditySha3 } from "web3-utils"; + +import { WAD, SECONDS_PER_DAY } from "../../helpers/constants"; +import { setupColonyNetwork, setupColony } from "../../helpers/test-data-generator"; +import { forwardTime, getTokenArgs } from "../../helpers/test-helper"; + +const { expect } = chai; +chai.use(bnChai(web3.utils.BN)); + +const Token = artifacts.require("Token"); +const TokenAuthority = artifacts.require("TokenAuthority"); +const TokenLocking = artifacts.require("TokenLocking"); +const VotingToken = artifacts.require("VotingToken"); + +contract("Voting Token", accounts => { + let colony; + let token; + let colonyNetwork; + let tokenLocking; + let votingToken; + + const USER0 = accounts[0]; + const USER1 = accounts[1]; + + const SALT = soliditySha3(shortid.generate()); + const WAD2 = WAD.muln(2); + + before(async () => { + colonyNetwork = await setupColonyNetwork(); + + const tokenLockingAddress = await colonyNetwork.getTokenLocking(); + tokenLocking = await TokenLocking.at(tokenLockingAddress); + }); + + beforeEach(async () => { + token = await Token.new(...getTokenArgs()); + colony = await setupColony(colonyNetwork, token.address); + + 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(USER1, WAD); + await token.approve(tokenLocking.address, WAD, { from: USER1 }); + await tokenLocking.deposit(token.address, WAD, { from: USER1 }); + + votingToken = await VotingToken.new(colony.address); + }); + + describe.only("token voting", async () => { + it("can create a new poll", async () => { + let pollCount = await votingToken.getPollCount(); + expect(pollCount).to.be.zero; + + await votingToken.createPoll(2, SECONDS_PER_DAY); + pollCount = await votingToken.getPollCount(); + expect(pollCount).to.eq.BN(1); + }); + + 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 forwardTime(SECONDS_PER_DAY, this); + await votingToken.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 forwardTime(SECONDS_PER_DAY, this); + + await votingToken.revealVote(pollId, SALT, 0, { from: USER0 }); + await votingToken.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); + expect(voteCounts[2]).to.be.zero; + }); + }); +}); From 5bd6de6bc27bd7fb5517adaddc8ce21347ea6395 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Wed, 19 Jun 2019 12:34:33 +0300 Subject: [PATCH 02/61] Add support for vote actions --- contracts/extensions/VotingBase.sol | 77 ++++- contracts/extensions/VotingReputation.sol | 39 ++- contracts/extensions/VotingToken.sol | 110 ------- helpers/task-review-signing.js | 20 +- helpers/test-helper.js | 22 +- test/extensions/voting-rep.js | 332 +++++++++++++--------- test/extensions/voting-token.js | 95 ------- 7 files changed, 317 insertions(+), 378 deletions(-) delete mode 100644 contracts/extensions/VotingToken.sol delete mode 100644 test/extensions/voting-token.js 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 deleted file mode 100644 index dcce40b37d..0000000000 --- a/contracts/extensions/VotingToken.sol +++ /dev/null @@ -1,110 +0,0 @@ -/* - This file is part of The Colony Network. - - The Colony Network is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - The Colony Network is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with The Colony Network. If not, see . -*/ - -pragma solidity 0.5.8; -pragma experimental ABIEncoderV2; - -import "../ITokenLocking.sol"; -import "./VotingBase.sol"; - - -contract VotingToken is VotingBase { - - constructor(address _colony) public VotingBase(_colony) {} - - struct UserVote { - uint256 pollId; - bytes32 voteSecret; - uint256 prevPollCloses; - uint256 nextPollCloses; - } - - 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"); - - UserVote storage prev = userVotes[msg.sender][_prevPollCloses]; - UserVote storage next = userVotes[msg.sender][prev.nextPollCloses]; - - // Check we are inserting at the correct location - uint256 pollCloses = polls[_pollId].pollCloses; - require(pollCloses > _prevPollCloses, "colony-token-voting-insert-too-soon"); - require(pollCloses < prev.nextPollCloses || prev.nextPollCloses == 0, "colony-token-voting-insert-too-late"); - - userVotes[msg.sender][pollCloses] = UserVote({ - pollId: _pollId, - voteSecret: _voteSecret, - prevPollCloses: _prevPollCloses, - nextPollCloses: prev.nextPollCloses - }); - - prev.nextPollCloses = pollCloses; - next.prevPollCloses = pollCloses; - } - - function revealVote(uint256 _pollId, bytes32 _salt, uint256 _vote) public { - require(getPollState(_pollId) != PollState.Open, "colony-token-voting-poll-still-open"); - - uint256 pollCloses = polls[_pollId].pollCloses; - UserVote storage curr = userVotes[msg.sender][pollCloses]; - UserVote storage prev = userVotes[msg.sender][curr.prevPollCloses]; - UserVote storage next = userVotes[msg.sender][curr.nextPollCloses]; - - require(curr.voteSecret == getVoteSecret(_salt, _vote), "colony-token-voting-secret-no-match"); - require(_vote < polls[_pollId].voteCounts.length, "colony-token-voting-invalid-vote"); - - // Remove the secret - prev.nextPollCloses = curr.nextPollCloses; - next.prevPollCloses = curr.prevPollCloses; - delete userVotes[msg.sender][pollCloses]; - - // Increment the vote if poll in reveal - if (getPollState(_pollId) == PollState.Reveal) { - address token = colony.getToken(); - address tokenLocking = colonyNetwork.getTokenLocking(); - uint256 userBalance = ITokenLocking(tokenLocking).getUserLock(token, msg.sender).balance; - polls[_pollId].voteCounts[_vote] += userBalance; - } - } - - function isAddressLocked(address _address) public view returns (bool) { - uint256 nextPollCloses = userVotes[_address][0].nextPollCloses; - if (nextPollCloses == 0) { - // The list is empty, no unrevealed votes for this address - return false; - } else if (now < nextPollCloses) { - // The poll is still open for voting and tokens transfer - return false; - } else { - // The poll is closed for voting and is in the reveal period, during which all votes' tokens are locked until reveal - // Note: even after the poll is resolved, tokens remain locked until reveal - return true; - } - } - -} diff --git a/helpers/task-review-signing.js b/helpers/task-review-signing.js index 99fbe7cea7..96f4893f6e 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 }); @@ -17,22 +17,8 @@ export async function executeSignedRoleAssignment({ colony, taskId, functionName export async function getSigsAndTransactionData({ colony, taskId, functionName, signers, privKeys, sigTypes, args }) { // 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 txData = await encodeTxData(colony, functionName, args); const ethersBNTaskId = ethers.BigNumber.from(taskId.toString()); - const convertedArgs = []; - args.forEach((arg) => { - if (Number.isInteger(arg)) { - const convertedArg = ethers.BigNumber.from(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.BigNumber.from(arg.toString()); - convertedArgs.push(convertedArg); - } else { - convertedArgs.push(arg); - } - }); - - const txData = await colony.contract.methods[functionName](...convertedArgs).encodeABI(); const sigsPromises = sigTypes.map((type, i) => { let privKey = []; if (privKeys) { diff --git a/helpers/test-helper.js b/helpers/test-helper.js index 85b3f1110d..d2e8d7a36f 100644 --- a/helpers/test-helper.js +++ b/helpers/test-helper.js @@ -1,8 +1,10 @@ /* globals artifacts */ 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, SUBMITTER_ONLY_WINDOW } from "./constants"; @@ -825,6 +827,24 @@ export async function getWaitForNSubmissionsPromise(repCycleEthers, rootHash, nL reject(new Error("Timeout while waiting for 12 hash submissions")); }, 60 * 1000); }); + +export async function encodeTxData(colony, functionName, args) { + const convertedArgs = []; + args.forEach(arg => { + if (Number.isInteger(arg)) { + const convertedArg = ethers.BigNumber.from(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.BigNumber.from(arg.toString()); + convertedArgs.push(convertedArg); + } else { + convertedArgs.push(arg); + } + }); + + const txData = await colony.contract.methods[functionName](...convertedArgs).encodeABI(); + return txData; } export async function getRewardClaimSquareRootsAndProofs(client, tokenLocking, colony, payoutId, userAddress) { 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 deleted file mode 100644 index e0723ba9ef..0000000000 --- a/test/extensions/voting-token.js +++ /dev/null @@ -1,95 +0,0 @@ -/* globals artifacts */ - -import chai from "chai"; -import bnChai from "bn-chai"; -import shortid from "shortid"; -import { soliditySha3 } from "web3-utils"; - -import { WAD, SECONDS_PER_DAY } from "../../helpers/constants"; -import { setupColonyNetwork, setupColony } from "../../helpers/test-data-generator"; -import { forwardTime, getTokenArgs } from "../../helpers/test-helper"; - -const { expect } = chai; -chai.use(bnChai(web3.utils.BN)); - -const Token = artifacts.require("Token"); -const TokenAuthority = artifacts.require("TokenAuthority"); -const TokenLocking = artifacts.require("TokenLocking"); -const VotingToken = artifacts.require("VotingToken"); - -contract("Voting Token", accounts => { - let colony; - let token; - let colonyNetwork; - let tokenLocking; - let votingToken; - - const USER0 = accounts[0]; - const USER1 = accounts[1]; - - const SALT = soliditySha3(shortid.generate()); - const WAD2 = WAD.muln(2); - - before(async () => { - colonyNetwork = await setupColonyNetwork(); - - const tokenLockingAddress = await colonyNetwork.getTokenLocking(); - tokenLocking = await TokenLocking.at(tokenLockingAddress); - }); - - beforeEach(async () => { - token = await Token.new(...getTokenArgs()); - colony = await setupColony(colonyNetwork, token.address); - - 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(USER1, WAD); - await token.approve(tokenLocking.address, WAD, { from: USER1 }); - await tokenLocking.deposit(token.address, WAD, { from: USER1 }); - - votingToken = await VotingToken.new(colony.address); - }); - - describe.only("token voting", async () => { - it("can create a new poll", async () => { - let pollCount = await votingToken.getPollCount(); - expect(pollCount).to.be.zero; - - await votingToken.createPoll(2, SECONDS_PER_DAY); - pollCount = await votingToken.getPollCount(); - expect(pollCount).to.eq.BN(1); - }); - - 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 forwardTime(SECONDS_PER_DAY, this); - await votingToken.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 forwardTime(SECONDS_PER_DAY, this); - - await votingToken.revealVote(pollId, SALT, 0, { from: USER0 }); - await votingToken.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); - expect(voteCounts[2]).to.be.zero; - }); - }); -}); From c99f70a67ddb820ba5cefd38af5454e2888a8182 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Fri, 21 Jun 2019 11:24:08 +0300 Subject: [PATCH 03/61] Add VotingReputationFactory --- .../extensions/VotingReputationFactory.sol | 48 +++++++++++++++++++ scripts/check-recovery.js | 1 + test/extensions/voting-rep.js | 16 +++++-- 3 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 contracts/extensions/VotingReputationFactory.sol diff --git a/contracts/extensions/VotingReputationFactory.sol b/contracts/extensions/VotingReputationFactory.sol new file mode 100644 index 0000000000..c29dbddd53 --- /dev/null +++ b/contracts/extensions/VotingReputationFactory.sol @@ -0,0 +1,48 @@ +/* + This file is part of The Colony Network. + + The Colony Network is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + The Colony Network is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with The Colony Network. If not, see . +*/ + +pragma solidity 0.5.8; +pragma experimental ABIEncoderV2; + +import "./../ColonyDataTypes.sol"; +import "./../IColony.sol"; +import "./ExtensionFactory.sol"; +import "./VotingReputation.sol"; + + +contract VotingReputationFactory is ExtensionFactory, ColonyDataTypes { // ignore-swc-123 + mapping (address => VotingReputation) public deployedExtensions; + + modifier isRoot(address _colony) { + require(IColony(_colony).hasUserRole(msg.sender, 1, ColonyRole.Root), "colony-extension-user-not-root"); // ignore-swc-123 + _; + } + + function deployExtension(address _colony) external isRoot(_colony) { + require(deployedExtensions[_colony] == VotingReputation(0x00), "colony-extension-already-deployed"); + VotingReputation newExtensionAddress = new VotingReputation(_colony); + deployedExtensions[_colony] = newExtensionAddress; + + emit ExtensionDeployed("VotingReputation", _colony, address(newExtensionAddress)); + } + + function removeExtension(address _colony) external isRoot(_colony) { + deployedExtensions[_colony] = VotingReputation(0x00); + + emit ExtensionRemoved("VotingReputation", _colony); + } +} diff --git a/scripts/check-recovery.js b/scripts/check-recovery.js index 5d3a8f2346..462fc5416a 100644 --- a/scripts/check-recovery.js +++ b/scripts/check-recovery.js @@ -48,6 +48,7 @@ walkSync("./contracts/").forEach((contractName) => { "contracts/extensions/OneTxPaymentFactory.sol", "contracts/extensions/VotingBase.sol", "contracts/extensions/VotingReputation.sol", + "contracts/extensions/VotingReputationFactory.sol", "contracts/gnosis/MultiSigWallet.sol", "contracts/patriciaTree/Bits.sol", "contracts/patriciaTree/Data.sol", diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index 20bc9f125d..a882df8df0 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -21,12 +21,16 @@ const { expect } = chai; chai.use(bnChai(web3.utils.BN)); const VotingReputation = artifacts.require("VotingReputation"); +const VotingReputationFactory = artifacts.require("VotingReputationFactory"); contract("Voting Reputation", accounts => { let colony; let metaColony; let colonyNetwork; + let voting; + let votingFactory; + let reputationTree; const USER0 = accounts[0]; @@ -43,11 +47,15 @@ contract("Voting Reputation", accounts => { await giveUserCLNYTokensAndStake(colonyNetwork, MINER, DEFAULT_STAKE); await colonyNetwork.initialiseReputationMining(); await colonyNetwork.startNextCycle(); + + votingFactory = await VotingReputationFactory.new(); }); beforeEach(async () => { ({ colony } = await setupRandomColony(colonyNetwork)); - voting = await VotingReputation.new(colony.address); + await votingFactory.deployExtension(colony.address); + const votingAddress = await votingFactory.deployedExtensions(colony.address); + voting = await VotingReputation.at(votingAddress); reputationTree = new PatriciaTree(); await reputationTree.insert( @@ -74,7 +82,7 @@ contract("Voting Reputation", accounts => { await repCycle.confirmNewHash(0); }); - describe.only("creating and editing polls", async () => { + describe("creating and editing polls", async () => { it("can create a new poll", async () => { let pollId = await voting.getPollCount(); expect(pollId).to.be.zero; @@ -143,7 +151,7 @@ contract("Voting Reputation", accounts => { }); }); - describe.only("voting on polls", async () => { + describe("voting on polls", async () => { let key, value, mask, siblings, pollId; // eslint-disable-line one-var beforeEach(async () => { @@ -295,7 +303,7 @@ contract("Voting Reputation", accounts => { }); }); - describe.only("simple exceptions", async () => { + describe("simple exceptions", async () => { let pollId; beforeEach(async () => { From a90996ff14eff7775566e60d809ca99b28e2d1bd Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Wed, 26 Jun 2019 13:13:54 +0300 Subject: [PATCH 04/61] Make polls binary Remove quorum requrement --- contracts/extensions/VotingBase.sol | 64 ++---- contracts/extensions/VotingReputation.sol | 38 ++-- test/extensions/voting-rep.js | 257 ++++++++-------------- 3 files changed, 128 insertions(+), 231 deletions(-) diff --git a/contracts/extensions/VotingBase.sol b/contracts/extensions/VotingBase.sol index aa29613937..ab091d9a3e 100644 --- a/contracts/extensions/VotingBase.sol +++ b/contracts/extensions/VotingBase.sol @@ -38,56 +38,31 @@ contract VotingBase is DSMath { } // Data structures - enum PollState { Pending, Open, Reveal, Closed, Executed } + enum PollState { Open, Reveal, Closed, Executed } struct Poll { bool executed; - address creator; - uint256 pollCloses; - uint256[] voteCounts; + uint256 closes; + uint256[2] votes; // [nay, yay] + uint256 maxVotes; + bytes action; } // 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; + Poll storage poll = polls[_pollId]; + poll.executed = true; - uint256 winner = getPollWinner(_pollId); - bytes storage action = pollActions[_pollId][winner]; - return executeCall(address(colony), action); + if (poll.votes[0] < poll.votes[1]) { + return executeCall(address(colony), poll.action); + } } function getPollCount() public view returns (uint256) { @@ -100,11 +75,9 @@ contract VotingBase is DSMath { 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) { + if (now < poll.closes) { return PollState.Open; - } else if (now < add(poll.pollCloses, REVEAL_PERIOD)) { + } else if (now < add(poll.closes, REVEAL_PERIOD)) { return PollState.Reveal; } else if (!poll.executed) { return PollState.Closed; @@ -113,17 +86,6 @@ contract VotingBase is DSMath { } } - 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)) @@ -135,7 +97,7 @@ contract VotingBase is DSMath { } } - function getVoteSecret(bytes32 _salt, uint256 _vote) internal pure returns (bytes32) { + function getVoteSecret(bytes32 _salt, bool _vote) internal pure returns (bytes32) { return keccak256(abi.encodePacked(_salt, _vote)); } } diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index c5c59c6054..2dbd85a04f 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -37,17 +37,25 @@ contract VotingReputation is VotingBase, PatriciaTreeProofs { mapping (address => mapping (uint256 => bytes32)) userVotes; - function setPollRepInfo(uint256 _pollId, uint256 _skillId) public onlyCreator(_pollId) pollPending(_pollId) { - repInfo[pollCount] = RepInfo({ - rootHash: colonyNetwork.getReputationRootHash(), - skillId: _skillId - }); - } + function createPoll( + bytes memory _action, + uint256 _duration, + uint256 _skillId, + bytes memory _key, + bytes memory _value, + uint256 _branchMask, + bytes32[] memory _siblings + ) + public + { + pollCount += 1; + + repInfo[pollCount].rootHash = colonyNetwork.getReputationRootHash(); + repInfo[pollCount].skillId = _skillId; - // 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); + polls[pollCount].closes = add(now, _duration); + polls[pollCount].maxVotes = checkReputation(pollCount, address(0x0), _key, _value, _branchMask, _siblings); + polls[pollCount].action = _action; } function submitVote(uint256 _pollId, bytes32 _voteSecret) public { @@ -58,7 +66,7 @@ contract VotingReputation is VotingBase, PatriciaTreeProofs { function revealVote( uint256 _pollId, bytes32 _salt, - uint256 _vote, + bool _vote, bytes memory _key, bytes memory _value, uint256 _branchMask, @@ -70,10 +78,9 @@ contract VotingReputation is VotingBase, PatriciaTreeProofs { bytes32 voteSecret = userVotes[msg.sender][_pollId]; 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); + uint256 userReputation = checkReputation(_pollId, msg.sender, _key, _value, _branchMask, _siblings); // Remove the secret delete userVotes[msg.sender][_pollId]; @@ -81,12 +88,13 @@ contract VotingReputation is VotingBase, PatriciaTreeProofs { // Increment the vote if poll in reveal, otherwise skip // NOTE: since there's no locking, we could just `require` PollState.Reveal if (getPollState(_pollId) == PollState.Reveal) { - polls[_pollId].voteCounts[_vote] += userReputation; + polls[_pollId].votes[_vote ? 1 : 0] += userReputation; } } function checkReputation( uint256 _pollId, + address _who, bytes memory _key, bytes memory _value, uint256 _branchMask, @@ -111,7 +119,7 @@ contract VotingReputation is VotingBase, PatriciaTreeProofs { 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"); + require(keyUserAddress == _who, "voting-rep-invalid-user-address"); return reputationValue; } diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index a882df8df0..670c9be524 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -58,21 +58,25 @@ contract("Voting Reputation", accounts => { voting = await VotingReputation.at(votingAddress); reputationTree = new PatriciaTree(); + await reputationTree.insert( + makeReputationKey(colony.address, 1), // Colony total + makeReputationValue(WAD2.add(WAD), 1) + ); await reputationTree.insert( makeReputationKey(colony.address, 1, USER0), // All good - makeReputationValue(WAD, 1) + makeReputationValue(WAD, 2) ); await reputationTree.insert( makeReputationKey(metaColony.address, 1, USER0), // Wrong colony - makeReputationValue(WAD, 2) + makeReputationValue(WAD, 3) ); await reputationTree.insert( makeReputationKey(colony.address, 2, USER0), // Wrong skill - makeReputationValue(WAD, 3) + makeReputationValue(WAD, 4) ); await reputationTree.insert( makeReputationKey(colony.address, 1, USER1), // Wrong user (and 2x value) - makeReputationValue(WAD2, 4) + makeReputationValue(WAD2, 5) ); const rootHash = await reputationTree.getRootHash(); @@ -82,72 +86,13 @@ contract("Voting Reputation", accounts => { await repCycle.confirmNewHash(0); }); - describe("creating and editing polls", async () => { - it("can create a new poll", async () => { - let pollId = await voting.getPollCount(); - expect(pollId).to.be.zero; - - 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("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 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("cannot add an option once a poll is open", 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.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("using the extension factory", async () => { + it("can install the extension factory once if root and uninstall", async () => { + ({ colony } = await setupRandomColony(colonyNetwork)); + await checkErrorRevert(votingFactory.deployExtension(colony.address, { from: USER1 }), "colony-extension-user-not-root"); + await votingFactory.deployExtension(colony.address, { from: USER0 }); + await checkErrorRevert(votingFactory.deployExtension(colony.address, { from: USER0 }), "colony-extension-already-deployed"); + await votingFactory.removeExtension(colony.address, { from: USER0 }); }); }); @@ -155,100 +100,84 @@ contract("Voting Reputation", accounts => { let key, value, mask, siblings, pollId; // eslint-disable-line one-var beforeEach(async () => { - await voting.createPoll(); - pollId = await voting.getPollCount(); + key = makeReputationKey(colony.address, 1); + value = makeReputationValue(WAD2.add(WAD), 1); + [mask, siblings] = await reputationTree.getProof(key); - 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); + const action = await encodeTxData(colony, "makeTask", [1, 0, FAKE, 1, 0, 0]); + await voting.createPoll(action, SECONDS_PER_DAY, 1, key, value, mask, siblings); + pollId = await voting.getPollCount(); key = makeReputationKey(colony.address, 1, USER0); - value = makeReputationValue(WAD, 1); + value = makeReputationValue(WAD, 2); [mask, siblings] = await reputationTree.getProof(key); }); it("can rate and reveal for a poll", async () => { - await voting.submitVote(pollId, soliditySha3(SALT, 0), { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, false), { from: USER0 }); await forwardTime(SECONDS_PER_DAY, this); - await voting.revealVote(pollId, SALT, 0, key, value, mask, siblings, { from: USER0 }); + await voting.revealVote(pollId, SALT, false, 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 voting.submitVote(pollId, soliditySha3(SALT, false), { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, true), { from: USER1 }); 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); + await voting.revealVote(pollId, SALT, false, key, value, mask, siblings, { from: USER0 }); key = makeReputationKey(colony.address, 1, USER1); - value = makeReputationValue(WAD2, 4); + value = makeReputationValue(WAD2, 5); [mask, siblings] = await reputationTree.getProof(key); - await voting.revealVote(pollId, SALT, 1, key, value, mask, siblings, { from: USER1 }); - - // Second option wins - pollWinner = await voting.getPollWinner(pollId); - expect(pollWinner).to.eq.BN(1); + await voting.revealVote(pollId, SALT, true, key, value, mask, siblings, { from: USER1 }); // 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); + const { votes } = await voting.getPollInfo(pollId); + expect(votes[0]).to.eq.BN(WAD); + expect(votes[1]).to.eq.BN(WAD2); }); it("can update votes, but only last one counts", async () => { - await voting.submitVote(pollId, soliditySha3(SALT, 0), { from: USER0 }); - await voting.submitVote(pollId, soliditySha3(SALT, 1), { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, false), { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, true), { from: USER0 }); await forwardTime(SECONDS_PER_DAY, this); // Revealing first vote fails - await checkErrorRevert(voting.revealVote(pollId, SALT, 0, key, value, mask, siblings, { from: USER0 }), "voting-rep-secret-no-match"); + await checkErrorRevert(voting.revealVote(pollId, SALT, false, key, value, mask, siblings, { from: USER0 }), "voting-rep-secret-no-match"); // Revealing second succeeds - await voting.revealVote(pollId, SALT, 1, key, value, mask, siblings, { from: USER0 }); + await voting.revealVote(pollId, SALT, true, key, value, mask, siblings, { from: USER0 }); }); it("can reveal votes after poll closes, but doesn't count", async () => { - await voting.submitVote(pollId, soliditySha3(SALT, 0), { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, false), { from: USER0 }); // Close the poll (1 day voting, 2 day reveal) await forwardTime(SECONDS_PER_DAY * 3, this); - await voting.revealVote(pollId, SALT, 0, key, value, mask, siblings, { from: USER0 }); + await voting.revealVote(pollId, SALT, false, key, value, mask, siblings, { from: USER0 }); // Vote didn't count - const { voteCounts } = await voting.getPollInfo(pollId); - expect(voteCounts[0]).to.be.zero; - expect(voteCounts[1]).to.be.zero; - expect(voteCounts[2]).to.be.zero; + const { votes } = await voting.getPollInfo(pollId); + expect(votes[0]).to.be.zero; + expect(votes[1]).to.be.zero; }); it("cannot reveal a vote twice, and so cannot vote twice", async () => { - await voting.submitVote(pollId, soliditySha3(SALT, 0), { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, false), { from: USER0 }); await forwardTime(SECONDS_PER_DAY, this); - 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"); + await voting.revealVote(pollId, SALT, false, key, value, mask, siblings, { from: USER0 }); + await checkErrorRevert(voting.revealVote(pollId, SALT, false, 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 voting.submitVote(pollId, soliditySha3(SALT, 0), { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, false), { from: USER0 }); // Update reputation state - const value2 = makeReputationValue(WAD.muln(3), 1); + const value2 = makeReputationValue(WAD.muln(3), 2); await reputationTree.insert(key, value2); // Set new rootHash @@ -259,37 +188,29 @@ contract("Voting Reputation", accounts => { await repCycle.confirmNewHash(0); // Create new poll with new reputation state - await voting.createPoll(); + const keyColony = makeReputationKey(colony.address, 1); + const valueColony = makeReputationValue(WAD2.add(WAD), 1); + const [maskColony, siblingsColony] = await reputationTree.getProof(keyColony); + await voting.createPoll(FAKE, SECONDS_PER_DAY, 1, keyColony, valueColony, maskColony, siblingsColony); + 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 voting.submitVote(pollId2, soliditySha3(SALT, false), { from: USER0 }); await forwardTime(SECONDS_PER_DAY, this); const [mask2, siblings2] = await reputationTree.getProof(key); - await voting.revealVote(pollId, SALT, 0, key, value, mask, siblings, { from: USER0 }); - await voting.revealVote(pollId2, SALT, 0, key, value2, mask2, siblings2, { from: USER0 }); + await voting.revealVote(pollId, SALT, false, key, value, mask, siblings, { from: USER0 }); + await voting.revealVote(pollId2, SALT, false, key, value2, mask2, siblings2, { from: USER0 }); }); - it("can take an action based on the result of a poll", async () => { + it("can take an action if the poll passes", 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 voting.submitVote(pollId, soliditySha3(SALT, true), { from: USER0 }); await forwardTime(SECONDS_PER_DAY, this); - await voting.revealVote(pollId, SALT, 1, key, value, mask, siblings, { from: USER0 }); + await voting.revealVote(pollId, SALT, true, key, value, mask, siblings, { from: USER0 }); await checkErrorRevert(voting.executePoll(pollId), "voting-base-poll-not-closed"); @@ -301,74 +222,80 @@ contract("Voting Reputation", accounts => { await checkErrorRevert(voting.executePoll(pollId), "voting-base-poll-already-executed"); }); + + it("cannot take an action if the poll fails", async () => { + await colony.setAdministrationRole(1, 0, voting.address, 1, true); + + await voting.submitVote(pollId, soliditySha3(SALT, false), { from: USER0 }); + + await forwardTime(SECONDS_PER_DAY, this); + await voting.revealVote(pollId, SALT, false, key, value, mask, siblings, { from: USER0 }); + + 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); + }); }); describe("simple exceptions", async () => { let pollId; 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.openPoll(pollId, SECONDS_PER_DAY); - }); + const key = makeReputationKey(colony.address, 1); + const value = makeReputationValue(WAD2.add(WAD), 1); + const [mask, siblings] = await reputationTree.getProof(key); - it("cannot submit a vote if poll is pending", async () => { - await voting.createPoll(); + await voting.createPoll(FAKE, SECONDS_PER_DAY, 1, key, value, mask, siblings); 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(voting.submitVote(pollId, soliditySha3(SALT, 0)), "voting-rep-poll-not-open"); + await checkErrorRevert(voting.submitVote(pollId, soliditySha3(SALT, false)), "voting-rep-poll-not-open"); }); it("cannot reveal a vote if voting is open", async () => { - await voting.submitVote(pollId, soliditySha3(SALT, 0)); - await checkErrorRevert(voting.revealVote(pollId, SALT, 1, FAKE, FAKE, 0, []), "voting-rep-poll-still-open"); + await voting.submitVote(pollId, soliditySha3(SALT, false)); + await checkErrorRevert(voting.revealVote(pollId, SALT, true, FAKE, FAKE, 0, []), "voting-rep-poll-still-open"); }); it("cannot reveal a vote with a bad secret", async () => { - await voting.submitVote(pollId, soliditySha3(SALT, 0)); - await forwardTime(SECONDS_PER_DAY, this); - await checkErrorRevert(voting.revealVote(pollId, SALT, 1, FAKE, FAKE, 0, []), "voting-rep-secret-no-match"); - }); - - it("cannot reveal an invalid vote", async () => { - await voting.submitVote(pollId, soliditySha3(SALT, 2)); + await voting.submitVote(pollId, soliditySha3(SALT, false)); await forwardTime(SECONDS_PER_DAY, this); - await checkErrorRevert(voting.revealVote(pollId, SALT, 2, FAKE, FAKE, 0, []), "voting-rep-invalid-vote"); + await checkErrorRevert(voting.revealVote(pollId, SALT, true, FAKE, FAKE, 0, []), "voting-rep-secret-no-match"); }); // VotingReputation specific it("cannot reveal a vote with a bad proof", async () => { - await voting.submitVote(pollId, soliditySha3(SALT, 0), { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, false), { from: USER0 }); await forwardTime(SECONDS_PER_DAY, this); // Invalid proof (wrong root hash) - await checkErrorRevert(voting.revealVote(pollId, SALT, 0, FAKE, FAKE, 0, [], { from: USER0 }), "voting-rep-invalid-root-hash"); + await checkErrorRevert(voting.revealVote(pollId, SALT, false, FAKE, FAKE, 0, [], { from: USER0 }), "voting-rep-invalid-root-hash"); // Invalid colony address let key, value, mask, siblings; // eslint-disable-line one-var key = makeReputationKey(metaColony.address, 1, USER0); - value = makeReputationValue(WAD, 2); + 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-colony-address"); + await checkErrorRevert( + voting.revealVote(pollId, SALT, false, key, value, mask, siblings, { from: USER0 }), + "voting-rep-invalid-colony-address" + ); // Invalid skill id key = makeReputationKey(colony.address, 2, USER0); - value = makeReputationValue(WAD, 3); + value = makeReputationValue(WAD, 4); [mask, siblings] = await reputationTree.getProof(key); - await checkErrorRevert(voting.revealVote(pollId, SALT, 0, key, value, mask, siblings, { from: USER0 }), "voting-rep-invalid-skill-id"); + await checkErrorRevert(voting.revealVote(pollId, SALT, false, key, value, mask, siblings, { from: USER0 }), "voting-rep-invalid-skill-id"); // Invalid user address key = makeReputationKey(colony.address, 1, USER1); - value = makeReputationValue(WAD2, 4); + value = makeReputationValue(WAD2, 5); [mask, siblings] = await reputationTree.getProof(key); - await checkErrorRevert(voting.revealVote(pollId, SALT, 0, key, value, mask, siblings, { from: USER0 }), "voting-rep-invalid-user-address"); + await checkErrorRevert(voting.revealVote(pollId, SALT, false, key, value, mask, siblings, { from: USER0 }), "voting-rep-invalid-user-address"); }); }); }); From df4189f24fa146e56eb2d7566a7e723b04c708e1 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Thu, 9 Jan 2020 08:48:29 +0200 Subject: [PATCH 05/61] Add ability to create root and domain polls --- contracts/extensions/VotingBase.sol | 11 +- contracts/extensions/VotingReputation.sol | 87 +++++++++++++--- test/extensions/voting-rep.js | 121 ++++++++++++++++++---- 3 files changed, 184 insertions(+), 35 deletions(-) diff --git a/contracts/extensions/VotingBase.sol b/contracts/extensions/VotingBase.sol index ab091d9a3e..d351f91709 100644 --- a/contracts/extensions/VotingBase.sol +++ b/contracts/extensions/VotingBase.sol @@ -26,6 +26,8 @@ import "../IColonyNetwork.sol"; contract VotingBase is DSMath { // Constants + uint256 constant UINT256_MAX = 2**256 - 1; + uint256 constant VOTE_PERIOD = 1 days; uint256 constant REVEAL_PERIOD = 2 days; // Initialization data @@ -42,9 +44,10 @@ contract VotingBase is DSMath { struct Poll { bool executed; - uint256 closes; + uint256 createdAt; + uint256 skillRep; + uint256[2] stakes; // [nay, yay] uint256[2] votes; // [nay, yay] - uint256 maxVotes; bytes action; } @@ -75,9 +78,9 @@ contract VotingBase is DSMath { function getPollState(uint256 _pollId) public view returns (PollState) { Poll storage poll = polls[_pollId]; - if (now < poll.closes) { + if (now < poll.createdAt + VOTE_PERIOD) { return PollState.Open; - } else if (now < add(poll.closes, REVEAL_PERIOD)) { + } else if (now < poll.createdAt + VOTE_PERIOD + REVEAL_PERIOD) { return PollState.Reveal; } else if (!poll.executed) { return PollState.Closed; diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index 2dbd85a04f..c2237f54b6 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -31,16 +31,15 @@ contract VotingReputation is VotingBase, PatriciaTreeProofs { uint256 skillId; } - mapping (uint256 => RepInfo) repInfo; + mapping (uint256 => RepInfo) repInfos; // The UserVote type here is just the bytes32 voteSecret - mapping (address => mapping (uint256 => bytes32)) userVotes; - function createPoll( + // Public functions + + function createRootPoll( bytes memory _action, - uint256 _duration, - uint256 _skillId, bytes memory _key, bytes memory _value, uint256 _branchMask, @@ -48,14 +47,30 @@ contract VotingReputation is VotingBase, PatriciaTreeProofs { ) public { - pollCount += 1; + uint256 rootSkillId = colony.getDomain(1).skillId; + createPoll(_action, rootSkillId, _key, _value, _branchMask, _siblings); + } + + function createDomainPoll( + uint256 _domainId, + uint256 _childSkillIndex, + bytes memory _action, + bytes memory _key, + bytes memory _value, + uint256 _branchMask, + bytes32[] memory _siblings + ) + public + { + uint256 domainSkillId = colony.getDomain(_domainId).skillId; + uint256 actionDomainSkillId = getActionDomainSkillId(_action); - repInfo[pollCount].rootHash = colonyNetwork.getReputationRootHash(); - repInfo[pollCount].skillId = _skillId; + if (domainSkillId != actionDomainSkillId) { + uint256 childSkillId = colonyNetwork.getChildSkillId(domainSkillId, _childSkillIndex); + require(childSkillId == actionDomainSkillId, "voting-rep-invalid-domain-id"); + } - polls[pollCount].closes = add(now, _duration); - polls[pollCount].maxVotes = checkReputation(pollCount, address(0x0), _key, _value, _branchMask, _siblings); - polls[pollCount].action = _action; + createPoll(_action, domainSkillId, _key, _value, _branchMask, _siblings); } function submitVote(uint256 _pollId, bytes32 _voteSecret) public { @@ -92,6 +107,34 @@ contract VotingReputation is VotingBase, PatriciaTreeProofs { } } + // Public view functions + + function getPollRepInfo(uint256 _pollId) public view returns (RepInfo memory repInfo) { + repInfo = repInfos[_pollId]; + } + + // Internal functions + + function createPoll( + bytes memory _action, + uint256 _skillId, + bytes memory _key, + bytes memory _value, + uint256 _branchMask, + bytes32[] memory _siblings + ) + internal + { + pollCount += 1; + + repInfos[pollCount].rootHash = colonyNetwork.getReputationRootHash(); + repInfos[pollCount].skillId = _skillId; + + polls[pollCount].createdAt = now; + polls[pollCount].skillRep = checkReputation(pollCount, address(0x0), _key, _value, _branchMask, _siblings); + polls[pollCount].action = _action; + } + function checkReputation( uint256 _pollId, address _who, @@ -103,7 +146,7 @@ contract VotingReputation is VotingBase, PatriciaTreeProofs { internal view returns (uint256) { bytes32 impliedRoot = getImpliedRootHashKey(_key, _value, _branchMask, _siblings); - require(repInfo[_pollId].rootHash == impliedRoot, "voting-rep-invalid-root-hash"); + require(repInfos[_pollId].rootHash == impliedRoot, "voting-rep-invalid-root-hash"); uint256 reputationValue; address keyColonyAddress; @@ -118,10 +161,28 @@ contract VotingReputation is VotingBase, PatriciaTreeProofs { } require(keyColonyAddress == address(colony), "voting-rep-invalid-colony-address"); - require(keySkill == repInfo[_pollId].skillId, "voting-rep-invalid-skill-id"); + require(keySkill == repInfos[_pollId].skillId, "voting-rep-invalid-skill-id"); require(keyUserAddress == _who, "voting-rep-invalid-user-address"); return reputationValue; } + function getActionDomainSkillId(bytes memory _action) internal view returns (uint256) { + uint256 permissionDomainId; + uint256 childSkillIndex; + + assembly { + permissionDomainId := mload(add(_action, 0x24)) + childSkillIndex := mload(add(_action, 0x44)) + } + + uint256 permissionSkillId = colony.getDomain(permissionDomainId).skillId; + + if (childSkillIndex == UINT256_MAX) { + return permissionSkillId; + } else { + return colonyNetwork.getChildSkillId(permissionSkillId, childSkillIndex); + } + } + } diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index 670c9be524..7a6b3c51e2 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -5,7 +5,7 @@ import bnChai from "bn-chai"; import shortid from "shortid"; import { soliditySha3 } from "web3-utils"; -import { WAD, MINING_CYCLE_DURATION, SECONDS_PER_DAY, DEFAULT_STAKE } from "../../helpers/constants"; +import { UINT256_MAX, WAD, MINING_CYCLE_DURATION, SECONDS_PER_DAY, DEFAULT_STAKE } from "../../helpers/constants"; import { checkErrorRevert, makeReputationKey, makeReputationValue, getActiveRepCycle, forwardTime, encodeTxData } from "../../helpers/test-helper"; import { @@ -25,6 +25,9 @@ const VotingReputationFactory = artifacts.require("VotingReputationFactory"); contract("Voting Reputation", accounts => { let colony; + let domain1; + let domain2; + let domain3; let metaColony; let colonyNetwork; @@ -53,31 +56,46 @@ contract("Voting Reputation", accounts => { beforeEach(async () => { ({ colony } = await setupRandomColony(colonyNetwork)); + + await colony.addDomain(1, 0, 1); + await colony.addDomain(1, 0, 1); + domain1 = await colony.getDomain(1); + domain2 = await colony.getDomain(2); + domain3 = await colony.getDomain(3); + await votingFactory.deployExtension(colony.address); const votingAddress = await votingFactory.deployedExtensions(colony.address); voting = await VotingReputation.at(votingAddress); reputationTree = new PatriciaTree(); await reputationTree.insert( - makeReputationKey(colony.address, 1), // Colony total + makeReputationKey(colony.address, domain1.skillId), // Colony total makeReputationValue(WAD2.add(WAD), 1) ); await reputationTree.insert( - makeReputationKey(colony.address, 1, USER0), // All good + makeReputationKey(colony.address, domain1.skillId, USER0), // All good makeReputationValue(WAD, 2) ); await reputationTree.insert( - makeReputationKey(metaColony.address, 1, USER0), // Wrong colony + makeReputationKey(metaColony.address, domain1.skillId, USER0), // Wrong colony makeReputationValue(WAD, 3) ); await reputationTree.insert( - makeReputationKey(colony.address, 2, USER0), // Wrong skill + makeReputationKey(colony.address, 1234, USER0), // Wrong skill makeReputationValue(WAD, 4) ); await reputationTree.insert( - makeReputationKey(colony.address, 1, USER1), // Wrong user (and 2x value) + makeReputationKey(colony.address, domain1.skillId, USER1), // Wrong user (and 2x value) makeReputationValue(WAD2, 5) ); + await reputationTree.insert( + makeReputationKey(colony.address, domain2.skillId), // Colony total, domain 2 + makeReputationValue(WAD, 6) + ); + await reputationTree.insert( + makeReputationKey(colony.address, domain3.skillId), // Colony total, domain 3 + makeReputationValue(WAD, 7) + ); const rootHash = await reputationTree.getRootHash(); const repCycle = await getActiveRepCycle(colonyNetwork); @@ -96,19 +114,86 @@ contract("Voting Reputation", accounts => { }); }); + describe("creating polls", async () => { + it("can create a root poll", async () => { + const key = makeReputationKey(colony.address, domain1.skillId); + const value = makeReputationValue(WAD2.add(WAD), 1); + const [mask, siblings] = await reputationTree.getProof(key); + + const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); + await voting.createRootPoll(action, key, value, mask, siblings); + + const pollId = await voting.getPollCount(); + const repInfo = await voting.getPollRepInfo(pollId); + expect(repInfo.skillId).to.eq.BN(domain1.skillId); + }); + + it("can create a domain poll in the root domain", async () => { + const key = makeReputationKey(colony.address, domain1.skillId); + const value = makeReputationValue(WAD.muln(3), 1); + const [mask, siblings] = await reputationTree.getProof(key); + + // Create poll in domain of action (1) + const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); + await voting.createDomainPoll(1, UINT256_MAX, action, key, value, mask, siblings); + + const pollId = await voting.getPollCount(); + const repInfo = await voting.getPollRepInfo(pollId); + expect(repInfo.skillId).to.eq.BN(domain1.skillId); + }); + + it("can create a domain poll in a child domain", async () => { + const key = makeReputationKey(colony.address, domain2.skillId); + const value = makeReputationValue(WAD, 6); + const [mask, siblings] = await reputationTree.getProof(key); + + // Create poll in domain of action (2) + const action = await encodeTxData(colony, "makeTask", [1, 0, FAKE, 2, 0, 0]); + await voting.createDomainPoll(2, UINT256_MAX, action, key, value, mask, siblings); + + const pollId = await voting.getPollCount(); + const repInfo = await voting.getPollRepInfo(pollId); + expect(repInfo.skillId).to.eq.BN(domain2.skillId); + }); + + it("can escalate a domain poll", async () => { + const key = makeReputationKey(colony.address, domain1.skillId); + const value = makeReputationValue(WAD.muln(3), 1); + const [mask, siblings] = await reputationTree.getProof(key); + + // Create poll in parent domain (1) of action (2) + const action = await encodeTxData(colony, "makeTask", [1, 0, FAKE, 2, 0, 0]); + await voting.createDomainPoll(1, 0, action, key, value, mask, siblings); + + const pollId = await voting.getPollCount(); + const repInfo = await voting.getPollRepInfo(pollId); + expect(repInfo.skillId).to.eq.BN(domain1.skillId); + }); + + it("cannot escalate a domain poll with an invalid domain proof", async () => { + const key = makeReputationKey(colony.address, domain3.skillId); + const value = makeReputationValue(WAD, 7); + const [mask, siblings] = await reputationTree.getProof(key); + + // Provide proof for (3) instead of (2) + const action = await encodeTxData(colony, "makeTask", [1, 0, FAKE, 2, 0, 0]); + await checkErrorRevert(voting.createDomainPoll(1, 1, action, key, value, mask, siblings), "voting-rep-invalid-domain-id"); + }); + }); + describe("voting on polls", async () => { let key, value, mask, siblings, pollId; // eslint-disable-line one-var beforeEach(async () => { - key = makeReputationKey(colony.address, 1); + key = makeReputationKey(colony.address, domain1.skillId); value = makeReputationValue(WAD2.add(WAD), 1); [mask, siblings] = await reputationTree.getProof(key); - const action = await encodeTxData(colony, "makeTask", [1, 0, FAKE, 1, 0, 0]); - await voting.createPoll(action, SECONDS_PER_DAY, 1, key, value, mask, siblings); + const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); + await voting.createRootPoll(action, key, value, mask, siblings); pollId = await voting.getPollCount(); - key = makeReputationKey(colony.address, 1, USER0); + key = makeReputationKey(colony.address, domain1.skillId, USER0); value = makeReputationValue(WAD, 2); [mask, siblings] = await reputationTree.getProof(key); }); @@ -126,7 +211,7 @@ contract("Voting Reputation", accounts => { await forwardTime(SECONDS_PER_DAY, this); await voting.revealVote(pollId, SALT, false, key, value, mask, siblings, { from: USER0 }); - key = makeReputationKey(colony.address, 1, USER1); + key = makeReputationKey(colony.address, domain1.skillId, USER1); value = makeReputationValue(WAD2, 5); [mask, siblings] = await reputationTree.getProof(key); await voting.revealVote(pollId, SALT, true, key, value, mask, siblings, { from: USER1 }); @@ -188,10 +273,10 @@ contract("Voting Reputation", accounts => { await repCycle.confirmNewHash(0); // Create new poll with new reputation state - const keyColony = makeReputationKey(colony.address, 1); + const keyColony = makeReputationKey(colony.address, domain1.skillId); const valueColony = makeReputationValue(WAD2.add(WAD), 1); const [maskColony, siblingsColony] = await reputationTree.getProof(keyColony); - await voting.createPoll(FAKE, SECONDS_PER_DAY, 1, keyColony, valueColony, maskColony, siblingsColony); + await voting.createRootPoll(FAKE, keyColony, valueColony, maskColony, siblingsColony); const pollId2 = await voting.getPollCount(); @@ -243,11 +328,11 @@ contract("Voting Reputation", accounts => { let pollId; beforeEach(async () => { - const key = makeReputationKey(colony.address, 1); + const key = makeReputationKey(colony.address, domain1.skillId); const value = makeReputationValue(WAD2.add(WAD), 1); const [mask, siblings] = await reputationTree.getProof(key); - await voting.createPoll(FAKE, SECONDS_PER_DAY, 1, key, value, mask, siblings); + await voting.createRootPoll(FAKE, key, value, mask, siblings); pollId = await voting.getPollCount(); }); @@ -277,7 +362,7 @@ contract("Voting Reputation", accounts => { // Invalid colony address let key, value, mask, siblings; // eslint-disable-line one-var - key = makeReputationKey(metaColony.address, 1, USER0); + key = makeReputationKey(metaColony.address, domain1.skillId, USER0); value = makeReputationValue(WAD, 3); [mask, siblings] = await reputationTree.getProof(key); await checkErrorRevert( @@ -286,13 +371,13 @@ contract("Voting Reputation", accounts => { ); // Invalid skill id - key = makeReputationKey(colony.address, 2, USER0); + key = makeReputationKey(colony.address, 1234, USER0); value = makeReputationValue(WAD, 4); [mask, siblings] = await reputationTree.getProof(key); await checkErrorRevert(voting.revealVote(pollId, SALT, false, key, value, mask, siblings, { from: USER0 }), "voting-rep-invalid-skill-id"); // Invalid user address - key = makeReputationKey(colony.address, 1, USER1); + key = makeReputationKey(colony.address, domain1.skillId, USER1); value = makeReputationValue(WAD2, 5); [mask, siblings] = await reputationTree.getProof(key); await checkErrorRevert(voting.revealVote(pollId, SALT, false, key, value, mask, siblings, { from: USER0 }), "voting-rep-invalid-user-address"); From 6374b711dd4a02ba660234d8039a49e80de3d4b8 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Sun, 19 Jan 2020 14:44:59 +0200 Subject: [PATCH 06/61] Delete VotingBase and add support for staking --- contracts/extensions/VotingBase.sol | 106 ------------------ contracts/extensions/VotingReputation.sol | 125 +++++++++++++++++++--- scripts/check-storage.js | 2 + test/extensions/voting-rep.js | 81 ++++++++++++-- 4 files changed, 185 insertions(+), 129 deletions(-) delete mode 100644 contracts/extensions/VotingBase.sol diff --git a/contracts/extensions/VotingBase.sol b/contracts/extensions/VotingBase.sol deleted file mode 100644 index d351f91709..0000000000 --- a/contracts/extensions/VotingBase.sol +++ /dev/null @@ -1,106 +0,0 @@ -/* - This file is part of The Colony Network. - - The Colony Network is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - The Colony Network is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with The Colony Network. If not, see . -*/ - -pragma solidity 0.5.8; -pragma experimental ABIEncoderV2; - -import "../../lib/dappsys/math.sol"; -import "../IColony.sol"; -import "../IColonyNetwork.sol"; - - -contract VotingBase is DSMath { - - // Constants - uint256 constant UINT256_MAX = 2**256 - 1; - uint256 constant VOTE_PERIOD = 1 days; - uint256 constant REVEAL_PERIOD = 2 days; - - // Initialization data - IColony colony; - IColonyNetwork colonyNetwork; - - constructor(address _colony) public { - colony = IColony(_colony); - colonyNetwork = IColonyNetwork(colony.getColonyNetwork()); - } - - // Data structures - enum PollState { Open, Reveal, Closed, Executed } - - struct Poll { - bool executed; - uint256 createdAt; - uint256 skillRep; - uint256[2] stakes; // [nay, yay] - uint256[2] votes; // [nay, yay] - bytes action; - } - - // Storage - uint256 pollCount; - mapping (uint256 => Poll) polls; - - // Functions - 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"); - - Poll storage poll = polls[_pollId]; - poll.executed = true; - - if (poll.votes[0] < poll.votes[1]) { - return executeCall(address(colony), poll.action); - } - } - - function getPollCount() public view returns (uint256) { - return pollCount; - } - - function getPollInfo(uint256 _pollId) public view returns (Poll memory poll) { - poll = polls[_pollId]; - } - - function getPollState(uint256 _pollId) public view returns (PollState) { - Poll storage poll = polls[_pollId]; - if (now < poll.createdAt + VOTE_PERIOD) { - return PollState.Open; - } else if (now < poll.createdAt + VOTE_PERIOD + REVEAL_PERIOD) { - return PollState.Reveal; - } else if (!poll.executed) { - return PollState.Closed; - } else { - return PollState.Executed; - } - } - - 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) - } - } - - function getVoteSecret(bytes32 _salt, bool _vote) internal pure returns (bytes32) { - return keccak256(abi.encodePacked(_salt, _vote)); - } -} diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index c2237f54b6..7adf84b088 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -18,25 +18,52 @@ pragma solidity 0.5.8; pragma experimental ABIEncoderV2; +import "../IColony.sol"; +import "../IColonyNetwork.sol"; import "../PatriciaTree/PatriciaTreeProofs.sol"; -import "./VotingBase.sol"; +import "../../lib/dappsys/math.sol"; -contract VotingReputation is VotingBase, PatriciaTreeProofs { +contract VotingReputation is DSMath, PatriciaTreeProofs { - constructor(address _colony) public VotingBase(_colony) {} + // Constants + uint256 constant UINT256_MAX = 2**256 - 1; + uint256 constant STAKE_INVERSE = 1000; + uint256 constant VOTE_PERIOD = 1 days; + uint256 constant REVEAL_PERIOD = 2 days; - struct RepInfo { + // Initialization data + IColony colony; + IColonyNetwork colonyNetwork; + + constructor(address _colony) public { + colony = IColony(_colony); + colonyNetwork = IColonyNetwork(colony.getColonyNetwork()); + } + + // Data structures + enum PollState { Open, Reveal, Closed, Executed } + + struct Poll { + bool executed; + uint256 createdAt; bytes32 rootHash; uint256 skillId; + uint256 skillRep; + uint256[2] stakes; // [nay, yay] + uint256[2] votes; // [nay, yay] + bytes action; } - mapping (uint256 => RepInfo) repInfos; + // Storage + uint256 pollCount; + mapping (uint256 => Poll) polls; + mapping (uint256 => mapping (address => mapping (bool => uint256))) stakers; // The UserVote type here is just the bytes32 voteSecret mapping (address => mapping (uint256 => bytes32)) userVotes; - // Public functions + // Public functions (interface) function createRootPoll( bytes memory _action, @@ -73,6 +100,39 @@ contract VotingReputation is VotingBase, PatriciaTreeProofs { createPoll(_action, domainSkillId, _key, _value, _branchMask, _siblings); } + function stakePoll(uint256 _pollId, uint256 _domainId, bool _vote, uint256 _amount) public { + Poll storage poll = polls[_pollId]; + + // TODO: can we keep the domainId on the poll somewhere? This seems like a wasteful external call. + // But if it's 10 external calls per word of storage, then < 10 stakers makes this cheaper. + require(colony.getDomain(_domainId).skillId == poll.skillId, "voting-rep-bad-stake-domain"); + require(stakers[_pollId][msg.sender][!_vote] == 0, "voting-rep-cannot-stake-both-sides"); + + // TODO: come up with something better than `bool2vote`. Maybe an enum? + uint256 currentStake = poll.stakes[bool2vote(_vote)]; + uint256 requiredStake = poll.skillRep / STAKE_INVERSE; + + require(add(currentStake, _amount) <= requiredStake, "voting-rep-stake-too-large"); + + poll.stakes[bool2vote(_vote)] = add(poll.stakes[bool2vote(_vote)], _amount); + stakers[_pollId][msg.sender][_vote] = add(stakers[_pollId][msg.sender][_vote], _amount); + + // TODO: add implementation! + // colony.obligateStake(msg.sender, _domainId, _amount); + } + + 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"); + + Poll storage poll = polls[_pollId]; + poll.executed = true; + + if (poll.votes[0] < poll.votes[1]) { + return executeCall(address(colony), poll.action); + } + } + function submitVote(uint256 _pollId, bytes32 _voteSecret) public { require(getPollState(_pollId) == PollState.Open, "voting-rep-poll-not-open"); userVotes[msg.sender][_pollId] = _voteSecret; @@ -103,14 +163,35 @@ contract VotingReputation is VotingBase, PatriciaTreeProofs { // Increment the vote if poll in reveal, otherwise skip // NOTE: since there's no locking, we could just `require` PollState.Reveal if (getPollState(_pollId) == PollState.Reveal) { - polls[_pollId].votes[_vote ? 1 : 0] += userReputation; + polls[_pollId].votes[bool2vote(_vote)] += userReputation; } } // Public view functions - function getPollRepInfo(uint256 _pollId) public view returns (RepInfo memory repInfo) { - repInfo = repInfos[_pollId]; + function getPollCount() public view returns (uint256) { + return pollCount; + } + + function getPoll(uint256 _pollId) public view returns (Poll memory poll) { + poll = polls[_pollId]; + } + + function getStake(uint256 _pollId, address _staker, bool _vote) public view returns (uint256) { + return stakers[_pollId][_staker][_vote]; + } + + function getPollState(uint256 _pollId) public view returns (PollState) { + Poll storage poll = polls[_pollId]; + if (now < poll.createdAt + VOTE_PERIOD) { + return PollState.Open; + } else if (now < poll.createdAt + VOTE_PERIOD + REVEAL_PERIOD) { + return PollState.Reveal; + } else if (!poll.executed) { + return PollState.Closed; + } else { + return PollState.Executed; + } } // Internal functions @@ -127,8 +208,8 @@ contract VotingReputation is VotingBase, PatriciaTreeProofs { { pollCount += 1; - repInfos[pollCount].rootHash = colonyNetwork.getReputationRootHash(); - repInfos[pollCount].skillId = _skillId; + polls[pollCount].rootHash = colonyNetwork.getReputationRootHash(); + polls[pollCount].skillId = _skillId; polls[pollCount].createdAt = now; polls[pollCount].skillRep = checkReputation(pollCount, address(0x0), _key, _value, _branchMask, _siblings); @@ -146,7 +227,7 @@ contract VotingReputation is VotingBase, PatriciaTreeProofs { internal view returns (uint256) { bytes32 impliedRoot = getImpliedRootHashKey(_key, _value, _branchMask, _siblings); - require(repInfos[_pollId].rootHash == impliedRoot, "voting-rep-invalid-root-hash"); + require(polls[_pollId].rootHash == impliedRoot, "voting-rep-invalid-root-hash"); uint256 reputationValue; address keyColonyAddress; @@ -161,7 +242,7 @@ contract VotingReputation is VotingBase, PatriciaTreeProofs { } require(keyColonyAddress == address(colony), "voting-rep-invalid-colony-address"); - require(keySkill == repInfos[_pollId].skillId, "voting-rep-invalid-skill-id"); + require(keySkill == polls[_pollId].skillId, "voting-rep-invalid-skill-id"); require(keyUserAddress == _who, "voting-rep-invalid-user-address"); return reputationValue; @@ -185,4 +266,22 @@ contract VotingReputation is VotingBase, PatriciaTreeProofs { } } + 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) + } + } + + function getVoteSecret(bytes32 _salt, bool _vote) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(_salt, _vote)); + } + + function bool2vote(bool _vote) internal pure returns (uint256) { + return _vote ? 1 : 0; + } } diff --git a/scripts/check-storage.js b/scripts/check-storage.js index ce02762b2b..0d2a44b653 100644 --- a/scripts/check-storage.js +++ b/scripts/check-storage.js @@ -30,6 +30,8 @@ walkSync("./contracts/").forEach((contractName) => { "contracts/extensions/FundingQueueFactory.sol", "contracts/extensions/OneTxPayment.sol", "contracts/extensions/OneTxPaymentFactory.sol", + "contracts/extensions/VotingReputation.sol", + "contracts/extensions/VotingReputationFactory.sol", "contracts/gnosis/MultiSigWallet.sol", // Not directly used by any colony contracts "contracts/patriciaTree/PatriciaTreeBase.sol", // Only used by mining clients "contracts/reputationMiningCycle/ReputationMiningCycleStorage.sol", diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index 7a6b3c51e2..6fdc3b38fd 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -124,8 +124,8 @@ contract("Voting Reputation", accounts => { await voting.createRootPoll(action, key, value, mask, siblings); const pollId = await voting.getPollCount(); - const repInfo = await voting.getPollRepInfo(pollId); - expect(repInfo.skillId).to.eq.BN(domain1.skillId); + const poll = await voting.getPoll(pollId); + expect(poll.skillId).to.eq.BN(domain1.skillId); }); it("can create a domain poll in the root domain", async () => { @@ -138,8 +138,8 @@ contract("Voting Reputation", accounts => { await voting.createDomainPoll(1, UINT256_MAX, action, key, value, mask, siblings); const pollId = await voting.getPollCount(); - const repInfo = await voting.getPollRepInfo(pollId); - expect(repInfo.skillId).to.eq.BN(domain1.skillId); + const poll = await voting.getPoll(pollId); + expect(poll.skillId).to.eq.BN(domain1.skillId); }); it("can create a domain poll in a child domain", async () => { @@ -152,8 +152,8 @@ contract("Voting Reputation", accounts => { await voting.createDomainPoll(2, UINT256_MAX, action, key, value, mask, siblings); const pollId = await voting.getPollCount(); - const repInfo = await voting.getPollRepInfo(pollId); - expect(repInfo.skillId).to.eq.BN(domain2.skillId); + const poll = await voting.getPoll(pollId); + expect(poll.skillId).to.eq.BN(domain2.skillId); }); it("can escalate a domain poll", async () => { @@ -166,8 +166,8 @@ contract("Voting Reputation", accounts => { await voting.createDomainPoll(1, 0, action, key, value, mask, siblings); const pollId = await voting.getPollCount(); - const repInfo = await voting.getPollRepInfo(pollId); - expect(repInfo.skillId).to.eq.BN(domain1.skillId); + const poll = await voting.getPoll(pollId); + expect(poll.skillId).to.eq.BN(domain1.skillId); }); it("cannot escalate a domain poll with an invalid domain proof", async () => { @@ -179,6 +179,67 @@ contract("Voting Reputation", accounts => { const action = await encodeTxData(colony, "makeTask", [1, 0, FAKE, 2, 0, 0]); await checkErrorRevert(voting.createDomainPoll(1, 1, action, key, value, mask, siblings), "voting-rep-invalid-domain-id"); }); + + it("can stake on a poll", async () => { + const key = makeReputationKey(colony.address, domain1.skillId); + const value = makeReputationValue(WAD2.add(WAD), 1); + const [mask, siblings] = await reputationTree.getProof(key); + + const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); + await voting.createRootPoll(action, key, value, mask, siblings); + + const pollId = await voting.getPollCount(); + await voting.stakePoll(pollId, 1, true, 100, { from: USER0 }); + await voting.stakePoll(pollId, 1, true, 100, { from: USER1 }); + + const poll = await voting.getPoll(pollId); + expect(poll.stakes[0]).to.be.zero; + expect(poll.stakes[1]).to.eq.BN(200); + + const stake0 = await voting.getStake(pollId, USER0, true); + const stake1 = await voting.getStake(pollId, USER1, true); + expect(stake0).to.eq.BN(100); + expect(stake1).to.eq.BN(100); + }); + + it("cannot stake on both sides of a poll", async () => { + const key = makeReputationKey(colony.address, domain1.skillId); + const value = makeReputationValue(WAD2.add(WAD), 1); + const [mask, siblings] = await reputationTree.getProof(key); + + const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); + await voting.createRootPoll(action, key, value, mask, siblings); + + const pollId = await voting.getPollCount(); + await voting.stakePoll(pollId, 1, true, 100, { from: USER0 }); + + await checkErrorRevert(voting.stakePoll(pollId, 1, false, 100, { from: USER0 }), "voting-rep-cannot-stake-both-sides"); + }); + + it("cannot stake more than the required stake", async () => { + const key = makeReputationKey(colony.address, domain1.skillId); + const value = makeReputationValue(WAD2.add(WAD), 1); + const [mask, siblings] = await reputationTree.getProof(key); + + const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); + await voting.createRootPoll(action, key, value, mask, siblings); + + const totalStake = WAD.muln(3).divn(1000); + const pollId = await voting.getPollCount(); + await checkErrorRevert(voting.stakePoll(pollId, 1, true, totalStake.addn(1), { from: USER0 }), "voting-rep-stake-too-large"); + }); + + it("cannot stake with an invalid domainId", async () => { + const key = makeReputationKey(colony.address, domain1.skillId); + const value = makeReputationValue(WAD2.add(WAD), 1); + const [mask, siblings] = await reputationTree.getProof(key); + + const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); + await voting.createRootPoll(action, key, value, mask, siblings); + + const pollId = await voting.getPollCount(); + await checkErrorRevert(voting.stakePoll(pollId, 2, true, 100, { from: USER0 }), "voting-rep-bad-stake-domain"); + }); }); describe("voting on polls", async () => { @@ -217,7 +278,7 @@ contract("Voting Reputation", accounts => { await voting.revealVote(pollId, SALT, true, key, value, mask, siblings, { from: USER1 }); // See final counts - const { votes } = await voting.getPollInfo(pollId); + const { votes } = await voting.getPoll(pollId); expect(votes[0]).to.eq.BN(WAD); expect(votes[1]).to.eq.BN(WAD2); }); @@ -244,7 +305,7 @@ contract("Voting Reputation", accounts => { await voting.revealVote(pollId, SALT, false, key, value, mask, siblings, { from: USER0 }); // Vote didn't count - const { votes } = await voting.getPollInfo(pollId); + const { votes } = await voting.getPoll(pollId); expect(votes[0]).to.be.zero; expect(votes[1]).to.be.zero; }); From 8b8da0044ab028fef7dfe85cb2fd47f8a5875018 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Sun, 26 Jan 2020 14:55:45 +0200 Subject: [PATCH 07/61] Set voting window to 2 days, clean up tests --- contracts/extensions/VotingReputation.sol | 2 +- helpers/test-helper.js | 1 + test/extensions/voting-rep.js | 57 ++++++++++++----------- 3 files changed, 31 insertions(+), 29 deletions(-) diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index 7adf84b088..883993b038 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -29,7 +29,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { // Constants uint256 constant UINT256_MAX = 2**256 - 1; uint256 constant STAKE_INVERSE = 1000; - uint256 constant VOTE_PERIOD = 1 days; + uint256 constant VOTE_PERIOD = 2 days; uint256 constant REVEAL_PERIOD = 2 days; // Initialization data diff --git a/helpers/test-helper.js b/helpers/test-helper.js index d2e8d7a36f..5ff97f40b4 100644 --- a/helpers/test-helper.js +++ b/helpers/test-helper.js @@ -827,6 +827,7 @@ export async function getWaitForNSubmissionsPromise(repCycleEthers, rootHash, nL reject(new Error("Timeout while waiting for 12 hash submissions")); }, 60 * 1000); }); +} export async function encodeTxData(colony, functionName, args) { const convertedArgs = []; diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index 6fdc3b38fd..7b20ef81bf 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -36,13 +36,15 @@ contract("Voting Reputation", accounts => { let reputationTree; + const VOTE_WINDOW = SECONDS_PER_DAY * 2; + const REVEAL_WINDOW = SECONDS_PER_DAY * 2; + const USER0 = accounts[0]; const USER1 = accounts[1]; const MINER = accounts[5]; const SALT = soliditySha3(shortid.generate()); const FAKE = soliditySha3(shortid.generate()); - const WAD2 = WAD.muln(2); before(async () => { colonyNetwork = await setupColonyNetwork(); @@ -70,7 +72,7 @@ contract("Voting Reputation", accounts => { reputationTree = new PatriciaTree(); await reputationTree.insert( makeReputationKey(colony.address, domain1.skillId), // Colony total - makeReputationValue(WAD2.add(WAD), 1) + makeReputationValue(WAD.muln(3), 1) ); await reputationTree.insert( makeReputationKey(colony.address, domain1.skillId, USER0), // All good @@ -86,7 +88,7 @@ contract("Voting Reputation", accounts => { ); await reputationTree.insert( makeReputationKey(colony.address, domain1.skillId, USER1), // Wrong user (and 2x value) - makeReputationValue(WAD2, 5) + makeReputationValue(WAD.muln(2), 5) ); await reputationTree.insert( makeReputationKey(colony.address, domain2.skillId), // Colony total, domain 2 @@ -117,7 +119,7 @@ contract("Voting Reputation", accounts => { describe("creating polls", async () => { it("can create a root poll", async () => { const key = makeReputationKey(colony.address, domain1.skillId); - const value = makeReputationValue(WAD2.add(WAD), 1); + const value = makeReputationValue(WAD.muln(3), 1); const [mask, siblings] = await reputationTree.getProof(key); const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); @@ -182,7 +184,7 @@ contract("Voting Reputation", accounts => { it("can stake on a poll", async () => { const key = makeReputationKey(colony.address, domain1.skillId); - const value = makeReputationValue(WAD2.add(WAD), 1); + const value = makeReputationValue(WAD.muln(3), 1); const [mask, siblings] = await reputationTree.getProof(key); const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); @@ -204,7 +206,7 @@ contract("Voting Reputation", accounts => { it("cannot stake on both sides of a poll", async () => { const key = makeReputationKey(colony.address, domain1.skillId); - const value = makeReputationValue(WAD2.add(WAD), 1); + const value = makeReputationValue(WAD.muln(3), 1); const [mask, siblings] = await reputationTree.getProof(key); const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); @@ -218,7 +220,7 @@ contract("Voting Reputation", accounts => { it("cannot stake more than the required stake", async () => { const key = makeReputationKey(colony.address, domain1.skillId); - const value = makeReputationValue(WAD2.add(WAD), 1); + const value = makeReputationValue(WAD.muln(3), 1); const [mask, siblings] = await reputationTree.getProof(key); const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); @@ -231,7 +233,7 @@ contract("Voting Reputation", accounts => { it("cannot stake with an invalid domainId", async () => { const key = makeReputationKey(colony.address, domain1.skillId); - const value = makeReputationValue(WAD2.add(WAD), 1); + const value = makeReputationValue(WAD.muln(3), 1); const [mask, siblings] = await reputationTree.getProof(key); const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); @@ -247,7 +249,7 @@ contract("Voting Reputation", accounts => { beforeEach(async () => { key = makeReputationKey(colony.address, domain1.skillId); - value = makeReputationValue(WAD2.add(WAD), 1); + value = makeReputationValue(WAD.muln(3), 1); [mask, siblings] = await reputationTree.getProof(key); const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); @@ -261,7 +263,7 @@ contract("Voting Reputation", accounts => { it("can rate and reveal for a poll", async () => { await voting.submitVote(pollId, soliditySha3(SALT, false), { from: USER0 }); - await forwardTime(SECONDS_PER_DAY, this); + await forwardTime(VOTE_WINDOW, this); await voting.revealVote(pollId, SALT, false, key, value, mask, siblings, { from: USER0 }); }); @@ -269,25 +271,25 @@ contract("Voting Reputation", accounts => { await voting.submitVote(pollId, soliditySha3(SALT, false), { from: USER0 }); await voting.submitVote(pollId, soliditySha3(SALT, true), { from: USER1 }); - await forwardTime(SECONDS_PER_DAY, this); + await forwardTime(VOTE_WINDOW, this); await voting.revealVote(pollId, SALT, false, key, value, mask, siblings, { from: USER0 }); key = makeReputationKey(colony.address, domain1.skillId, USER1); - value = makeReputationValue(WAD2, 5); + value = makeReputationValue(WAD.muln(2), 5); [mask, siblings] = await reputationTree.getProof(key); await voting.revealVote(pollId, SALT, true, key, value, mask, siblings, { from: USER1 }); // See final counts const { votes } = await voting.getPoll(pollId); expect(votes[0]).to.eq.BN(WAD); - expect(votes[1]).to.eq.BN(WAD2); + expect(votes[1]).to.eq.BN(WAD.muln(2)); }); it("can update votes, but only last one counts", async () => { await voting.submitVote(pollId, soliditySha3(SALT, false), { from: USER0 }); await voting.submitVote(pollId, soliditySha3(SALT, true), { from: USER0 }); - await forwardTime(SECONDS_PER_DAY, this); + await forwardTime(VOTE_WINDOW, this); // Revealing first vote fails await checkErrorRevert(voting.revealVote(pollId, SALT, false, key, value, mask, siblings, { from: USER0 }), "voting-rep-secret-no-match"); @@ -299,8 +301,7 @@ contract("Voting Reputation", accounts => { it("can reveal votes after poll closes, but doesn't count", async () => { await voting.submitVote(pollId, soliditySha3(SALT, false), { from: USER0 }); - // Close the poll (1 day voting, 2 day reveal) - await forwardTime(SECONDS_PER_DAY * 3, this); + await forwardTime(VOTE_WINDOW + REVEAL_WINDOW, this); await voting.revealVote(pollId, SALT, false, key, value, mask, siblings, { from: USER0 }); @@ -313,7 +314,7 @@ contract("Voting Reputation", accounts => { it("cannot reveal a vote twice, and so cannot vote twice", async () => { await voting.submitVote(pollId, soliditySha3(SALT, false), { from: USER0 }); - await forwardTime(SECONDS_PER_DAY, this); + await forwardTime(VOTE_WINDOW, this); await voting.revealVote(pollId, SALT, false, key, value, mask, siblings, { from: USER0 }); await checkErrorRevert(voting.revealVote(pollId, SALT, false, key, value, mask, siblings, { from: USER0 }), "voting-rep-secret-no-match"); @@ -335,7 +336,7 @@ contract("Voting Reputation", accounts => { // Create new poll with new reputation state const keyColony = makeReputationKey(colony.address, domain1.skillId); - const valueColony = makeReputationValue(WAD2.add(WAD), 1); + const valueColony = makeReputationValue(WAD.muln(3), 1); const [maskColony, siblingsColony] = await reputationTree.getProof(keyColony); await voting.createRootPoll(FAKE, keyColony, valueColony, maskColony, siblingsColony); @@ -343,7 +344,7 @@ contract("Voting Reputation", accounts => { await voting.submitVote(pollId2, soliditySha3(SALT, false), { from: USER0 }); - await forwardTime(SECONDS_PER_DAY, this); + await forwardTime(VOTE_WINDOW, this); const [mask2, siblings2] = await reputationTree.getProof(key); await voting.revealVote(pollId, SALT, false, key, value, mask, siblings, { from: USER0 }); @@ -355,12 +356,12 @@ contract("Voting Reputation", accounts => { await voting.submitVote(pollId, soliditySha3(SALT, true), { from: USER0 }); - await forwardTime(SECONDS_PER_DAY, this); + await forwardTime(VOTE_WINDOW, this); await voting.revealVote(pollId, SALT, true, key, value, mask, siblings, { from: USER0 }); await checkErrorRevert(voting.executePoll(pollId), "voting-base-poll-not-closed"); - await forwardTime(SECONDS_PER_DAY * 2, this); + await forwardTime(REVEAL_WINDOW * 2, this); const taskCountPrev = await colony.getTaskCount(); await voting.executePoll(pollId); const taskCountPost = await colony.getTaskCount(); @@ -374,10 +375,10 @@ contract("Voting Reputation", accounts => { await voting.submitVote(pollId, soliditySha3(SALT, false), { from: USER0 }); - await forwardTime(SECONDS_PER_DAY, this); + await forwardTime(VOTE_WINDOW, this); await voting.revealVote(pollId, SALT, false, key, value, mask, siblings, { from: USER0 }); - await forwardTime(SECONDS_PER_DAY * 2, this); + await forwardTime(REVEAL_WINDOW * 2, this); const taskCountPrev = await colony.getTaskCount(); await voting.executePoll(pollId); const taskCountPost = await colony.getTaskCount(); @@ -390,7 +391,7 @@ contract("Voting Reputation", accounts => { beforeEach(async () => { const key = makeReputationKey(colony.address, domain1.skillId); - const value = makeReputationValue(WAD2.add(WAD), 1); + const value = makeReputationValue(WAD.muln(3), 1); const [mask, siblings] = await reputationTree.getProof(key); await voting.createRootPoll(FAKE, key, value, mask, siblings); @@ -398,7 +399,7 @@ contract("Voting Reputation", accounts => { }); it("cannot submit a vote if voting is closed", async () => { - await forwardTime(SECONDS_PER_DAY * 2, this); + await forwardTime(VOTE_WINDOW, this); await checkErrorRevert(voting.submitVote(pollId, soliditySha3(SALT, false)), "voting-rep-poll-not-open"); }); @@ -409,14 +410,14 @@ contract("Voting Reputation", accounts => { it("cannot reveal a vote with a bad secret", async () => { await voting.submitVote(pollId, soliditySha3(SALT, false)); - await forwardTime(SECONDS_PER_DAY, this); + await forwardTime(VOTE_WINDOW, this); await checkErrorRevert(voting.revealVote(pollId, SALT, true, FAKE, FAKE, 0, []), "voting-rep-secret-no-match"); }); // VotingReputation specific it("cannot reveal a vote with a bad proof", async () => { await voting.submitVote(pollId, soliditySha3(SALT, false), { from: USER0 }); - await forwardTime(SECONDS_PER_DAY, this); + await forwardTime(VOTE_WINDOW, this); // Invalid proof (wrong root hash) await checkErrorRevert(voting.revealVote(pollId, SALT, false, FAKE, FAKE, 0, [], { from: USER0 }), "voting-rep-invalid-root-hash"); @@ -439,7 +440,7 @@ contract("Voting Reputation", accounts => { // Invalid user address key = makeReputationKey(colony.address, domain1.skillId, USER1); - value = makeReputationValue(WAD2, 5); + value = makeReputationValue(WAD.muln(2), 5); [mask, siblings] = await reputationTree.getProof(key); await checkErrorRevert(voting.revealVote(pollId, SALT, false, key, value, mask, siblings, { from: USER0 }), "voting-rep-invalid-user-address"); }); From d422dc227c9b1b26bd1b09460e854a2ef1b2dd4c Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Tue, 28 Jan 2020 13:50:22 +0200 Subject: [PATCH 08/61] Add support for tracking prior votes on variables --- contracts/extensions/VotingReputation.sol | 25 ++++++++++++++++ test/extensions/voting-rep.js | 36 +++++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index 883993b038..3cef510fff 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -31,6 +31,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { uint256 constant STAKE_INVERSE = 1000; uint256 constant VOTE_PERIOD = 2 days; uint256 constant REVEAL_PERIOD = 2 days; + bytes4 constant CHANGE_FUNC = bytes4(keccak256("setExpenditureState(uint256,uint256,uint256,uint256,bool[],bytes32[],bytes32)")); // Initialization data IColony colony; @@ -63,6 +64,8 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { // The UserVote type here is just the bytes32 voteSecret mapping (address => mapping (uint256 => bytes32)) userVotes; + mapping (bytes32 => uint256) pastVotes; + // Public functions (interface) function createRootPoll( @@ -128,6 +131,15 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { Poll storage poll = polls[_pollId]; poll.executed = true; + if (getSig(poll.action) == CHANGE_FUNC) { + bytes32 slot = encodeSlot(poll.action); + uint256 votePower = add(poll.votes[0], poll.votes[1]); + + require(pastVotes[slot] < votePower, "voting-rep-insufficient-vote-power"); + + pastVotes[slot] = votePower; + } + if (poll.votes[0] < poll.votes[1]) { return executeCall(address(colony), poll.action); } @@ -284,4 +296,17 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { function bool2vote(bool _vote) internal pure returns (uint256) { return _vote ? 1 : 0; } + + function getSig(bytes memory action) internal returns (bytes4 sig) { + assembly { + sig := mload(add(action, 0x20)) + } + } + + function encodeSlot(bytes memory action) internal returns (bytes32 slot) { + assembly { + // Hash all but last (value) byte, since mload(action) gives length+32 + slot := keccak256(action, mload(action)) + } + } } diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index 7b20ef81bf..71447efbb0 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -3,6 +3,7 @@ import chai from "chai"; import bnChai from "bn-chai"; import shortid from "shortid"; +import { ethers } from "ethers"; import { soliditySha3 } from "web3-utils"; import { UINT256_MAX, WAD, MINING_CYCLE_DURATION, SECONDS_PER_DAY, DEFAULT_STAKE } from "../../helpers/constants"; @@ -384,6 +385,41 @@ contract("Voting Reputation", accounts => { const taskCountPost = await colony.getTaskCount(); expect(taskCountPost).to.eq.BN(taskCountPrev); }); + + it("cannot take an action if there is insufficient voting power (state change actions)", async () => { + await colony.setArbitrationRole(1, 0, voting.address, 1, true); + + key = makeReputationKey(colony.address, domain1.skillId); + value = makeReputationValue(WAD.muln(3), 1); + [mask, siblings] = await reputationTree.getProof(key); + + // Set first slot of first expenditure struct to 0x0 + const action = await encodeTxData(colony, "setExpenditureState", [1, UINT256_MAX, 1, 0, [], [], ethers.constants.HashZero]); + + // Create two polls for same variable + await voting.createDomainPoll(1, UINT256_MAX, action, key, value, mask, siblings); + const pollId1 = await voting.getPollCount(); + await voting.createDomainPoll(1, UINT256_MAX, action, key, value, mask, siblings); + const pollId2 = await voting.getPollCount(); + + key = makeReputationKey(colony.address, domain1.skillId, USER0); + value = makeReputationValue(WAD, 2); + [mask, siblings] = await reputationTree.getProof(key); + + await voting.submitVote(pollId1, soliditySha3(SALT, true), { from: USER0 }); + await voting.submitVote(pollId2, soliditySha3(SALT, true), { from: USER0 }); + + await forwardTime(VOTE_WINDOW, this); + + await voting.revealVote(pollId1, SALT, true, key, value, mask, siblings, { from: USER0 }); + await voting.revealVote(pollId2, SALT, true, key, value, mask, siblings, { from: USER0 }); + + await forwardTime(REVEAL_WINDOW, this); + + await voting.executePoll(pollId1); + + await checkErrorRevert(voting.executePoll(pollId2), "voting-rep-insufficient-vote-power"); + }); }); describe("simple exceptions", async () => { From ef4f3d84c2648f2d1cf0af35879ac4fdd62a075c Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Tue, 10 Mar 2020 15:39:01 -0700 Subject: [PATCH 09/61] Rename userVotes to voteSecrets --- contracts/extensions/VotingReputation.sol | 9 ++++----- test/extensions/voting-rep.js | 1 + 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index 3cef510fff..d379992140 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -61,8 +61,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { mapping (uint256 => Poll) polls; mapping (uint256 => mapping (address => mapping (bool => uint256))) stakers; - // The UserVote type here is just the bytes32 voteSecret - mapping (address => mapping (uint256 => bytes32)) userVotes; + mapping (address => mapping (uint256 => bytes32)) voteSecrets; mapping (bytes32 => uint256) pastVotes; @@ -147,7 +146,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { function submitVote(uint256 _pollId, bytes32 _voteSecret) public { require(getPollState(_pollId) == PollState.Open, "voting-rep-poll-not-open"); - userVotes[msg.sender][_pollId] = _voteSecret; + voteSecrets[msg.sender][_pollId] = _voteSecret; } function revealVote( @@ -163,14 +162,14 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { { require(getPollState(_pollId) != PollState.Open, "voting-rep-poll-still-open"); - bytes32 voteSecret = userVotes[msg.sender][_pollId]; + bytes32 voteSecret = voteSecrets[msg.sender][_pollId]; require(voteSecret == getVoteSecret(_salt, _vote), "voting-rep-secret-no-match"); // Validate proof and get reputation value uint256 userReputation = checkReputation(_pollId, msg.sender, _key, _value, _branchMask, _siblings); // Remove the secret - delete userVotes[msg.sender][_pollId]; + delete voteSecrets[msg.sender][_pollId]; // Increment the vote if poll in reveal, otherwise skip // NOTE: since there's no locking, we could just `require` PollState.Reveal diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index 71447efbb0..6cf33cb1af 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -60,6 +60,7 @@ contract("Voting Reputation", accounts => { beforeEach(async () => { ({ colony } = await setupRandomColony(colonyNetwork)); + // 1 => { 2, 3 } await colony.addDomain(1, 0, 1); await colony.addDomain(1, 0, 1); domain1 = await colony.getDomain(1); From c61979f2f948807e9a62ba5c266d10b0857e4958 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Sun, 15 Mar 2020 09:00:27 -0700 Subject: [PATCH 10/61] Add staking flow! --- contracts/extensions/VotingReputation.sol | 157 ++++++-- test/extensions/voting-rep.js | 464 +++++++++++++++------- 2 files changed, 437 insertions(+), 184 deletions(-) diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index d379992140..8585a08f78 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -20,6 +20,7 @@ pragma experimental ABIEncoderV2; import "../IColony.sol"; import "../IColonyNetwork.sol"; +import "../ITokenLocking.sol"; import "../PatriciaTree/PatriciaTreeProofs.sol"; import "../../lib/dappsys/math.sol"; @@ -27,33 +28,44 @@ import "../../lib/dappsys/math.sol"; contract VotingReputation is DSMath, PatriciaTreeProofs { // Constants + uint256 constant NAY = 0; + uint256 constant YAY = 1; uint256 constant UINT256_MAX = 2**256 - 1; - uint256 constant STAKE_INVERSE = 1000; + uint256 constant STAKE_PCT = WAD / 1000; // 0.1% + uint256 constant VOTER_REWARD_PCT = WAD / 10; // 10% + uint256 constant STAKER_REWARD_PCT = WAD - VOTER_REWARD_PCT; // 90% + uint256 constant STAKE_PERIOD = 3 days; uint256 constant VOTE_PERIOD = 2 days; uint256 constant REVEAL_PERIOD = 2 days; - bytes4 constant CHANGE_FUNC = bytes4(keccak256("setExpenditureState(uint256,uint256,uint256,uint256,bool[],bytes32[],bytes32)")); + bytes4 constant CHANGE_FUNC = bytes4( + keccak256("setExpenditureState(uint256,uint256,uint256,uint256,bool[],bytes32[],bytes32)") + ); // Initialization data IColony colony; IColonyNetwork colonyNetwork; + ITokenLocking tokenLocking; + address token; constructor(address _colony) public { colony = IColony(_colony); colonyNetwork = IColonyNetwork(colony.getColonyNetwork()); + tokenLocking = ITokenLocking(colonyNetwork.getTokenLocking()); + token = colony.getToken(); } // Data structures - enum PollState { Open, Reveal, Closed, Executed } + enum PollState { StakeYay, StakeNay, Open, Reveal, Closed, Executed, Failed } struct Poll { - bool executed; - uint256 createdAt; - bytes32 rootHash; + uint256 lastEvent; // Set at creation and when fully staked yay and nay uint256 skillId; + bytes32 rootHash; uint256 skillRep; uint256[2] stakes; // [nay, yay] uint256[2] votes; // [nay, yay] bytes action; + bool executed; } // Storage @@ -102,30 +114,53 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { createPoll(_action, domainSkillId, _key, _value, _branchMask, _siblings); } - function stakePoll(uint256 _pollId, uint256 _domainId, bool _vote, uint256 _amount) public { - Poll storage poll = polls[_pollId]; + function stakePoll( + uint256 _pollId, + uint256 _permissionDomainId, + uint256 _childSkillIndex, + uint256 _domainId, + bool _vote, + uint256 _amount, + bytes memory _key, + bytes memory _value, + uint256 _branchMask, + bytes32[] memory _siblings + ) + public + { + PollState pollState = getPollState(_pollId); + uint256 stakerRep = checkReputation(_pollId, msg.sender, _key, _value, _branchMask, _siblings); // TODO: can we keep the domainId on the poll somewhere? This seems like a wasteful external call. // But if it's 10 external calls per word of storage, then < 10 stakers makes this cheaper. - require(colony.getDomain(_domainId).skillId == poll.skillId, "voting-rep-bad-stake-domain"); - require(stakers[_pollId][msg.sender][!_vote] == 0, "voting-rep-cannot-stake-both-sides"); + require(colony.getDomain(_domainId).skillId == polls[_pollId].skillId, "voting-rep-bad-stake-domain"); + require(add(stakers[_pollId][msg.sender][_vote], _amount) <= stakerRep, "voting-rep-insufficient-rep"); - // TODO: come up with something better than `bool2vote`. Maybe an enum? - uint256 currentStake = poll.stakes[bool2vote(_vote)]; - uint256 requiredStake = poll.skillRep / STAKE_INVERSE; + require(add(polls[_pollId].stakes[toInt(_vote)], _amount) <= getRequiredStake(_pollId), "voting-rep-stake-too-large"); + require(pollState == PollState.StakeYay || pollState == PollState.StakeNay, "voting-rep-staking-closed"); + require(_vote || pollState == PollState.StakeNay, "voting-rep-out-of-order"); - require(add(currentStake, _amount) <= requiredStake, "voting-rep-stake-too-large"); + colony.obligateStake(msg.sender, _domainId, _amount); + colony.slashStake(_permissionDomainId, _childSkillIndex, address(this), msg.sender, _domainId, _amount, address(this)); - poll.stakes[bool2vote(_vote)] = add(poll.stakes[bool2vote(_vote)], _amount); + polls[_pollId].stakes[toInt(_vote)] = add(polls[_pollId].stakes[toInt(_vote)], _amount); stakers[_pollId][msg.sender][_vote] = add(stakers[_pollId][msg.sender][_vote], _amount); - // TODO: add implementation! - // colony.obligateStake(msg.sender, _domainId, _amount); + // Update timestamp if fully staked + if (polls[_pollId].stakes[YAY] == getRequiredStake(_pollId) || polls[_pollId].stakes[NAY] == getRequiredStake(_pollId)) { + polls[_pollId].lastEvent = now; + } + + // If all stakes are in, claim the pending tokens + if (polls[_pollId].stakes[NAY] == getRequiredStake(_pollId)) { + tokenLocking.claim(token, true); + } } 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"); + require(getPollState(_pollId) != PollState.Failed, "voting-rep-poll-failed"); + require(getPollState(_pollId) != PollState.Executed, "voting-rep-poll-already-executed"); + require(getPollState(_pollId) == PollState.Closed, "voting-rep-poll-not-closed"); Poll storage poll = polls[_pollId]; poll.executed = true; @@ -133,13 +168,12 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { if (getSig(poll.action) == CHANGE_FUNC) { bytes32 slot = encodeSlot(poll.action); uint256 votePower = add(poll.votes[0], poll.votes[1]); - require(pastVotes[slot] < votePower, "voting-rep-insufficient-vote-power"); pastVotes[slot] = votePower; } - if (poll.votes[0] < poll.votes[1]) { + if (poll.stakes[NAY] < poll.stakes[YAY] || poll.votes[NAY] < poll.votes[YAY]) { return executeCall(address(colony), poll.action); } } @@ -160,6 +194,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { ) public { + Poll storage poll = polls[_pollId]; require(getPollState(_pollId) != PollState.Open, "voting-rep-poll-still-open"); bytes32 voteSecret = voteSecrets[msg.sender][_pollId]; @@ -174,8 +209,31 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { // Increment the vote if poll in reveal, otherwise skip // NOTE: since there's no locking, we could just `require` PollState.Reveal if (getPollState(_pollId) == PollState.Reveal) { - polls[_pollId].votes[bool2vote(_vote)] += userReputation; + poll.votes[toInt(_vote)] += userReputation; } + + uint256 pctReputation = wdiv(userReputation, poll.skillRep); + uint256 totalStake = add(poll.stakes[YAY], poll.stakes[NAY]); + uint256 voterReward = wmul(wmul(pctReputation, totalStake), VOTER_REWARD_PCT); + tokenLocking.transfer(token, voterReward, msg.sender, true); + } + + function claimReward(uint256 _pollId, bool _vote) public { + Poll storage poll = polls[_pollId]; + require(getPollState(_pollId) == PollState.Executed, "voting-rep-not-executed"); + + // stakerReward = (voterStake * .9) * (winPercent * 2) + uint256 stakerVotes = poll.votes[toInt(_vote)]; + uint256 totalVotes = add(poll.votes[NAY], poll.votes[YAY]); + uint256 winPercent = wdiv(stakerVotes, totalVotes); + uint256 winShare = wmul(winPercent, 2 * WAD); + + uint256 voterStake = stakers[_pollId][msg.sender][_vote]; + uint256 voterRewardStake = wmul(voterStake, STAKER_REWARD_PCT); + uint256 stakerReward = wmul(voterRewardStake, winShare); + + delete stakers[_pollId][msg.sender][_vote]; + tokenLocking.transfer(token, stakerReward, msg.sender, true); } // Public view functions @@ -194,14 +252,33 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { function getPollState(uint256 _pollId) public view returns (PollState) { Poll storage poll = polls[_pollId]; - if (now < poll.createdAt + VOTE_PERIOD) { + uint256 requiredStake = getRequiredStake(_pollId); + + if (poll.executed) { + return PollState.Executed; + + } else if (poll.stakes[YAY] < requiredStake) { + if (now < poll.lastEvent + STAKE_PERIOD) { + return PollState.StakeYay; + } else { + return PollState.Failed; + } + + } else if (poll.stakes[NAY] < requiredStake) { + if (now < poll.lastEvent + STAKE_PERIOD) { + return PollState.StakeNay; + } else { + return PollState.Closed; + } + + } else if (now < poll.lastEvent + VOTE_PERIOD) { return PollState.Open; - } else if (now < poll.createdAt + VOTE_PERIOD + REVEAL_PERIOD) { + + } else if (now < poll.lastEvent + (VOTE_PERIOD + REVEAL_PERIOD)) { return PollState.Reveal; - } else if (!poll.executed) { - return PollState.Closed; + } else { - return PollState.Executed; + return PollState.Closed; } } @@ -218,15 +295,25 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { internal { pollCount += 1; - - polls[pollCount].rootHash = colonyNetwork.getReputationRootHash(); + polls[pollCount].lastEvent = now; polls[pollCount].skillId = _skillId; - - polls[pollCount].createdAt = now; + polls[pollCount].rootHash = colonyNetwork.getReputationRootHash(); polls[pollCount].skillRep = checkReputation(pollCount, address(0x0), _key, _value, _branchMask, _siblings); polls[pollCount].action = _action; } + function getVoteSecret(bytes32 _salt, bool _vote) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(_salt, _vote)); + } + + function toInt(bool _vote) internal pure returns (uint256) { + return _vote ? YAY : NAY; + } + + function getRequiredStake(uint256 _pollId) internal view returns (uint256) { + return wmul(polls[_pollId].skillRep, STAKE_PCT); + } + function checkReputation( uint256 _pollId, address _who, @@ -288,14 +375,6 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { } } - function getVoteSecret(bytes32 _salt, bool _vote) internal pure returns (bytes32) { - return keccak256(abi.encodePacked(_salt, _vote)); - } - - function bool2vote(bool _vote) internal pure returns (uint256) { - return _vote ? 1 : 0; - } - function getSig(bytes memory action) internal returns (bytes4 sig) { assembly { sig := mload(add(action, 0x20)) diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index 6cf33cb1af..0ced6d2726 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -21,44 +21,77 @@ import PatriciaTree from "../../packages/reputation-miner/patricia"; const { expect } = chai; chai.use(bnChai(web3.utils.BN)); +const TokenLocking = artifacts.require("TokenLocking"); const VotingReputation = artifacts.require("VotingReputation"); const VotingReputationFactory = artifacts.require("VotingReputationFactory"); contract("Voting Reputation", accounts => { let colony; + let token; let domain1; let domain2; let domain3; let metaColony; let colonyNetwork; + let tokenLocking; let voting; let votingFactory; let reputationTree; + let colonyKey; + let colonyValue; + let colonyMask; + let colonySiblings; + + let user0Key; + let user0Value; + let user0Mask; + let user0Siblings; + + let user1Key; + let user1Value; + let user1Mask; + let user1Siblings; + + const STAKE_WINDOW = SECONDS_PER_DAY * 3; const VOTE_WINDOW = SECONDS_PER_DAY * 2; const REVEAL_WINDOW = SECONDS_PER_DAY * 2; const USER0 = accounts[0]; const USER1 = accounts[1]; + const USER2 = accounts[2]; const MINER = accounts[5]; const SALT = soliditySha3(shortid.generate()); const FAKE = soliditySha3(shortid.generate()); + const STAKE_YAY = 0; + const STAKE_NAY = 1; + const OPEN = 2; + // const REVEAL = 3; + // const CLOSED = 4; + // const EXECUTED = 5; + + const REQUIRED_STAKE = WAD.muln(3).divn(1000); + before(async () => { colonyNetwork = await setupColonyNetwork(); ({ metaColony } = await setupMetaColonyWithLockedCLNYToken(colonyNetwork)); + await giveUserCLNYTokensAndStake(colonyNetwork, MINER, DEFAULT_STAKE); await colonyNetwork.initialiseReputationMining(); await colonyNetwork.startNextCycle(); + const tokenLockingAddress = await colonyNetwork.getTokenLocking(); + tokenLocking = await TokenLocking.at(tokenLockingAddress); + votingFactory = await VotingReputationFactory.new(); }); beforeEach(async () => { - ({ colony } = await setupRandomColony(colonyNetwork)); + ({ colony, token } = await setupRandomColony(colonyNetwork)); // 1 => { 2, 3 } await colony.addDomain(1, 0, 1); @@ -70,6 +103,24 @@ contract("Voting Reputation", accounts => { await votingFactory.deployExtension(colony.address); const votingAddress = await votingFactory.deployedExtensions(colony.address); voting = await VotingReputation.at(votingAddress); + await colony.setArbitrationRole(1, 0, voting.address, 1, true); + await colony.setAdministrationRole(1, 0, voting.address, 1, true); + + await token.mint(USER0, WAD); + await token.mint(USER1, WAD); + await token.mint(USER2, WAD); + await token.approve(tokenLocking.address, WAD, { from: USER0 }); + await token.approve(tokenLocking.address, WAD, { from: USER1 }); + await token.approve(tokenLocking.address, WAD, { from: USER2 }); + await tokenLocking.deposit(token.address, WAD, true, { from: USER0 }); + await tokenLocking.deposit(token.address, WAD, true, { from: USER1 }); + await tokenLocking.deposit(token.address, WAD, true, { from: USER2 }); + await colony.approveStake(voting.address, 1, WAD, { from: USER0 }); + await colony.approveStake(voting.address, 1, WAD, { from: USER1 }); + await colony.approveStake(voting.address, 1, WAD, { from: USER2 }); + await tokenLocking.approveStake(colony.address, WAD, { from: USER0 }); + await tokenLocking.approveStake(colony.address, WAD, { from: USER1 }); + await tokenLocking.approveStake(colony.address, WAD, { from: USER2 }); reputationTree = new PatriciaTree(); await reputationTree.insert( @@ -77,7 +128,7 @@ contract("Voting Reputation", accounts => { makeReputationValue(WAD.muln(3), 1) ); await reputationTree.insert( - makeReputationKey(colony.address, domain1.skillId, USER0), // All good + makeReputationKey(colony.address, domain1.skillId, USER0), // User0 makeReputationValue(WAD, 2) ); await reputationTree.insert( @@ -89,7 +140,7 @@ contract("Voting Reputation", accounts => { makeReputationValue(WAD, 4) ); await reputationTree.insert( - makeReputationKey(colony.address, domain1.skillId, USER1), // Wrong user (and 2x value) + makeReputationKey(colony.address, domain1.skillId, USER1), // User1 (and 2x value) makeReputationValue(WAD.muln(2), 5) ); await reputationTree.insert( @@ -100,6 +151,22 @@ contract("Voting Reputation", accounts => { makeReputationKey(colony.address, domain3.skillId), // Colony total, domain 3 makeReputationValue(WAD, 7) ); + await reputationTree.insert( + makeReputationKey(colony.address, domain1.skillId, USER2), // User2, very little rep + makeReputationValue(REQUIRED_STAKE.subn(1), 8) + ); + + colonyKey = makeReputationKey(colony.address, domain1.skillId); + colonyValue = makeReputationValue(WAD.muln(3), 1); + [colonyMask, colonySiblings] = await reputationTree.getProof(colonyKey); + + user0Key = makeReputationKey(colony.address, domain1.skillId, USER0); + user0Value = makeReputationValue(WAD, 2); + [user0Mask, user0Siblings] = await reputationTree.getProof(user0Key); + + user1Key = makeReputationKey(colony.address, domain1.skillId, USER1); + user1Value = makeReputationValue(WAD.muln(2), 5); + [user1Mask, user1Siblings] = await reputationTree.getProof(user1Key); const rootHash = await reputationTree.getRootHash(); const repCycle = await getActiveRepCycle(colonyNetwork); @@ -120,12 +187,8 @@ contract("Voting Reputation", accounts => { describe("creating polls", async () => { it("can create a root poll", async () => { - const key = makeReputationKey(colony.address, domain1.skillId); - const value = makeReputationValue(WAD.muln(3), 1); - const [mask, siblings] = await reputationTree.getProof(key); - const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); - await voting.createRootPoll(action, key, value, mask, siblings); + await voting.createRootPoll(action, colonyKey, colonyValue, colonyMask, colonySiblings); const pollId = await voting.getPollCount(); const poll = await voting.getPoll(pollId); @@ -133,13 +196,9 @@ contract("Voting Reputation", accounts => { }); it("can create a domain poll in the root domain", async () => { - const key = makeReputationKey(colony.address, domain1.skillId); - const value = makeReputationValue(WAD.muln(3), 1); - const [mask, siblings] = await reputationTree.getProof(key); - // Create poll in domain of action (1) const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); - await voting.createDomainPoll(1, UINT256_MAX, action, key, value, mask, siblings); + await voting.createDomainPoll(1, UINT256_MAX, action, colonyKey, colonyValue, colonyMask, colonySiblings); const pollId = await voting.getPollCount(); const poll = await voting.getPoll(pollId); @@ -161,13 +220,9 @@ contract("Voting Reputation", accounts => { }); it("can escalate a domain poll", async () => { - const key = makeReputationKey(colony.address, domain1.skillId); - const value = makeReputationValue(WAD.muln(3), 1); - const [mask, siblings] = await reputationTree.getProof(key); - // Create poll in parent domain (1) of action (2) const action = await encodeTxData(colony, "makeTask", [1, 0, FAKE, 2, 0, 0]); - await voting.createDomainPoll(1, 0, action, key, value, mask, siblings); + await voting.createDomainPoll(1, 0, action, colonyKey, colonyValue, colonyMask, colonySiblings); const pollId = await voting.getPollCount(); const poll = await voting.getPoll(pollId); @@ -183,18 +238,20 @@ contract("Voting Reputation", accounts => { const action = await encodeTxData(colony, "makeTask", [1, 0, FAKE, 2, 0, 0]); await checkErrorRevert(voting.createDomainPoll(1, 1, action, key, value, mask, siblings), "voting-rep-invalid-domain-id"); }); + }); - it("can stake on a poll", async () => { - const key = makeReputationKey(colony.address, domain1.skillId); - const value = makeReputationValue(WAD.muln(3), 1); - const [mask, siblings] = await reputationTree.getProof(key); + describe("staking on polls", async () => { + let pollId; + beforeEach(async () => { const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); - await voting.createRootPoll(action, key, value, mask, siblings); + await voting.createRootPoll(action, colonyKey, colonyValue, colonyMask, colonySiblings); + pollId = await voting.getPollCount(); + }); - const pollId = await voting.getPollCount(); - await voting.stakePoll(pollId, 1, true, 100, { from: USER0 }); - await voting.stakePoll(pollId, 1, true, 100, { from: USER1 }); + it("can stake on a poll", async () => { + await voting.stakePoll(pollId, 1, 0, 1, true, 100, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, 0, 1, true, 100, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); const poll = await voting.getPoll(pollId); expect(poll.stakes[0]).to.be.zero; @@ -206,67 +263,97 @@ contract("Voting Reputation", accounts => { expect(stake1).to.eq.BN(100); }); - it("cannot stake on both sides of a poll", async () => { - const key = makeReputationKey(colony.address, domain1.skillId); - const value = makeReputationValue(WAD.muln(3), 1); - const [mask, siblings] = await reputationTree.getProof(key); + it("updates the poll states correctly", async () => { + let pollState = await voting.getPollState(pollId); + expect(pollState).to.eq.BN(STAKE_YAY); - const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); - await voting.createRootPoll(action, key, value, mask, siblings); + await voting.stakePoll(pollId, 1, 0, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + pollState = await voting.getPollState(pollId); + expect(pollState).to.eq.BN(STAKE_NAY); - const pollId = await voting.getPollCount(); - await voting.stakePoll(pollId, 1, true, 100, { from: USER0 }); + await voting.stakePoll(pollId, 1, 0, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + pollState = await voting.getPollState(pollId); + expect(pollState).to.eq.BN(OPEN); + }); - await checkErrorRevert(voting.stakePoll(pollId, 1, false, 100, { from: USER0 }), "voting-rep-cannot-stake-both-sides"); + it("cannot stake with someone else's reputation", async () => { + await checkErrorRevert( + voting.stakePoll(pollId, 1, 0, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER1 }), + "voting-rep-invalid-user-address" + ); }); - it("cannot stake more than the required stake", async () => { - const key = makeReputationKey(colony.address, domain1.skillId); - const value = makeReputationValue(WAD.muln(3), 1); - const [mask, siblings] = await reputationTree.getProof(key); + it("cannot stake with insufficient reputation", async () => { + const user2Key = makeReputationKey(colony.address, domain1.skillId, USER2); + const user2Value = makeReputationValue(REQUIRED_STAKE.subn(1), 8); + const [user2Mask, user2Siblings] = await reputationTree.getProof(user2Key); - const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); - await voting.createRootPoll(action, key, value, mask, siblings); + await checkErrorRevert( + voting.stakePoll(pollId, 1, 0, 1, true, REQUIRED_STAKE, user2Key, user2Value, user2Mask, user2Siblings, { from: USER2 }), + "voting-rep-insufficient-rep" + ); + }); - const totalStake = WAD.muln(3).divn(1000); - const pollId = await voting.getPollCount(); - await checkErrorRevert(voting.stakePoll(pollId, 1, true, totalStake.addn(1), { from: USER0 }), "voting-rep-stake-too-large"); + it("cannot stake more than the required stake", async () => { + await checkErrorRevert( + voting.stakePoll(pollId, 1, 0, 1, true, REQUIRED_STAKE.addn(1), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), + "voting-rep-stake-too-large" + ); }); it("cannot stake with an invalid domainId", async () => { - const key = makeReputationKey(colony.address, domain1.skillId); - const value = makeReputationValue(WAD.muln(3), 1); - const [mask, siblings] = await reputationTree.getProof(key); + await checkErrorRevert( + voting.stakePoll(pollId, 1, 0, 2, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), + "voting-rep-bad-stake-domain" + ); + }); - const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); - await voting.createRootPoll(action, key, value, mask, siblings); + it("cannot stake out-of-order", async () => { + await checkErrorRevert( + voting.stakePoll(pollId, 1, 0, 1, false, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), + "voting-rep-out-of-order" + ); + }); - const pollId = await voting.getPollCount(); - await checkErrorRevert(voting.stakePoll(pollId, 2, true, 100, { from: USER0 }), "voting-rep-bad-stake-domain"); + it("cannot stake yay, once time runs out", async () => { + await forwardTime(STAKE_WINDOW, this); + + await checkErrorRevert( + voting.stakePoll(pollId, 1, 0, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), + "voting-rep-staking-closed" + ); + }); + + it("cannot stake nay, once time runs out", async () => { + await voting.stakePoll(pollId, 1, 0, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + + await forwardTime(STAKE_WINDOW, this); + + await checkErrorRevert( + voting.stakePoll(pollId, 1, 0, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }), + "voting-rep-staking-closed" + ); }); }); describe("voting on polls", async () => { - let key, value, mask, siblings, pollId; // eslint-disable-line one-var + let pollId; beforeEach(async () => { - key = makeReputationKey(colony.address, domain1.skillId); - value = makeReputationValue(WAD.muln(3), 1); - [mask, siblings] = await reputationTree.getProof(key); - const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); - await voting.createRootPoll(action, key, value, mask, siblings); + await voting.createRootPoll(action, colonyKey, colonyValue, colonyMask, colonySiblings); pollId = await voting.getPollCount(); - key = makeReputationKey(colony.address, domain1.skillId, USER0); - value = makeReputationValue(WAD, 2); - [mask, siblings] = await reputationTree.getProof(key); + await voting.stakePoll(pollId, 1, 0, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, 0, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); }); it("can rate and reveal for a poll", async () => { await voting.submitVote(pollId, soliditySha3(SALT, false), { from: USER0 }); + await forwardTime(VOTE_WINDOW, this); - await voting.revealVote(pollId, SALT, false, key, value, mask, siblings, { from: USER0 }); + + await voting.revealVote(pollId, SALT, false, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); }); it("can tally votes from two users", async () => { @@ -274,12 +361,9 @@ contract("Voting Reputation", accounts => { await voting.submitVote(pollId, soliditySha3(SALT, true), { from: USER1 }); await forwardTime(VOTE_WINDOW, this); - await voting.revealVote(pollId, SALT, false, key, value, mask, siblings, { from: USER0 }); - key = makeReputationKey(colony.address, domain1.skillId, USER1); - value = makeReputationValue(WAD.muln(2), 5); - [mask, siblings] = await reputationTree.getProof(key); - await voting.revealVote(pollId, SALT, true, key, value, mask, siblings, { from: USER1 }); + await voting.revealVote(pollId, SALT, false, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.revealVote(pollId, SALT, true, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); // See final counts const { votes } = await voting.getPoll(pollId); @@ -294,10 +378,13 @@ contract("Voting Reputation", accounts => { await forwardTime(VOTE_WINDOW, this); // Revealing first vote fails - await checkErrorRevert(voting.revealVote(pollId, SALT, false, key, value, mask, siblings, { from: USER0 }), "voting-rep-secret-no-match"); + await checkErrorRevert( + voting.revealVote(pollId, SALT, false, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), + "voting-rep-secret-no-match" + ); // Revealing second succeeds - await voting.revealVote(pollId, SALT, true, key, value, mask, siblings, { from: USER0 }); + await voting.revealVote(pollId, SALT, true, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); }); it("can reveal votes after poll closes, but doesn't count", async () => { @@ -305,7 +392,7 @@ contract("Voting Reputation", accounts => { await forwardTime(VOTE_WINDOW + REVEAL_WINDOW, this); - await voting.revealVote(pollId, SALT, false, key, value, mask, siblings, { from: USER0 }); + await voting.revealVote(pollId, SALT, false, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); // Vote didn't count const { votes } = await voting.getPoll(pollId); @@ -318,16 +405,24 @@ contract("Voting Reputation", accounts => { await forwardTime(VOTE_WINDOW, this); - await voting.revealVote(pollId, SALT, false, key, value, mask, siblings, { from: USER0 }); - await checkErrorRevert(voting.revealVote(pollId, SALT, false, key, value, mask, siblings, { from: USER0 }), "voting-rep-secret-no-match"); + await voting.revealVote(pollId, SALT, false, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + + await checkErrorRevert( + voting.revealVote(pollId, SALT, false, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), + "voting-rep-secret-no-match" + ); }); it("can vote in two polls with two reputation states, with different proofs", async () => { await voting.submitVote(pollId, soliditySha3(SALT, false), { from: USER0 }); // Update reputation state - const value2 = makeReputationValue(WAD.muln(3), 2); - await reputationTree.insert(key, value2); + const user0Value2 = makeReputationValue(WAD.muln(3), 2); + await reputationTree.insert(user0Key, user0Value2); + + const [colonyMask2, colonySiblings2] = await reputationTree.getProof(colonyKey); + const [user0Mask2, user0Siblings2] = await reputationTree.getProof(user0Key); + const [user1Mask2, user1Siblings2] = await reputationTree.getProof(user1Key); // Set new rootHash const rootHash = await reputationTree.getRootHash(); @@ -337,48 +432,133 @@ contract("Voting Reputation", accounts => { await repCycle.confirmNewHash(0); // Create new poll with new reputation state - const keyColony = makeReputationKey(colony.address, domain1.skillId); - const valueColony = makeReputationValue(WAD.muln(3), 1); - const [maskColony, siblingsColony] = await reputationTree.getProof(keyColony); - await voting.createRootPoll(FAKE, keyColony, valueColony, maskColony, siblingsColony); - + await voting.createRootPoll(FAKE, colonyKey, colonyValue, colonyMask2, colonySiblings2); const pollId2 = await voting.getPollCount(); - + await voting.stakePoll(pollId2, 1, 0, 1, true, REQUIRED_STAKE, user0Key, user0Value2, user0Mask2, user0Siblings2, { from: USER0 }); + await voting.stakePoll(pollId2, 1, 0, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask2, user1Siblings2, { from: USER1 }); await voting.submitVote(pollId2, soliditySha3(SALT, false), { from: USER0 }); await forwardTime(VOTE_WINDOW, this); - const [mask2, siblings2] = await reputationTree.getProof(key); - await voting.revealVote(pollId, SALT, false, key, value, mask, siblings, { from: USER0 }); - await voting.revealVote(pollId2, SALT, false, key, value2, mask2, siblings2, { from: USER0 }); + await voting.revealVote(pollId, SALT, false, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.revealVote(pollId2, SALT, false, user0Key, user0Value2, user0Mask2, user0Siblings2, { from: USER0 }); + }); + + it("cannot submit a vote if voting is closed", async () => { + await forwardTime(VOTE_WINDOW, this); + await checkErrorRevert(voting.submitVote(pollId, soliditySha3(SALT, false)), "voting-rep-poll-not-open"); + }); + + it("cannot reveal a vote if voting is open", async () => { + await voting.submitVote(pollId, soliditySha3(SALT, false)); + await checkErrorRevert(voting.revealVote(pollId, SALT, true, FAKE, FAKE, 0, []), "voting-rep-poll-still-open"); + }); + + it("cannot reveal a vote with a bad secret", async () => { + await voting.submitVote(pollId, soliditySha3(SALT, false)); + await forwardTime(VOTE_WINDOW, this); + await checkErrorRevert(voting.revealVote(pollId, SALT, true, FAKE, FAKE, 0, []), "voting-rep-secret-no-match"); + }); + + it("cannot reveal a vote with a bad proof", async () => { + await voting.submitVote(pollId, soliditySha3(SALT, false), { from: USER0 }); + await forwardTime(VOTE_WINDOW, this); + + // Invalid proof (wrong root hash) + await checkErrorRevert(voting.revealVote(pollId, SALT, false, FAKE, FAKE, 0, [], { from: USER0 }), "voting-rep-invalid-root-hash"); + + // Invalid colony address + let key, value, mask, siblings; // eslint-disable-line one-var + key = makeReputationKey(metaColony.address, domain1.skillId, USER0); + value = makeReputationValue(WAD, 3); + [mask, siblings] = await reputationTree.getProof(key); + await checkErrorRevert( + voting.revealVote(pollId, SALT, false, key, value, mask, siblings, { from: USER0 }), + "voting-rep-invalid-colony-address" + ); + + // Invalid skill id + key = makeReputationKey(colony.address, 1234, USER0); + value = makeReputationValue(WAD, 4); + [mask, siblings] = await reputationTree.getProof(key); + await checkErrorRevert(voting.revealVote(pollId, SALT, false, key, value, mask, siblings, { from: USER0 }), "voting-rep-invalid-skill-id"); + + // Invalid user address + await checkErrorRevert( + voting.revealVote(pollId, SALT, false, user1Key, user1Value, user1Mask, user1Siblings, { from: USER0 }), + "voting-rep-invalid-user-address" + ); + }); + }); + + describe("executing polls", async () => { + let pollId; + + beforeEach(async () => { + const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); + await voting.createRootPoll(action, colonyKey, colonyValue, colonyMask, colonySiblings); + pollId = await voting.getPollCount(); + }); + + it("cannot take an action if there is insufficient support", async () => { + await voting.stakePoll(pollId, 1, 0, 1, true, REQUIRED_STAKE.subn(1), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + + await forwardTime(STAKE_WINDOW, this); + + await checkErrorRevert(voting.executePoll(pollId), "voting-rep-poll-failed"); + }); + + it("can take an action if there is insufficient opposition", async () => { + await voting.stakePoll(pollId, 1, 0, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, 0, 1, false, REQUIRED_STAKE.subn(1), user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + + await forwardTime(STAKE_WINDOW, this); + + const taskCountPrev = await colony.getTaskCount(); + await voting.executePoll(pollId); + const taskCountPost = await colony.getTaskCount(); + expect(taskCountPost).to.eq.BN(taskCountPrev.addn(1)); + }); + + it("cannot take an action twice", async () => { + await voting.stakePoll(pollId, 1, 0, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + + await forwardTime(STAKE_WINDOW, 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-rep-poll-already-executed"); }); it("can take an action if the poll passes", async () => { - await colony.setAdministrationRole(1, 0, voting.address, 1, true); + await voting.stakePoll(pollId, 1, 0, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, 0, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await voting.submitVote(pollId, soliditySha3(SALT, true), { from: USER0 }); await forwardTime(VOTE_WINDOW, this); - await voting.revealVote(pollId, SALT, true, key, value, mask, siblings, { from: USER0 }); + await voting.revealVote(pollId, SALT, true, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await checkErrorRevert(voting.executePoll(pollId), "voting-base-poll-not-closed"); + await checkErrorRevert(voting.executePoll(pollId), "voting-rep-poll-not-closed"); await forwardTime(REVEAL_WINDOW * 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"); }); it("cannot take an action if the poll fails", async () => { - await colony.setAdministrationRole(1, 0, voting.address, 1, true); + await voting.stakePoll(pollId, 1, 0, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, 0, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await voting.submitVote(pollId, soliditySha3(SALT, false), { from: USER0 }); await forwardTime(VOTE_WINDOW, this); - await voting.revealVote(pollId, SALT, false, key, value, mask, siblings, { from: USER0 }); + await voting.revealVote(pollId, SALT, false, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await forwardTime(REVEAL_WINDOW * 2, this); const taskCountPrev = await colony.getTaskCount(); @@ -388,32 +568,27 @@ contract("Voting Reputation", accounts => { }); it("cannot take an action if there is insufficient voting power (state change actions)", async () => { - await colony.setArbitrationRole(1, 0, voting.address, 1, true); - - key = makeReputationKey(colony.address, domain1.skillId); - value = makeReputationValue(WAD.muln(3), 1); - [mask, siblings] = await reputationTree.getProof(key); - // Set first slot of first expenditure struct to 0x0 const action = await encodeTxData(colony, "setExpenditureState", [1, UINT256_MAX, 1, 0, [], [], ethers.constants.HashZero]); // Create two polls for same variable - await voting.createDomainPoll(1, UINT256_MAX, action, key, value, mask, siblings); + await voting.createDomainPoll(1, UINT256_MAX, action, colonyKey, colonyValue, colonyMask, colonySiblings); const pollId1 = await voting.getPollCount(); - await voting.createDomainPoll(1, UINT256_MAX, action, key, value, mask, siblings); - const pollId2 = await voting.getPollCount(); + await voting.stakePoll(pollId1, 1, 0, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId1, 1, 0, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - key = makeReputationKey(colony.address, domain1.skillId, USER0); - value = makeReputationValue(WAD, 2); - [mask, siblings] = await reputationTree.getProof(key); + await voting.createDomainPoll(1, UINT256_MAX, action, colonyKey, colonyValue, colonyMask, colonySiblings); + const pollId2 = await voting.getPollCount(); + await voting.stakePoll(pollId2, 1, 0, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId2, 1, 0, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await voting.submitVote(pollId1, soliditySha3(SALT, true), { from: USER0 }); await voting.submitVote(pollId2, soliditySha3(SALT, true), { from: USER0 }); await forwardTime(VOTE_WINDOW, this); - await voting.revealVote(pollId1, SALT, true, key, value, mask, siblings, { from: USER0 }); - await voting.revealVote(pollId2, SALT, true, key, value, mask, siblings, { from: USER0 }); + await voting.revealVote(pollId1, SALT, true, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.revealVote(pollId2, SALT, true, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await forwardTime(REVEAL_WINDOW, this); @@ -423,63 +598,62 @@ contract("Voting Reputation", accounts => { }); }); - describe("simple exceptions", async () => { + describe("claiming staker rewards", async () => { let pollId; beforeEach(async () => { - const key = makeReputationKey(colony.address, domain1.skillId); - const value = makeReputationValue(WAD.muln(3), 1); - const [mask, siblings] = await reputationTree.getProof(key); - - await voting.createRootPoll(FAKE, key, value, mask, siblings); + const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); + await voting.createRootPoll(action, colonyKey, colonyValue, colonyMask, colonySiblings); pollId = await voting.getPollCount(); }); - it("cannot submit a vote if voting is closed", async () => { + it("can let stakers claim rewards, based on the outcome", async () => { + await voting.stakePoll(pollId, 1, 0, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, 0, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + + await voting.submitVote(pollId, soliditySha3(SALT, true), { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, false), { from: USER1 }); await forwardTime(VOTE_WINDOW, this); - await checkErrorRevert(voting.submitVote(pollId, soliditySha3(SALT, false)), "voting-rep-poll-not-open"); - }); + await voting.revealVote(pollId, SALT, true, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.revealVote(pollId, SALT, false, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await forwardTime(REVEAL_WINDOW, this); + await voting.executePoll(pollId); - it("cannot reveal a vote if voting is open", async () => { - await voting.submitVote(pollId, soliditySha3(SALT, false)); - await checkErrorRevert(voting.revealVote(pollId, SALT, true, FAKE, FAKE, 0, []), "voting-rep-poll-still-open"); - }); + await tokenLocking.claim(token.address, true, { from: USER0 }); + await tokenLocking.claim(token.address, true, { from: USER1 }); + await voting.claimReward(pollId, true, { from: USER0 }); + await voting.claimReward(pollId, false, { from: USER1 }); - it("cannot reveal a vote with a bad secret", async () => { - await voting.submitVote(pollId, soliditySha3(SALT, false)); - await forwardTime(VOTE_WINDOW, this); - await checkErrorRevert(voting.revealVote(pollId, SALT, true, FAKE, FAKE, 0, []), "voting-rep-secret-no-match"); + const stakerRewards = REQUIRED_STAKE.divn(10).muln(9); + const expectedReward0 = stakerRewards.divn(3).muln(2); // (stake * .9) * (winPct = 1/3 * 2) + const expectedReward1 = stakerRewards.divn(3).muln(4); // (stake * .9) * (winPct = 2/3 * 2) + + const user0Lock = await tokenLocking.getUserLock(token.address, USER0); + const user1Lock = await tokenLocking.getUserLock(token.address, USER1); + expect(user0Lock.pendingBalance).to.eq.BN(expectedReward0); + expect(user1Lock.pendingBalance).to.eq.BN(expectedReward1); }); - // VotingReputation specific - it("cannot reveal a vote with a bad proof", async () => { - await voting.submitVote(pollId, soliditySha3(SALT, false), { from: USER0 }); + it("cannot claim rewards twice", async () => { + await voting.stakePoll(pollId, 1, 0, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, 0, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + + await voting.submitVote(pollId, soliditySha3(SALT, true), { from: USER0 }); await forwardTime(VOTE_WINDOW, this); + await voting.revealVote(pollId, SALT, true, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await forwardTime(REVEAL_WINDOW, this); + await voting.executePoll(pollId); - // Invalid proof (wrong root hash) - await checkErrorRevert(voting.revealVote(pollId, SALT, false, FAKE, FAKE, 0, [], { from: USER0 }), "voting-rep-invalid-root-hash"); + await voting.claimReward(pollId, true, { from: USER0 }); + await tokenLocking.claim(token.address, true, { from: USER0 }); + await voting.claimReward(pollId, true, { from: USER0 }); - // Invalid colony address - let key, value, mask, siblings; // eslint-disable-line one-var - key = makeReputationKey(metaColony.address, domain1.skillId, USER0); - value = makeReputationValue(WAD, 3); - [mask, siblings] = await reputationTree.getProof(key); - await checkErrorRevert( - voting.revealVote(pollId, SALT, false, key, value, mask, siblings, { from: USER0 }), - "voting-rep-invalid-colony-address" - ); - - // Invalid skill id - key = makeReputationKey(colony.address, 1234, USER0); - value = makeReputationValue(WAD, 4); - [mask, siblings] = await reputationTree.getProof(key); - await checkErrorRevert(voting.revealVote(pollId, SALT, false, key, value, mask, siblings, { from: USER0 }), "voting-rep-invalid-skill-id"); + const userLock = await tokenLocking.getUserLock(token.address, USER0); + expect(userLock.pendingBalance).to.be.zero; + }); - // Invalid user address - key = makeReputationKey(colony.address, domain1.skillId, USER1); - value = makeReputationValue(WAD.muln(2), 5); - [mask, siblings] = await reputationTree.getProof(key); - await checkErrorRevert(voting.revealVote(pollId, SALT, false, key, value, mask, siblings, { from: USER0 }), "voting-rep-invalid-user-address"); + it("cannot claim rewards before a poll is executed", async () => { + await checkErrorRevert(voting.claimReward(pollId, true, { from: USER0 }), "voting-rep-not-executed"); }); }); }); From 60cccd9f4602d66ba450cf75b0be21053f825ee4 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Wed, 25 Mar 2020 16:47:11 -0700 Subject: [PATCH 11/61] Add events and support for claiming voter comp by colony --- contracts/extensions/VotingReputation.sol | 78 +++++++++++++++++------ docs/_Interface_IColonyNetwork.md | 20 +++--- test/extensions/voting-rep.js | 39 ++++++++++++ 3 files changed, 107 insertions(+), 30 deletions(-) diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index 8585a08f78..2abe4cf53c 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -18,6 +18,7 @@ pragma solidity 0.5.8; pragma experimental ABIEncoderV2; +import "../ERC20Extended.sol"; import "../IColony.sol"; import "../IColonyNetwork.sol"; import "../ITokenLocking.sol"; @@ -27,6 +28,15 @@ import "../../lib/dappsys/math.sol"; contract VotingReputation is DSMath, PatriciaTreeProofs { + // Events + event PollCreated(uint256 indexed pollId, uint256 indexed skillId); + event PollStaked(uint256 indexed pollId, address indexed staker, bool indexed side, uint256 amount); + event PollVoteSubmitted(uint256 indexed pollId, address indexed voter); + event PollVoteRevealed(uint256 indexed pollId, address indexed voter, bool indexed side); + event PollExecuted(uint256 indexed pollId); + event PollRewardClaimed(uint256 indexed pollId, address indexed staker, bool indexed side, uint256 amount); + event PollColonyRewardClaimed(uint256 indexed pollId, uint256 amount); + // Constants uint256 constant NAY = 0; uint256 constant YAY = 1; @@ -62,6 +72,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { uint256 skillId; bytes32 rootHash; uint256 skillRep; + uint256 voterComp; uint256[2] stakes; // [nay, yay] uint256[2] votes; // [nay, yay] bytes action; @@ -155,32 +166,15 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { if (polls[_pollId].stakes[NAY] == getRequiredStake(_pollId)) { tokenLocking.claim(token, true); } - } - - function executePoll(uint256 _pollId) public returns (bool) { - require(getPollState(_pollId) != PollState.Failed, "voting-rep-poll-failed"); - require(getPollState(_pollId) != PollState.Executed, "voting-rep-poll-already-executed"); - require(getPollState(_pollId) == PollState.Closed, "voting-rep-poll-not-closed"); - Poll storage poll = polls[_pollId]; - poll.executed = true; - - if (getSig(poll.action) == CHANGE_FUNC) { - bytes32 slot = encodeSlot(poll.action); - uint256 votePower = add(poll.votes[0], poll.votes[1]); - require(pastVotes[slot] < votePower, "voting-rep-insufficient-vote-power"); - - pastVotes[slot] = votePower; - } - - if (poll.stakes[NAY] < poll.stakes[YAY] || poll.votes[NAY] < poll.votes[YAY]) { - return executeCall(address(colony), poll.action); - } + emit PollStaked(_pollId, msg.sender, _vote, _amount); } function submitVote(uint256 _pollId, bytes32 _voteSecret) public { require(getPollState(_pollId) == PollState.Open, "voting-rep-poll-not-open"); voteSecrets[msg.sender][_pollId] = _voteSecret; + + emit PollVoteSubmitted(_pollId, msg.sender); } function revealVote( @@ -215,7 +209,34 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { uint256 pctReputation = wdiv(userReputation, poll.skillRep); uint256 totalStake = add(poll.stakes[YAY], poll.stakes[NAY]); uint256 voterReward = wmul(wmul(pctReputation, totalStake), VOTER_REWARD_PCT); + + poll.voterComp = sub(poll.voterComp, voterReward); tokenLocking.transfer(token, voterReward, msg.sender, true); + + emit PollVoteRevealed(_pollId, msg.sender, _vote); + } + + function executePoll(uint256 _pollId) public returns (bool) { + require(getPollState(_pollId) != PollState.Failed, "voting-rep-poll-failed"); + require(getPollState(_pollId) != PollState.Executed, "voting-rep-poll-already-executed"); + require(getPollState(_pollId) == PollState.Closed, "voting-rep-poll-not-closed"); + + Poll storage poll = polls[_pollId]; + poll.executed = true; + + if (getSig(poll.action) == CHANGE_FUNC) { + bytes32 slot = encodeSlot(poll.action); + uint256 votePower = add(poll.votes[0], poll.votes[1]); + require(pastVotes[slot] < votePower, "voting-rep-insufficient-vote-power"); + + pastVotes[slot] = votePower; + } + + if (poll.stakes[NAY] < poll.stakes[YAY] || poll.votes[NAY] < poll.votes[YAY]) { + return executeCall(address(colony), poll.action); + } + + emit PollExecuted(_pollId); } function claimReward(uint256 _pollId, bool _vote) public { @@ -234,6 +255,20 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { delete stakers[_pollId][msg.sender][_vote]; tokenLocking.transfer(token, stakerReward, msg.sender, true); + + emit PollRewardClaimed(_pollId, msg.sender,_vote, stakerReward); + } + + function claimRewardForColony(uint256 _pollId) public { + require(getPollState(_pollId) == PollState.Executed, "voting-rep-not-executed"); + + uint256 voterComp = polls[_pollId].voterComp; + delete polls[_pollId].voterComp; + + tokenLocking.withdraw(token, voterComp, true); + require(ERC20Extended(token).transfer(address(colony), voterComp), "voting-rep-colony-transfer-failed"); + + emit PollColonyRewardClaimed(_pollId, voterComp); } // Public view functions @@ -299,7 +334,10 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { polls[pollCount].skillId = _skillId; polls[pollCount].rootHash = colonyNetwork.getReputationRootHash(); polls[pollCount].skillRep = checkReputation(pollCount, address(0x0), _key, _value, _branchMask, _siblings); + polls[pollCount].voterComp = wmul(getRequiredStake(pollCount) * 2, VOTER_REWARD_PCT); polls[pollCount].action = _action; + + emit PollCreated(pollCount, _skillId); } function getVoteSecret(bytes32 _salt, bool _vote) internal pure returns (bytes32) { diff --git a/docs/_Interface_IColonyNetwork.md b/docs/_Interface_IColonyNetwork.md index e82352ab06..8990fa3f13 100644 --- a/docs/_Interface_IColonyNetwork.md +++ b/docs/_Interface_IColonyNetwork.md @@ -116,19 +116,15 @@ Used by a user to claim any mining rewards due to them. This will place them in ### `createColony` -Overload of the simpler `createColony` -- creates a new colony in the network with a variety of options +Creates a new colony in the network, at version 3 -*Note: For the colony to mint tokens, token ownership must be transferred to the new colony* +*Note: This is now deprecated and will be removed in a future version* **Parameters** |Name|Type|Description| |---|---|---| -|_tokenAddress|address|Address of an ERC20 token to serve as the colony token -|_version|uint256|The version of colony to deploy (pass 0 for the current version) -|_colonyName|string|The label to register (if null, no label is registered) -|_orbitdb|string|The path of the orbitDB database associated with the user profile -|_useExtensionManager|bool|If true, give the ExtensionManager the root role in the colony +|_tokenAddress|address|Address of an ERC20 token to serve as the colony token. **Return Parameters** @@ -138,15 +134,19 @@ Overload of the simpler `createColony` -- creates a new colony in the network wi ### `createColony` -Creates a new colony in the network, at version 3 +Overload of the simpler `createColony` -- creates a new colony in the network with a variety of options -*Note: This is now deprecated and will be removed in a future version* +*Note: For the colony to mint tokens, token ownership must be transferred to the new colony* **Parameters** |Name|Type|Description| |---|---|---| -|_tokenAddress|address|Address of an ERC20 token to serve as the colony token. +|_tokenAddress|address|Address of an ERC20 token to serve as the colony token +|_version|uint256|The version of colony to deploy (pass 0 for the current version) +|_colonyName|string|The label to register (if null, no label is registered) +|_orbitdb|string|The path of the orbitDB database associated with the user profile +|_useExtensionManager|bool|If true, give the ExtensionManager the root role in the colony **Return Parameters** diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index 0ced6d2726..7ba5647093 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -655,5 +655,44 @@ contract("Voting Reputation", accounts => { it("cannot claim rewards before a poll is executed", async () => { await checkErrorRevert(voting.claimReward(pollId, true, { from: USER0 }), "voting-rep-not-executed"); }); + + it("can claim unpaid voter rewards for colony", async () => { + await voting.stakePoll(pollId, 1, 0, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, 0, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + + await voting.submitVote(pollId, soliditySha3(SALT, true), { from: USER0 }); + await forwardTime(VOTE_WINDOW, this); + await voting.revealVote(pollId, SALT, true, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await forwardTime(REVEAL_WINDOW, this); + await voting.executePoll(pollId); + + const colonyBalancePrev = await token.balanceOf(colony.address); + await voting.claimRewardForColony(pollId); + const colonyBalancePost = await token.balanceOf(colony.address); + + // Since USER1 didn't vote and has 2/3 of the reputation + const expectedReward = REQUIRED_STAKE.muln(2).divn(10).divn(3).muln(2); // eslint-disable-line prettier/prettier + expect(colonyBalancePost.sub(colonyBalancePrev)).to.eq.BN(expectedReward); + }); + + it("canot claim unpaid voter rewards for colony twice", async () => { + await voting.stakePoll(pollId, 1, 0, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, 0, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + + await voting.submitVote(pollId, soliditySha3(SALT, true), { from: USER0 }); + await forwardTime(VOTE_WINDOW, this); + await voting.revealVote(pollId, SALT, true, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await forwardTime(REVEAL_WINDOW, this); + await voting.executePoll(pollId); + + await voting.claimRewardForColony(pollId); + // Already claimed it + + const colonyBalancePrev = await token.balanceOf(colony.address); + await voting.claimRewardForColony(pollId); + const colonyBalancePost = await token.balanceOf(colony.address); + + expect(colonyBalancePost.sub(colonyBalancePrev)).to.be.zero; + }); }); }); From 2a610ee3f99cf648ffb84249424cdbfe3ee7681d Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Fri, 27 Mar 2020 15:30:14 -0700 Subject: [PATCH 12/61] Conform to new linting rules --- contracts/extensions/VotingReputation.sol | 2 +- helpers/test-helper.js | 2 +- test/extensions/voting-rep.js | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index 2abe4cf53c..1d905b8474 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -226,7 +226,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { if (getSig(poll.action) == CHANGE_FUNC) { bytes32 slot = encodeSlot(poll.action); - uint256 votePower = add(poll.votes[0], poll.votes[1]); + uint256 votePower = add(poll.votes[NAY], poll.votes[YAY]); require(pastVotes[slot] < votePower, "voting-rep-insufficient-vote-power"); pastVotes[slot] = votePower; diff --git a/helpers/test-helper.js b/helpers/test-helper.js index 5ff97f40b4..a463f9166b 100644 --- a/helpers/test-helper.js +++ b/helpers/test-helper.js @@ -831,7 +831,7 @@ export async function getWaitForNSubmissionsPromise(repCycleEthers, rootHash, nL export async function encodeTxData(colony, functionName, args) { const convertedArgs = []; - args.forEach(arg => { + args.forEach((arg) => { if (Number.isInteger(arg)) { const convertedArg = ethers.BigNumber.from(arg); convertedArgs.push(convertedArg); diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index 7ba5647093..6e04d7a492 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -13,7 +13,7 @@ import { setupColonyNetwork, setupMetaColonyWithLockedCLNYToken, setupRandomColony, - giveUserCLNYTokensAndStake + giveUserCLNYTokensAndStake, } from "../../helpers/test-data-generator"; import PatriciaTree from "../../packages/reputation-miner/patricia"; @@ -25,7 +25,7 @@ const TokenLocking = artifacts.require("TokenLocking"); const VotingReputation = artifacts.require("VotingReputation"); const VotingReputationFactory = artifacts.require("VotingReputationFactory"); -contract("Voting Reputation", accounts => { +contract("Voting Reputation", (accounts) => { let colony; let token; let domain1; From cd0e11abfcd34b001b465385ffd1a78c6873fb9e Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Fri, 8 May 2020 16:23:00 -0700 Subject: [PATCH 13/61] Add escalation flow --- contracts/extensions/VotingReputation.sol | 251 +++++++---- .../extensions/VotingReputationFactory.sol | 4 +- test/extensions/voting-rep.js | 418 +++++++++++++----- 3 files changed, 473 insertions(+), 200 deletions(-) diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index 1d905b8474..9c7c47eeb5 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -18,12 +18,12 @@ pragma solidity 0.5.8; pragma experimental ABIEncoderV2; -import "../ERC20Extended.sol"; -import "../IColony.sol"; -import "../IColonyNetwork.sol"; -import "../ITokenLocking.sol"; -import "../PatriciaTree/PatriciaTreeProofs.sol"; -import "../../lib/dappsys/math.sol"; +import "./../../lib/dappsys/math.sol"; +import "./../colony/IColony.sol"; +import "./../colonyNetwork/IColonyNetwork.sol"; +import "./../common/ERC20Extended.sol"; +import "./../patriciaTree/PatriciaTreeProofs.sol"; +import "./../tokenLocking/ITokenLocking.sol"; contract VotingReputation is DSMath, PatriciaTreeProofs { @@ -33,20 +33,22 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { event PollStaked(uint256 indexed pollId, address indexed staker, bool indexed side, uint256 amount); event PollVoteSubmitted(uint256 indexed pollId, address indexed voter); event PollVoteRevealed(uint256 indexed pollId, address indexed voter, bool indexed side); - event PollExecuted(uint256 indexed pollId); + event PollExecuted(uint256 indexed pollId, bytes action, bool success); event PollRewardClaimed(uint256 indexed pollId, address indexed staker, bool indexed side, uint256 amount); - event PollColonyRewardClaimed(uint256 indexed pollId, uint256 amount); // Constants + uint256 constant UINT256_MAX = 2**256 - 1; uint256 constant NAY = 0; uint256 constant YAY = 1; - uint256 constant UINT256_MAX = 2**256 - 1; - uint256 constant STAKE_PCT = WAD / 1000; // 0.1% - uint256 constant VOTER_REWARD_PCT = WAD / 10; // 10% - uint256 constant STAKER_REWARD_PCT = WAD - VOTER_REWARD_PCT; // 90% + + uint256 constant STAKE_FRACTION = WAD / 1000; // 0.1% + uint256 constant VOTER_REWARD_FRACTION = WAD / 10; // 10% + uint256 constant VOTE_POWER_FRACTION = (WAD * 2) / 3; // 66.6% + uint256 constant STAKE_PERIOD = 3 days; uint256 constant VOTE_PERIOD = 2 days; uint256 constant REVEAL_PERIOD = 2 days; + bytes4 constant CHANGE_FUNC = bytes4( keccak256("setExpenditureState(uint256,uint256,uint256,uint256,bool[],bytes32[],bytes32)") ); @@ -65,14 +67,15 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { } // Data structures - enum PollState { StakeYay, StakeNay, Open, Reveal, Closed, Executed, Failed } + enum PollState { Staking, Voting, Reveal, Closed, Executable, Executed, Failed } struct Poll { - uint256 lastEvent; // Set at creation and when fully staked yay and nay - uint256 skillId; + uint256 lastEvent; // Set at creation / escalation & when fully staked bytes32 rootHash; + uint256 skillId; uint256 skillRep; - uint256 voterComp; + uint256 unpaidRewards; + address[2] leads; // [nay, yay] uint256[2] stakes; // [nay, yay] uint256[2] votes; // [nay, yay] bytes action; @@ -127,8 +130,8 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { function stakePoll( uint256 _pollId, - uint256 _permissionDomainId, - uint256 _childSkillIndex, + uint256 _permissionDomainId, // For extension's arbitration permission + uint256 _childSkillIndex, // For extension's arbitration permission uint256 _domainId, bool _vote, uint256 _amount, @@ -139,31 +142,37 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { ) public { - PollState pollState = getPollState(_pollId); + Poll storage poll = polls[_pollId]; + uint256 stakerRep = checkReputation(_pollId, msg.sender, _key, _value, _branchMask, _siblings); // TODO: can we keep the domainId on the poll somewhere? This seems like a wasteful external call. // But if it's 10 external calls per word of storage, then < 10 stakers makes this cheaper. - require(colony.getDomain(_domainId).skillId == polls[_pollId].skillId, "voting-rep-bad-stake-domain"); + require(colony.getDomain(_domainId).skillId == poll.skillId, "voting-rep-bad-stake-domain"); require(add(stakers[_pollId][msg.sender][_vote], _amount) <= stakerRep, "voting-rep-insufficient-rep"); - require(add(polls[_pollId].stakes[toInt(_vote)], _amount) <= getRequiredStake(_pollId), "voting-rep-stake-too-large"); - require(pollState == PollState.StakeYay || pollState == PollState.StakeNay, "voting-rep-staking-closed"); - require(_vote || pollState == PollState.StakeNay, "voting-rep-out-of-order"); + require(add(poll.stakes[toInt(_vote)], _amount) <= getRequiredStake(_pollId), "voting-rep-stake-too-large"); + require(getPollState(_pollId) == PollState.Staking, "voting-rep-staking-closed"); colony.obligateStake(msg.sender, _domainId, _amount); - colony.slashStake(_permissionDomainId, _childSkillIndex, address(this), msg.sender, _domainId, _amount, address(this)); + colony.transferStake(_permissionDomainId, _childSkillIndex, address(this), msg.sender, _domainId, _amount, address(this)); - polls[_pollId].stakes[toInt(_vote)] = add(polls[_pollId].stakes[toInt(_vote)], _amount); + // Set lead if first staker + if (poll.stakes[toInt(_vote)] == 0) { + poll.leads[toInt(_vote)] = msg.sender; + } + + // Update the stake + poll.unpaidRewards = add(poll.unpaidRewards, _amount); + poll.stakes[toInt(_vote)] = add(poll.stakes[toInt(_vote)], _amount); stakers[_pollId][msg.sender][_vote] = add(stakers[_pollId][msg.sender][_vote], _amount); // Update timestamp if fully staked - if (polls[_pollId].stakes[YAY] == getRequiredStake(_pollId) || polls[_pollId].stakes[NAY] == getRequiredStake(_pollId)) { - polls[_pollId].lastEvent = now; - } - - // If all stakes are in, claim the pending tokens - if (polls[_pollId].stakes[NAY] == getRequiredStake(_pollId)) { + if ( + poll.stakes[YAY] == getRequiredStake(_pollId) && + poll.stakes[NAY] == getRequiredStake(_pollId) + ) { + poll.lastEvent = now; tokenLocking.claim(token, true); } @@ -171,7 +180,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { } function submitVote(uint256 _pollId, bytes32 _voteSecret) public { - require(getPollState(_pollId) == PollState.Open, "voting-rep-poll-not-open"); + require(getPollState(_pollId) == PollState.Voting, "voting-rep-poll-not-open"); voteSecrets[msg.sender][_pollId] = _voteSecret; emit PollVoteSubmitted(_pollId, msg.sender); @@ -189,13 +198,13 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { public { Poll storage poll = polls[_pollId]; - require(getPollState(_pollId) != PollState.Open, "voting-rep-poll-still-open"); + require(getPollState(_pollId) != PollState.Voting, "voting-rep-poll-still-open"); bytes32 voteSecret = voteSecrets[msg.sender][_pollId]; require(voteSecret == getVoteSecret(_salt, _vote), "voting-rep-secret-no-match"); // Validate proof and get reputation value - uint256 userReputation = checkReputation(_pollId, msg.sender, _key, _value, _branchMask, _siblings); + uint256 userRep = checkReputation(_pollId, msg.sender, _key, _value, _branchMask, _siblings); // Remove the secret delete voteSecrets[msg.sender][_pollId]; @@ -203,72 +212,123 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { // Increment the vote if poll in reveal, otherwise skip // NOTE: since there's no locking, we could just `require` PollState.Reveal if (getPollState(_pollId) == PollState.Reveal) { - poll.votes[toInt(_vote)] += userReputation; + poll.votes[toInt(_vote)] = add(poll.votes[toInt(_vote)], userRep); } - uint256 pctReputation = wdiv(userReputation, poll.skillRep); + uint256 pctReputation = wdiv(userRep, poll.skillRep); uint256 totalStake = add(poll.stakes[YAY], poll.stakes[NAY]); - uint256 voterReward = wmul(wmul(pctReputation, totalStake), VOTER_REWARD_PCT); + uint256 voterReward = wmul(wmul(pctReputation, totalStake), VOTER_REWARD_FRACTION); - poll.voterComp = sub(poll.voterComp, voterReward); + poll.unpaidRewards = sub(poll.unpaidRewards, voterReward); tokenLocking.transfer(token, voterReward, msg.sender, true); emit PollVoteRevealed(_pollId, msg.sender, _vote); } - function executePoll(uint256 _pollId) public returns (bool) { - require(getPollState(_pollId) != PollState.Failed, "voting-rep-poll-failed"); - require(getPollState(_pollId) != PollState.Executed, "voting-rep-poll-already-executed"); + function escalatePoll( + uint256 _pollId, + uint256 _newDomainId, + uint256 _childSkillIndex, + bytes memory _key, + bytes memory _value, + uint256 _branchMask, + bytes32[] memory _siblings + ) + public + { + Poll storage poll = polls[_pollId]; require(getPollState(_pollId) == PollState.Closed, "voting-rep-poll-not-closed"); + require(msg.sender == poll.leads[NAY] || msg.sender == poll.leads[YAY], "voting-rep-user-not-lead"); + uint256 newDomainSkillId = colony.getDomain(_newDomainId).skillId; + uint256 childSkillId = colonyNetwork.getChildSkillId(newDomainSkillId, _childSkillIndex); + require(childSkillId == poll.skillId, "voting-rep-invalid-domain-proof"); + + poll.lastEvent = now; + poll.skillId = newDomainSkillId; + poll.skillRep = checkReputation(_pollId, address(0x0), _key, _value, _branchMask, _siblings); + } + + function executePoll(uint256 _pollId) public { Poll storage poll = polls[_pollId]; + PollState pollState = getPollState(_pollId); + + require(pollState != PollState.Failed, "voting-rep-poll-failed"); + require(pollState != PollState.Closed, "voting-rep-poll-escalation-window-open"); + require(pollState != PollState.Executed, "voting-rep-poll-already-executed"); + require(pollState == PollState.Executable, "voting-rep-poll-not-executable"); + poll.executed = true; + bool canExecute = poll.stakes[NAY] <= poll.stakes[YAY] && poll.votes[NAY] <= poll.votes[YAY]; if (getSig(poll.action) == CHANGE_FUNC) { - bytes32 slot = encodeSlot(poll.action); - uint256 votePower = add(poll.votes[NAY], poll.votes[YAY]); - require(pastVotes[slot] < votePower, "voting-rep-insufficient-vote-power"); - pastVotes[slot] = votePower; + // Conditions: + // - Yay side staked and nay side did not, and doman has sufficient vote power + // - Both sides staked and yay side won, with sufficient vote power + + uint256 votePower; + if (poll.stakes[NAY] < getRequiredStake(_pollId)) { + votePower = wmul(poll.skillRep, VOTE_POWER_FRACTION); + } else { + votePower = poll.votes[YAY]; + } + + bytes32 slot = encodeSlot(poll.action); + if (pastVotes[slot] < votePower) { + pastVotes[slot] = votePower; + canExecute = canExecute && true; + } else { + canExecute = canExecute && false; + } } - if (poll.stakes[NAY] < poll.stakes[YAY] || poll.votes[NAY] < poll.votes[YAY]) { - return executeCall(address(colony), poll.action); + if (canExecute) { + executeCall(address(colony), poll.action); } - emit PollExecuted(_pollId); + emit PollExecuted(_pollId, poll.action, canExecute); } function claimReward(uint256 _pollId, bool _vote) public { Poll storage poll = polls[_pollId]; require(getPollState(_pollId) == PollState.Executed, "voting-rep-not-executed"); - // stakerReward = (voterStake * .9) * (winPercent * 2) - uint256 stakerVotes = poll.votes[toInt(_vote)]; - uint256 totalVotes = add(poll.votes[NAY], poll.votes[YAY]); - uint256 winPercent = wdiv(stakerVotes, totalVotes); - uint256 winShare = wmul(winPercent, 2 * WAD); - - uint256 voterStake = stakers[_pollId][msg.sender][_vote]; - uint256 voterRewardStake = wmul(voterStake, STAKER_REWARD_PCT); - uint256 stakerReward = wmul(voterRewardStake, winShare); + // Calculate how much of the stake is left after voter compensation (>= 90%) + uint256 stake = stakers[_pollId][msg.sender][_vote]; + uint256 totalStake = add(poll.stakes[NAY], poll.stakes[YAY]); + uint256 rewardFraction = wdiv(poll.unpaidRewards, totalStake); + uint256 rewardStake = wmul(stake, rewardFraction); + + uint256 stakerReward; + + // Went to a vote, use vote to determine reward or penalty + if (poll.stakes[NAY] == getRequiredStake(_pollId) && poll.stakes[YAY] == getRequiredStake(_pollId)) { + uint256 stakerVotes = poll.votes[toInt(_vote)]; + uint256 totalVotes = add(poll.votes[NAY], poll.votes[YAY]); + uint256 winPercent = wdiv(stakerVotes, totalVotes); + uint256 winShare = wmul(winPercent, 2 * WAD); + stakerReward = wmul(rewardStake, winShare); + + // Your side fully staked, receive 10% (proportional) of loser's stake + } else if (poll.stakes[toInt(_vote)] == getRequiredStake(_pollId)) { + uint256 stakePercent = wdiv(stake, poll.stakes[toInt(_vote)]); + uint256 totalPenalty = wmul(poll.stakes[toInt(!_vote)], WAD / 10); + stakerReward = add(rewardStake, wmul(stakePercent, totalPenalty)); + + // Opponent's side fully staked, pay 10% penalty + } else if (poll.stakes[toInt(!_vote)] == getRequiredStake(_pollId)) { + stakerReward = wmul(rewardStake, (WAD / 10) * 9); + + // Neither side fully staked, no reward or penalty + } else { + stakerReward = rewardStake; + } delete stakers[_pollId][msg.sender][_vote]; tokenLocking.transfer(token, stakerReward, msg.sender, true); - emit PollRewardClaimed(_pollId, msg.sender,_vote, stakerReward); - } - - function claimRewardForColony(uint256 _pollId) public { - require(getPollState(_pollId) == PollState.Executed, "voting-rep-not-executed"); - - uint256 voterComp = polls[_pollId].voterComp; - delete polls[_pollId].voterComp; - - tokenLocking.withdraw(token, voterComp, true); - require(ERC20Extended(token).transfer(address(colony), voterComp), "voting-rep-colony-transfer-failed"); - - emit PollColonyRewardClaimed(_pollId, voterComp); + emit PollRewardClaimed(_pollId, msg.sender, _vote, stakerReward); } // Public view functions @@ -285,35 +345,43 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { return stakers[_pollId][_staker][_vote]; } + function getPastVotes(bytes32 _slot) public view returns (uint256) { + return pastVotes[_slot]; + } + function getPollState(uint256 _pollId) public view returns (PollState) { Poll storage poll = polls[_pollId]; uint256 requiredStake = getRequiredStake(_pollId); + // If executed, we're done if (poll.executed) { return PollState.Executed; - } else if (poll.stakes[YAY] < requiredStake) { + // Not fully staked, not (yet) going to a vote + } else if (poll.stakes[YAY] < requiredStake || poll.stakes[NAY] < requiredStake) { + // Are we still staking? if (now < poll.lastEvent + STAKE_PERIOD) { - return PollState.StakeYay; + return PollState.Staking; + // If not, did the YAY side reach a full stake? + } else if (poll.stakes[YAY] == requiredStake) { + return PollState.Executable; + // If not, was there a prior vote we can fall back on? + } else if (poll.votes[NAY] > 0 || poll.votes[YAY] > 0) { + return PollState.Executable; + // Otherwise, the poll failed } else { return PollState.Failed; } - } else if (poll.stakes[NAY] < requiredStake) { - if (now < poll.lastEvent + STAKE_PERIOD) { - return PollState.StakeNay; - } else { - return PollState.Closed; - } - + // Fully staked, going to a vote } else if (now < poll.lastEvent + VOTE_PERIOD) { - return PollState.Open; - + return PollState.Voting; } else if (now < poll.lastEvent + (VOTE_PERIOD + REVEAL_PERIOD)) { return PollState.Reveal; - - } else { + } else if (now < poll.lastEvent + (VOTE_PERIOD + REVEAL_PERIOD + STAKE_PERIOD)) { return PollState.Closed; + } else { + return PollState.Executable; } } @@ -331,10 +399,9 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { { pollCount += 1; polls[pollCount].lastEvent = now; - polls[pollCount].skillId = _skillId; polls[pollCount].rootHash = colonyNetwork.getReputationRootHash(); + polls[pollCount].skillId = _skillId; polls[pollCount].skillRep = checkReputation(pollCount, address(0x0), _key, _value, _branchMask, _siblings); - polls[pollCount].voterComp = wmul(getRequiredStake(pollCount) * 2, VOTER_REWARD_PCT); polls[pollCount].action = _action; emit PollCreated(pollCount, _skillId); @@ -349,7 +416,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { } function getRequiredStake(uint256 _pollId) internal view returns (uint256) { - return wmul(polls[_pollId].skillRep, STAKE_PCT); + return wmul(polls[_pollId].skillRep, STAKE_FRACTION); } function checkReputation( @@ -402,14 +469,14 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { } } - function executeCall(address to, bytes memory data) internal returns (bool success) { + function executeCall(address to, bytes memory action) 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) + // call(g, a, v, in, insize, out, outsize) + success := call(gas, to, 0, add(action, 0x20), mload(action), 0, 0) } } @@ -421,8 +488,10 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { function encodeSlot(bytes memory action) internal returns (bytes32 slot) { assembly { - // Hash all but last (value) byte, since mload(action) gives length+32 - slot := keccak256(action, mload(action)) + // Hash all but last (value) bytes32 + // Recall: mload(action) gives length of bytes array + // So skip past the first bytes32 (length), and the last bytes32 (value) + slot := keccak256(add(action, 0x20), sub(mload(action), 0x20)) } } } diff --git a/contracts/extensions/VotingReputationFactory.sol b/contracts/extensions/VotingReputationFactory.sol index c29dbddd53..211deeb24a 100644 --- a/contracts/extensions/VotingReputationFactory.sol +++ b/contracts/extensions/VotingReputationFactory.sol @@ -18,8 +18,8 @@ pragma solidity 0.5.8; pragma experimental ABIEncoderV2; -import "./../ColonyDataTypes.sol"; -import "./../IColony.sol"; +import "./../colony/ColonyDataTypes.sol"; +import "./../colony/IColony.sol"; import "./ExtensionFactory.sol"; import "./VotingReputation.sol"; diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index 6e04d7a492..14639dd8b4 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -1,5 +1,6 @@ /* globals artifacts */ +import BN from "bn.js"; import chai from "chai"; import bnChai from "bn-chai"; import shortid from "shortid"; @@ -40,10 +41,10 @@ contract("Voting Reputation", (accounts) => { let reputationTree; - let colonyKey; - let colonyValue; - let colonyMask; - let colonySiblings; + let domain1Key; + let domain1Value; + let domain1Mask; + let domain1Siblings; let user0Key; let user0Value; @@ -67,12 +68,11 @@ contract("Voting Reputation", (accounts) => { const SALT = soliditySha3(shortid.generate()); const FAKE = soliditySha3(shortid.generate()); - const STAKE_YAY = 0; - const STAKE_NAY = 1; - const OPEN = 2; - // const REVEAL = 3; - // const CLOSED = 4; - // const EXECUTED = 5; + const STAKING = 0; + const OPEN = 1; + // const REVEAL = 2; + // const CLOSED = 3; + // const EXECUTED = 4; const REQUIRED_STAKE = WAD.muln(3).divn(1000); @@ -112,15 +112,12 @@ contract("Voting Reputation", (accounts) => { await token.approve(tokenLocking.address, WAD, { from: USER0 }); await token.approve(tokenLocking.address, WAD, { from: USER1 }); await token.approve(tokenLocking.address, WAD, { from: USER2 }); - await tokenLocking.deposit(token.address, WAD, true, { from: USER0 }); - await tokenLocking.deposit(token.address, WAD, true, { from: USER1 }); - await tokenLocking.deposit(token.address, WAD, true, { from: USER2 }); + await tokenLocking.deposit(token.address, WAD, { from: USER0 }); + await tokenLocking.deposit(token.address, WAD, { from: USER1 }); + await tokenLocking.deposit(token.address, WAD, { from: USER2 }); await colony.approveStake(voting.address, 1, WAD, { from: USER0 }); await colony.approveStake(voting.address, 1, WAD, { from: USER1 }); await colony.approveStake(voting.address, 1, WAD, { from: USER2 }); - await tokenLocking.approveStake(colony.address, WAD, { from: USER0 }); - await tokenLocking.approveStake(colony.address, WAD, { from: USER1 }); - await tokenLocking.approveStake(colony.address, WAD, { from: USER2 }); reputationTree = new PatriciaTree(); await reputationTree.insert( @@ -155,10 +152,18 @@ contract("Voting Reputation", (accounts) => { makeReputationKey(colony.address, domain1.skillId, USER2), // User2, very little rep makeReputationValue(REQUIRED_STAKE.subn(1), 8) ); + await reputationTree.insert( + makeReputationKey(colony.address, domain2.skillId, USER0), // User0, domain 2 + makeReputationValue(WAD.divn(3), 9) + ); + await reputationTree.insert( + makeReputationKey(colony.address, domain2.skillId, USER1), // User1, domain 2 + makeReputationValue(WAD.divn(3).muln(2), 10) + ); - colonyKey = makeReputationKey(colony.address, domain1.skillId); - colonyValue = makeReputationValue(WAD.muln(3), 1); - [colonyMask, colonySiblings] = await reputationTree.getProof(colonyKey); + domain1Key = makeReputationKey(colony.address, domain1.skillId); + domain1Value = makeReputationValue(WAD.muln(3), 1); + [domain1Mask, domain1Siblings] = await reputationTree.getProof(domain1Key); user0Key = makeReputationKey(colony.address, domain1.skillId, USER0); user0Value = makeReputationValue(WAD, 2); @@ -188,7 +193,7 @@ contract("Voting Reputation", (accounts) => { describe("creating polls", async () => { it("can create a root poll", async () => { const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); - await voting.createRootPoll(action, colonyKey, colonyValue, colonyMask, colonySiblings); + await voting.createRootPoll(action, domain1Key, domain1Value, domain1Mask, domain1Siblings); const pollId = await voting.getPollCount(); const poll = await voting.getPoll(pollId); @@ -198,7 +203,7 @@ contract("Voting Reputation", (accounts) => { it("can create a domain poll in the root domain", async () => { // Create poll in domain of action (1) const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); - await voting.createDomainPoll(1, UINT256_MAX, action, colonyKey, colonyValue, colonyMask, colonySiblings); + await voting.createDomainPoll(1, UINT256_MAX, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); const pollId = await voting.getPollCount(); const poll = await voting.getPoll(pollId); @@ -219,17 +224,17 @@ contract("Voting Reputation", (accounts) => { expect(poll.skillId).to.eq.BN(domain2.skillId); }); - it("can escalate a domain poll", async () => { + it("can externally escalate a domain poll", async () => { // Create poll in parent domain (1) of action (2) const action = await encodeTxData(colony, "makeTask", [1, 0, FAKE, 2, 0, 0]); - await voting.createDomainPoll(1, 0, action, colonyKey, colonyValue, colonyMask, colonySiblings); + await voting.createDomainPoll(1, 0, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); const pollId = await voting.getPollCount(); const poll = await voting.getPoll(pollId); expect(poll.skillId).to.eq.BN(domain1.skillId); }); - it("cannot escalate a domain poll with an invalid domain proof", async () => { + it("cannot externally escalate a domain poll with an invalid domain proof", async () => { const key = makeReputationKey(colony.address, domain3.skillId); const value = makeReputationValue(WAD, 7); const [mask, siblings] = await reputationTree.getProof(key); @@ -245,7 +250,7 @@ contract("Voting Reputation", (accounts) => { beforeEach(async () => { const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); - await voting.createRootPoll(action, colonyKey, colonyValue, colonyMask, colonySiblings); + await voting.createRootPoll(action, domain1Key, domain1Value, domain1Mask, domain1Siblings); pollId = await voting.getPollCount(); }); @@ -265,11 +270,11 @@ contract("Voting Reputation", (accounts) => { it("updates the poll states correctly", async () => { let pollState = await voting.getPollState(pollId); - expect(pollState).to.eq.BN(STAKE_YAY); + expect(pollState).to.eq.BN(STAKING); await voting.stakePoll(pollId, 1, 0, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); pollState = await voting.getPollState(pollId); - expect(pollState).to.eq.BN(STAKE_NAY); + expect(pollState).to.eq.BN(STAKING); await voting.stakePoll(pollId, 1, 0, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); pollState = await voting.getPollState(pollId); @@ -308,26 +313,13 @@ contract("Voting Reputation", (accounts) => { ); }); - it("cannot stake out-of-order", async () => { - await checkErrorRevert( - voting.stakePoll(pollId, 1, 0, 1, false, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), - "voting-rep-out-of-order" - ); - }); - - it("cannot stake yay, once time runs out", async () => { + it("cannot stake once time runs out", async () => { await forwardTime(STAKE_WINDOW, this); await checkErrorRevert( voting.stakePoll(pollId, 1, 0, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), "voting-rep-staking-closed" ); - }); - - it("cannot stake nay, once time runs out", async () => { - await voting.stakePoll(pollId, 1, 0, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - - await forwardTime(STAKE_WINDOW, this); await checkErrorRevert( voting.stakePoll(pollId, 1, 0, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }), @@ -341,7 +333,7 @@ contract("Voting Reputation", (accounts) => { beforeEach(async () => { const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); - await voting.createRootPoll(action, colonyKey, colonyValue, colonyMask, colonySiblings); + await voting.createRootPoll(action, domain1Key, domain1Value, domain1Mask, domain1Siblings); pollId = await voting.getPollCount(); await voting.stakePoll(pollId, 1, 0, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); @@ -390,7 +382,8 @@ contract("Voting Reputation", (accounts) => { it("can reveal votes after poll closes, but doesn't count", async () => { await voting.submitVote(pollId, soliditySha3(SALT, false), { from: USER0 }); - await forwardTime(VOTE_WINDOW + REVEAL_WINDOW, this); + await forwardTime(VOTE_WINDOW, this); + await forwardTime(REVEAL_WINDOW, this); await voting.revealVote(pollId, SALT, false, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); @@ -420,19 +413,21 @@ contract("Voting Reputation", (accounts) => { const user0Value2 = makeReputationValue(WAD.muln(3), 2); await reputationTree.insert(user0Key, user0Value2); - const [colonyMask2, colonySiblings2] = await reputationTree.getProof(colonyKey); + const [domain1Mask2, domain1Siblings2] = await reputationTree.getProof(domain1Key); const [user0Mask2, user0Siblings2] = await reputationTree.getProof(user0Key); const [user1Mask2, user1Siblings2] = await reputationTree.getProof(user1Key); // Set new rootHash const rootHash = await reputationTree.getRootHash(); const repCycle = await getActiveRepCycle(colonyNetwork); + await forwardTime(MINING_CYCLE_DURATION, this); + await repCycle.submitRootHash(rootHash, 0, "0x00", 10, { from: MINER }); await repCycle.confirmNewHash(0); // Create new poll with new reputation state - await voting.createRootPoll(FAKE, colonyKey, colonyValue, colonyMask2, colonySiblings2); + await voting.createRootPoll(FAKE, domain1Key, domain1Value, domain1Mask2, domain1Siblings2); const pollId2 = await voting.getPollCount(); await voting.stakePoll(pollId2, 1, 0, 1, true, REQUIRED_STAKE, user0Key, user0Value2, user0Mask2, user0Siblings2, { from: USER0 }); await voting.stakePoll(pollId2, 1, 0, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask2, user1Siblings2, { from: USER1 }); @@ -446,6 +441,7 @@ contract("Voting Reputation", (accounts) => { it("cannot submit a vote if voting is closed", async () => { await forwardTime(VOTE_WINDOW, this); + await checkErrorRevert(voting.submitVote(pollId, soliditySha3(SALT, false)), "voting-rep-poll-not-open"); }); @@ -456,12 +452,15 @@ contract("Voting Reputation", (accounts) => { it("cannot reveal a vote with a bad secret", async () => { await voting.submitVote(pollId, soliditySha3(SALT, false)); + await forwardTime(VOTE_WINDOW, this); + await checkErrorRevert(voting.revealVote(pollId, SALT, true, FAKE, FAKE, 0, []), "voting-rep-secret-no-match"); }); it("cannot reveal a vote with a bad proof", async () => { await voting.submitVote(pollId, soliditySha3(SALT, false), { from: USER0 }); + await forwardTime(VOTE_WINDOW, this); // Invalid proof (wrong root hash) @@ -496,7 +495,7 @@ contract("Voting Reputation", (accounts) => { beforeEach(async () => { const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); - await voting.createRootPoll(action, colonyKey, colonyValue, colonyMask, colonySiblings); + await voting.createRootPoll(action, domain1Key, domain1Value, domain1Mask, domain1Siblings); pollId = await voting.getPollCount(); }); @@ -514,10 +513,8 @@ contract("Voting Reputation", (accounts) => { await forwardTime(STAKE_WINDOW, this); - const taskCountPrev = await colony.getTaskCount(); - await voting.executePoll(pollId); - const taskCountPost = await colony.getTaskCount(); - expect(taskCountPost).to.eq.BN(taskCountPrev.addn(1)); + const { logs } = await voting.executePoll(pollId); + expect(logs[0].args.success).to.be.true; }); it("cannot take an action twice", async () => { @@ -525,10 +522,8 @@ contract("Voting Reputation", (accounts) => { await forwardTime(STAKE_WINDOW, this); - const taskCountPrev = await colony.getTaskCount(); - await voting.executePoll(pollId); - const taskCountPost = await colony.getTaskCount(); - expect(taskCountPost).to.eq.BN(taskCountPrev.addn(1)); + const { logs } = await voting.executePoll(pollId); + expect(logs[0].args.success).to.be.true; await checkErrorRevert(voting.executePoll(pollId), "voting-rep-poll-already-executed"); }); @@ -540,15 +535,17 @@ contract("Voting Reputation", (accounts) => { await voting.submitVote(pollId, soliditySha3(SALT, true), { from: USER0 }); await forwardTime(VOTE_WINDOW, this); + await voting.revealVote(pollId, SALT, true, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await checkErrorRevert(voting.executePoll(pollId), "voting-rep-poll-not-closed"); + await forwardTime(REVEAL_WINDOW, this); + + await checkErrorRevert(voting.executePoll(pollId), "voting-rep-poll-escalation-window-open"); - await forwardTime(REVEAL_WINDOW * 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 forwardTime(STAKE_WINDOW, this); + + const { logs } = await voting.executePoll(pollId); + expect(logs[0].args.success).to.be.true; }); it("cannot take an action if the poll fails", async () => { @@ -558,13 +555,14 @@ contract("Voting Reputation", (accounts) => { await voting.submitVote(pollId, soliditySha3(SALT, false), { from: USER0 }); await forwardTime(VOTE_WINDOW, this); + await voting.revealVote(pollId, SALT, false, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await forwardTime(REVEAL_WINDOW * 2, this); - const taskCountPrev = await colony.getTaskCount(); - await voting.executePoll(pollId); - const taskCountPost = await colony.getTaskCount(); - expect(taskCountPost).to.eq.BN(taskCountPrev); + await forwardTime(REVEAL_WINDOW, this); + await forwardTime(STAKE_WINDOW, this); + + const { logs } = await voting.executePoll(pollId); + expect(logs[0].args.success).to.be.false; }); it("cannot take an action if there is insufficient voting power (state change actions)", async () => { @@ -572,13 +570,14 @@ contract("Voting Reputation", (accounts) => { const action = await encodeTxData(colony, "setExpenditureState", [1, UINT256_MAX, 1, 0, [], [], ethers.constants.HashZero]); // Create two polls for same variable - await voting.createDomainPoll(1, UINT256_MAX, action, colonyKey, colonyValue, colonyMask, colonySiblings); + await voting.createDomainPoll(1, UINT256_MAX, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); const pollId1 = await voting.getPollCount(); + await voting.createDomainPoll(1, UINT256_MAX, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + const pollId2 = await voting.getPollCount(); + await voting.stakePoll(pollId1, 1, 0, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await voting.stakePoll(pollId1, 1, 0, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await voting.createDomainPoll(1, UINT256_MAX, action, colonyKey, colonyValue, colonyMask, colonySiblings); - const pollId2 = await voting.getPollCount(); await voting.stakePoll(pollId2, 1, 0, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await voting.stakePoll(pollId2, 1, 0, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); @@ -591,47 +590,122 @@ contract("Voting Reputation", (accounts) => { await voting.revealVote(pollId2, SALT, true, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await forwardTime(REVEAL_WINDOW, this); + await forwardTime(STAKE_WINDOW, this); + + let logs; + ({ logs } = await voting.executePoll(pollId1)); + expect(logs[0].args.success).to.be.true; + + ({ logs } = await voting.executePoll(pollId2)); + expect(logs[0].args.success).to.be.false; + }); + + it("can set vote power correctly if there is insufficient opposition", async () => { + const action = await encodeTxData(colony, "setExpenditureState", [1, UINT256_MAX, 1, 0, [], [], ethers.constants.HashZero]); + + await voting.createDomainPoll(1, UINT256_MAX, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + pollId = await voting.getPollCount(); + + await voting.stakePoll(pollId, 1, 0, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + + await forwardTime(STAKE_WINDOW, this); + + await voting.executePoll(pollId); + const slot = soliditySha3(action.slice(0, action.length - 64)); + const pastVotes = await voting.getPastVotes(slot); + expect(pastVotes).to.eq.BN(WAD.muln(2).subn(2)); // ~66.6% of 3 WAD + }); + + it("can set vote power correctly after a vote", async () => { + const action = await encodeTxData(colony, "setExpenditureState", [1, UINT256_MAX, 1, 0, [], [], ethers.constants.HashZero]); + + await voting.createDomainPoll(1, UINT256_MAX, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + pollId = await voting.getPollCount(); + + await voting.stakePoll(pollId, 1, 0, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, 0, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.submitVote(pollId, soliditySha3(SALT, true), { from: USER0 }); - await voting.executePoll(pollId1); + await forwardTime(VOTE_WINDOW, this); + + await voting.revealVote(pollId, SALT, true, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + + await forwardTime(REVEAL_WINDOW, this); + await forwardTime(STAKE_WINDOW, this); - await checkErrorRevert(voting.executePoll(pollId2), "voting-rep-insufficient-vote-power"); + await voting.executePoll(pollId); + const slot = soliditySha3(action.slice(0, action.length - 64)); + const pastVotes = await voting.getPastVotes(slot); + expect(pastVotes).to.eq.BN(WAD); // USER0 had 1 WAD of reputation }); }); - describe("claiming staker rewards", async () => { + describe("claiming rewards", async () => { let pollId; beforeEach(async () => { const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); - await voting.createRootPoll(action, colonyKey, colonyValue, colonyMask, colonySiblings); + await voting.createRootPoll(action, domain1Key, domain1Value, domain1Mask, domain1Siblings); pollId = await voting.getPollCount(); }); - it("can let stakers claim rewards, based on the outcome", async () => { + it("can let stakers claim rewards, based on the stake outcome", async () => { + await voting.stakePoll(pollId, 1, 0, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, 0, 1, false, REQUIRED_STAKE.divn(2), user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + + await forwardTime(STAKE_WINDOW, this); + + await voting.executePoll(pollId); + + const user0LockPre = await tokenLocking.getUserLock(token.address, USER0); + const user1LockPre = await tokenLocking.getUserLock(token.address, USER1); + + await voting.claimReward(pollId, true, { from: USER0 }); + await voting.claimReward(pollId, false, { from: USER1 }); + + const user0LockPost = await tokenLocking.getUserLock(token.address, USER0); + const user1LockPost = await tokenLocking.getUserLock(token.address, USER1); + + // Note that no voter rewards were paid out + const expectedReward0 = REQUIRED_STAKE.add(REQUIRED_STAKE.divn(20)); // stake + (stake / 20) + const expectedReward1 = REQUIRED_STAKE.divn(20).muln(9); // (stake * 9 / 20) + + expect(new BN(user0LockPost.balance).sub(new BN(user0LockPre.balance))).to.eq.BN(expectedReward0); + expect(new BN(user1LockPost.balance).sub(new BN(user1LockPre.balance))).to.eq.BN(expectedReward1); + }); + + it("can let stakers claim rewards, based on the vote outcome", async () => { await voting.stakePoll(pollId, 1, 0, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await voting.stakePoll(pollId, 1, 0, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await voting.submitVote(pollId, soliditySha3(SALT, true), { from: USER0 }); await voting.submitVote(pollId, soliditySha3(SALT, false), { from: USER1 }); + await forwardTime(VOTE_WINDOW, this); + await voting.revealVote(pollId, SALT, true, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await voting.revealVote(pollId, SALT, false, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await forwardTime(REVEAL_WINDOW, this); + await forwardTime(STAKE_WINDOW, this); + await voting.executePoll(pollId); - await tokenLocking.claim(token.address, true, { from: USER0 }); - await tokenLocking.claim(token.address, true, { from: USER1 }); + const user0LockPre = await tokenLocking.getUserLock(token.address, USER0); + const user1LockPre = await tokenLocking.getUserLock(token.address, USER1); + await voting.claimReward(pollId, true, { from: USER0 }); await voting.claimReward(pollId, false, { from: USER1 }); + const user0LockPost = await tokenLocking.getUserLock(token.address, USER0); + const user1LockPost = await tokenLocking.getUserLock(token.address, USER1); + const stakerRewards = REQUIRED_STAKE.divn(10).muln(9); const expectedReward0 = stakerRewards.divn(3).muln(2); // (stake * .9) * (winPct = 1/3 * 2) const expectedReward1 = stakerRewards.divn(3).muln(4); // (stake * .9) * (winPct = 2/3 * 2) - const user0Lock = await tokenLocking.getUserLock(token.address, USER0); - const user1Lock = await tokenLocking.getUserLock(token.address, USER1); - expect(user0Lock.pendingBalance).to.eq.BN(expectedReward0); - expect(user1Lock.pendingBalance).to.eq.BN(expectedReward1); + expect(new BN(user0LockPost.balance).sub(new BN(user0LockPre.balance))).to.eq.BN(expectedReward0); + expect(new BN(user1LockPost.balance).sub(new BN(user1LockPre.balance))).to.eq.BN(expectedReward1); }); it("cannot claim rewards twice", async () => { @@ -639,60 +713,190 @@ contract("Voting Reputation", (accounts) => { await voting.stakePoll(pollId, 1, 0, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await voting.submitVote(pollId, soliditySha3(SALT, true), { from: USER0 }); + await forwardTime(VOTE_WINDOW, this); + await voting.revealVote(pollId, SALT, true, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await forwardTime(REVEAL_WINDOW, this); + await forwardTime(STAKE_WINDOW, this); + await voting.executePoll(pollId); await voting.claimReward(pollId, true, { from: USER0 }); - await tokenLocking.claim(token.address, true, { from: USER0 }); + const userLock0 = await tokenLocking.getUserLock(token.address, USER0); await voting.claimReward(pollId, true, { from: USER0 }); - - const userLock = await tokenLocking.getUserLock(token.address, USER0); - expect(userLock.pendingBalance).to.be.zero; + const userLock1 = await tokenLocking.getUserLock(token.address, USER0); + expect(userLock0.balance).to.eq.BN(userLock1.balance); }); it("cannot claim rewards before a poll is executed", async () => { await checkErrorRevert(voting.claimReward(pollId, true, { from: USER0 }), "voting-rep-not-executed"); }); + }); - it("can claim unpaid voter rewards for colony", async () => { - await voting.stakePoll(pollId, 1, 0, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, 0, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + describe("escalating polls", async () => { + let pollId; + + beforeEach(async () => { + const domain2Key = makeReputationKey(colony.address, domain2.skillId); + const domain2Value = makeReputationValue(WAD, 6); + const [domain2Mask, domain2Siblings] = await reputationTree.getProof(domain2Key); + + user0Key = makeReputationKey(colony.address, domain2.skillId, USER0); + user0Value = makeReputationValue(WAD.divn(3), 9); + [user0Mask, user0Siblings] = await reputationTree.getProof(user0Key); + + user1Key = makeReputationKey(colony.address, domain2.skillId, USER1); + user1Value = makeReputationValue(WAD.divn(3).muln(2), 10); + [user1Mask, user1Siblings] = await reputationTree.getProof(user1Key); + + const action = await encodeTxData(colony, "makeTask", [1, 0, FAKE, 2, 0, 0]); + await voting.createDomainPoll(2, UINT256_MAX, action, domain2Key, domain2Value, domain2Mask, domain2Siblings); + pollId = await voting.getPollCount(); + + await colony.approveStake(voting.address, 2, WAD, { from: USER0 }); + await colony.approveStake(voting.address, 2, WAD, { from: USER1 }); + + await voting.stakePoll(pollId, 1, 0, 2, true, WAD.divn(1000), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, 0, 2, false, WAD.divn(1000), user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + + // Note that this is a passing vote + await voting.submitVote(pollId, soliditySha3(SALT, false), { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, true), { from: USER1 }); - await voting.submitVote(pollId, soliditySha3(SALT, true), { from: USER0 }); await forwardTime(VOTE_WINDOW, this); - await voting.revealVote(pollId, SALT, true, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + + await voting.revealVote(pollId, SALT, false, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.revealVote(pollId, SALT, true, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await forwardTime(REVEAL_WINDOW, this); + }); + + it("can internally escalate a domain poll after a vote, as NAY lead", async () => { + await voting.escalatePoll(pollId, 1, 0, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); + }); + + it("can internally escalate a domain poll after a vote, as YAY lead", async () => { + await voting.escalatePoll(pollId, 1, 0, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER1 }); + }); + + it("cannot internally escalate a domain poll if not a lead", async () => { + await checkErrorRevert( + voting.escalatePoll(pollId, 1, 0, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER2 }), + "voting-rep-user-not-lead" + ); + }); + + it("cannot internally escalate a domain poll if not in a 'closed' state", async () => { + await forwardTime(STAKE_WINDOW, this); + await voting.executePoll(pollId); - const colonyBalancePrev = await token.balanceOf(colony.address); - await voting.claimRewardForColony(pollId); - const colonyBalancePost = await token.balanceOf(colony.address); + await checkErrorRevert( + voting.escalatePoll(pollId, 1, 0, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER2 }), + "voting-rep-poll-not-closed" + ); + }); - // Since USER1 didn't vote and has 2/3 of the reputation - const expectedReward = REQUIRED_STAKE.muln(2).divn(10).divn(3).muln(2); // eslint-disable-line prettier/prettier - expect(colonyBalancePost.sub(colonyBalancePrev)).to.eq.BN(expectedReward); + it("cannot internally escalate a domain poll with an invalid domain proof", async () => { + await checkErrorRevert( + voting.escalatePoll(pollId, 1, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }), + "voting-rep-invalid-domain-proof" + ); }); - it("canot claim unpaid voter rewards for colony twice", async () => { - await voting.stakePoll(pollId, 1, 0, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, 0, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + it("cannot internally escalate a domain poll with an invalid reputation proof", async () => { + await checkErrorRevert(voting.escalatePoll(pollId, 1, 0, "0x0", "0x0", "0x0", [], { from: USER0 }), "voting-rep-invalid-root-hash"); + }); + + it("can stake after internally escalating a domain poll", async () => { + await voting.escalatePoll(pollId, 1, 0, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); + + user0Key = makeReputationKey(colony.address, domain1.skillId, USER0); + user0Value = makeReputationValue(WAD, 2); + [user0Mask, user0Siblings] = await reputationTree.getProof(user0Key); + + user1Key = makeReputationKey(colony.address, domain1.skillId, USER1); + user1Value = makeReputationValue(WAD.muln(2), 5); + [user1Mask, user1Siblings] = await reputationTree.getProof(user1Key); + + const remainingStake = REQUIRED_STAKE.sub(WAD.divn(1000)); + await voting.stakePoll(pollId, 1, 0, 1, true, remainingStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, 0, 1, false, remainingStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + }); + + it("can execute after internally escalating a domain poll, if there is insufficient opposition", async () => { + await voting.escalatePoll(pollId, 1, 0, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); + + user0Key = makeReputationKey(colony.address, domain1.skillId, USER0); + user0Value = makeReputationValue(WAD, 2); + [user0Mask, user0Siblings] = await reputationTree.getProof(user0Key); + + const remainingStake = REQUIRED_STAKE.sub(WAD.divn(1000)); + await voting.stakePoll(pollId, 1, 0, 1, true, remainingStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + + await forwardTime(STAKE_WINDOW, this); + + const { logs } = await voting.executePoll(pollId); + expect(logs[0].args.success).to.be.true; + }); + + it("cannot execute after internally escalating a domain poll, if there is insufficient support", async () => { + await voting.escalatePoll(pollId, 1, 0, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); + + user1Key = makeReputationKey(colony.address, domain1.skillId, USER1); + user1Value = makeReputationValue(WAD.muln(2), 5); + [user1Mask, user1Siblings] = await reputationTree.getProof(user1Key); + + const remainingStake = REQUIRED_STAKE.sub(WAD.divn(1000)); + await voting.stakePoll(pollId, 1, 0, 1, false, remainingStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + + await forwardTime(STAKE_WINDOW, this); + + const { logs } = await voting.executePoll(pollId); + expect(logs[0].args.success).to.be.false; + }); + + it("can fall back on the previous vote if both sides fail to stake", async () => { + await voting.escalatePoll(pollId, 1, 0, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); + + await forwardTime(STAKE_WINDOW, this); + + // Note that the previous vote succeeded + const { logs } = await voting.executePoll(pollId); + expect(logs[0].args.success).to.be.true; + }); + + it("can use the result of a new vote after internally escalating a domain poll", async () => { + await voting.escalatePoll(pollId, 1, 0, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); + + user0Key = makeReputationKey(colony.address, domain1.skillId, USER0); + user0Value = makeReputationValue(WAD, 2); + [user0Mask, user0Siblings] = await reputationTree.getProof(user0Key); + + user1Key = makeReputationKey(colony.address, domain1.skillId, USER1); + user1Value = makeReputationValue(WAD.muln(2), 5); + [user1Mask, user1Siblings] = await reputationTree.getProof(user1Key); + + const remainingStake = REQUIRED_STAKE.sub(WAD.divn(1000)); + await voting.stakePoll(pollId, 1, 0, 1, true, remainingStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, 0, 1, false, remainingStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + + // Make the vote fail this time (everyone votes against) + await voting.submitVote(pollId, soliditySha3(SALT, false), { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, false), { from: USER1 }); - await voting.submitVote(pollId, soliditySha3(SALT, true), { from: USER0 }); await forwardTime(VOTE_WINDOW, this); - await voting.revealVote(pollId, SALT, true, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await forwardTime(REVEAL_WINDOW, this); - await voting.executePoll(pollId); - await voting.claimRewardForColony(pollId); - // Already claimed it + await voting.revealVote(pollId, SALT, false, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.revealVote(pollId, SALT, false, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - const colonyBalancePrev = await token.balanceOf(colony.address); - await voting.claimRewardForColony(pollId); - const colonyBalancePost = await token.balanceOf(colony.address); + await forwardTime(REVEAL_WINDOW, this); + await forwardTime(STAKE_WINDOW, this); - expect(colonyBalancePost.sub(colonyBalancePrev)).to.be.zero; + const { logs } = await voting.executePoll(pollId); + expect(logs[0].args.success).to.be.false; }); }); }); From 472472f2d3316afab08bc03c23a3bbaafbc27aad Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Fri, 22 May 2020 12:17:16 -0700 Subject: [PATCH 14/61] Add loser penalty, remove leads --- contracts/extensions/VotingReputation.sol | 48 +++++-- test/extensions/voting-rep.js | 158 +++++++++++++--------- 2 files changed, 126 insertions(+), 80 deletions(-) diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index 9c7c47eeb5..658f7bcf6f 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -75,7 +75,6 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { uint256 skillId; uint256 skillRep; uint256 unpaidRewards; - address[2] leads; // [nay, yay] uint256[2] stakes; // [nay, yay] uint256[2] votes; // [nay, yay] bytes action; @@ -157,11 +156,6 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { colony.obligateStake(msg.sender, _domainId, _amount); colony.transferStake(_permissionDomainId, _childSkillIndex, address(this), msg.sender, _domainId, _amount, address(this)); - // Set lead if first staker - if (poll.stakes[toInt(_vote)] == 0) { - poll.leads[toInt(_vote)] = msg.sender; - } - // Update the stake poll.unpaidRewards = add(poll.unpaidRewards, _amount); poll.stakes[toInt(_vote)] = add(poll.stakes[toInt(_vote)], _amount); @@ -238,7 +232,6 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { { Poll storage poll = polls[_pollId]; require(getPollState(_pollId) == PollState.Closed, "voting-rep-poll-not-closed"); - require(msg.sender == poll.leads[NAY] || msg.sender == poll.leads[YAY], "voting-rep-user-not-lead"); uint256 newDomainSkillId = colony.getDomain(_newDomainId).skillId; uint256 childSkillId = colonyNetwork.getChildSkillId(newDomainSkillId, _childSkillIndex); @@ -290,25 +283,43 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { emit PollExecuted(_pollId, poll.action, canExecute); } - function claimReward(uint256 _pollId, bool _vote) public { + function claimReward( + uint256 _pollId, + uint256 _permissionDomainId, // For extension's arbitration permission + uint256 _childSkillIndex, // For extension's arbitration permission + uint256 _domainId, + address _user, + bool _vote + ) + public + { Poll storage poll = polls[_pollId]; require(getPollState(_pollId) == PollState.Executed, "voting-rep-not-executed"); + // TODO: can we keep the domainId on the poll somewhere? This seems like a wasteful external call. + // But if it's 10 external calls per word of storage, then < 10 stakers makes this cheaper. + require(colony.getDomain(_domainId).skillId == poll.skillId, "voting-rep-bad-stake-domain"); + // Calculate how much of the stake is left after voter compensation (>= 90%) - uint256 stake = stakers[_pollId][msg.sender][_vote]; + uint256 stake = stakers[_pollId][_user][_vote]; uint256 totalStake = add(poll.stakes[NAY], poll.stakes[YAY]); uint256 rewardFraction = wdiv(poll.unpaidRewards, totalStake); uint256 rewardStake = wmul(stake, rewardFraction); uint256 stakerReward; + uint256 repPenalty; // Went to a vote, use vote to determine reward or penalty - if (poll.stakes[NAY] == getRequiredStake(_pollId) && poll.stakes[YAY] == getRequiredStake(_pollId)) { + if ( + poll.stakes[NAY] == getRequiredStake(_pollId) && + poll.stakes[YAY] == getRequiredStake(_pollId) + ) { uint256 stakerVotes = poll.votes[toInt(_vote)]; uint256 totalVotes = add(poll.votes[NAY], poll.votes[YAY]); uint256 winPercent = wdiv(stakerVotes, totalVotes); uint256 winShare = wmul(winPercent, 2 * WAD); stakerReward = wmul(rewardStake, winShare); + repPenalty = (winShare < WAD) ? sub(stake, wmul(winShare, stake)) : 0; // Your side fully staked, receive 10% (proportional) of loser's stake } else if (poll.stakes[toInt(_vote)] == getRequiredStake(_pollId)) { @@ -319,16 +330,27 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { // Opponent's side fully staked, pay 10% penalty } else if (poll.stakes[toInt(!_vote)] == getRequiredStake(_pollId)) { stakerReward = wmul(rewardStake, (WAD / 10) * 9); + repPenalty = stake / 10; // Neither side fully staked, no reward or penalty } else { stakerReward = rewardStake; } - delete stakers[_pollId][msg.sender][_vote]; - tokenLocking.transfer(token, stakerReward, msg.sender, true); + delete stakers[_pollId][_user][_vote]; + tokenLocking.transfer(token, stakerReward, _user, true); + + if (repPenalty > 0) { + colony.emitDomainReputationPenalty( + _permissionDomainId, + _childSkillIndex, + _domainId, + _user, + -int256(repPenalty) + ); + } - emit PollRewardClaimed(_pollId, msg.sender, _vote, stakerReward); + emit PollRewardClaimed(_pollId, _user, _vote, stakerReward); } // Public view functions diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index 14639dd8b4..9a097bb985 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -23,6 +23,7 @@ const { expect } = chai; chai.use(bnChai(web3.utils.BN)); const TokenLocking = artifacts.require("TokenLocking"); +const IReputationMiningCycle = artifacts.require("IReputationMiningCycle"); const VotingReputation = artifacts.require("VotingReputation"); const VotingReputationFactory = artifacts.require("VotingReputationFactory"); @@ -94,8 +95,8 @@ contract("Voting Reputation", (accounts) => { ({ colony, token } = await setupRandomColony(colonyNetwork)); // 1 => { 2, 3 } - await colony.addDomain(1, 0, 1); - await colony.addDomain(1, 0, 1); + await colony.addDomain(1, UINT256_MAX, 1); + await colony.addDomain(1, UINT256_MAX, 1); domain1 = await colony.getDomain(1); domain2 = await colony.getDomain(2); domain3 = await colony.getDomain(3); @@ -103,8 +104,8 @@ contract("Voting Reputation", (accounts) => { await votingFactory.deployExtension(colony.address); const votingAddress = await votingFactory.deployedExtensions(colony.address); voting = await VotingReputation.at(votingAddress); - await colony.setArbitrationRole(1, 0, voting.address, 1, true); - await colony.setAdministrationRole(1, 0, voting.address, 1, true); + await colony.setArbitrationRole(1, UINT256_MAX, voting.address, 1, true); + await colony.setAdministrationRole(1, UINT256_MAX, voting.address, 1, true); await token.mint(USER0, WAD); await token.mint(USER1, WAD); @@ -180,7 +181,7 @@ contract("Voting Reputation", (accounts) => { await repCycle.confirmNewHash(0); }); - describe("using the extension factory", async () => { + describe.only("using the extension factory", async () => { it("can install the extension factory once if root and uninstall", async () => { ({ colony } = await setupRandomColony(colonyNetwork)); await checkErrorRevert(votingFactory.deployExtension(colony.address, { from: USER1 }), "colony-extension-user-not-root"); @@ -190,7 +191,7 @@ contract("Voting Reputation", (accounts) => { }); }); - describe("creating polls", async () => { + describe.only("creating polls", async () => { it("can create a root poll", async () => { const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); await voting.createRootPoll(action, domain1Key, domain1Value, domain1Mask, domain1Siblings); @@ -245,7 +246,7 @@ contract("Voting Reputation", (accounts) => { }); }); - describe("staking on polls", async () => { + describe.only("staking on polls", async () => { let pollId; beforeEach(async () => { @@ -255,8 +256,8 @@ contract("Voting Reputation", (accounts) => { }); it("can stake on a poll", async () => { - await voting.stakePoll(pollId, 1, 0, 1, true, 100, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, 0, 1, true, 100, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, 100, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, 100, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); const poll = await voting.getPoll(pollId); expect(poll.stakes[0]).to.be.zero; @@ -272,18 +273,18 @@ contract("Voting Reputation", (accounts) => { let pollState = await voting.getPollState(pollId); expect(pollState).to.eq.BN(STAKING); - await voting.stakePoll(pollId, 1, 0, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); pollState = await voting.getPollState(pollId); expect(pollState).to.eq.BN(STAKING); - await voting.stakePoll(pollId, 1, 0, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); pollState = await voting.getPollState(pollId); expect(pollState).to.eq.BN(OPEN); }); it("cannot stake with someone else's reputation", async () => { await checkErrorRevert( - voting.stakePoll(pollId, 1, 0, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER1 }), + voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER1 }), "voting-rep-invalid-user-address" ); }); @@ -294,14 +295,14 @@ contract("Voting Reputation", (accounts) => { const [user2Mask, user2Siblings] = await reputationTree.getProof(user2Key); await checkErrorRevert( - voting.stakePoll(pollId, 1, 0, 1, true, REQUIRED_STAKE, user2Key, user2Value, user2Mask, user2Siblings, { from: USER2 }), + voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user2Key, user2Value, user2Mask, user2Siblings, { from: USER2 }), "voting-rep-insufficient-rep" ); }); it("cannot stake more than the required stake", async () => { await checkErrorRevert( - voting.stakePoll(pollId, 1, 0, 1, true, REQUIRED_STAKE.addn(1), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), + voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE.addn(1), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), "voting-rep-stake-too-large" ); }); @@ -317,18 +318,18 @@ contract("Voting Reputation", (accounts) => { await forwardTime(STAKE_WINDOW, this); await checkErrorRevert( - voting.stakePoll(pollId, 1, 0, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), + voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), "voting-rep-staking-closed" ); await checkErrorRevert( - voting.stakePoll(pollId, 1, 0, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }), + voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }), "voting-rep-staking-closed" ); }); }); - describe("voting on polls", async () => { + describe.only("voting on polls", async () => { let pollId; beforeEach(async () => { @@ -336,8 +337,8 @@ contract("Voting Reputation", (accounts) => { await voting.createRootPoll(action, domain1Key, domain1Value, domain1Mask, domain1Siblings); pollId = await voting.getPollCount(); - await voting.stakePoll(pollId, 1, 0, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, 0, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); }); it("can rate and reveal for a poll", async () => { @@ -429,8 +430,8 @@ contract("Voting Reputation", (accounts) => { // Create new poll with new reputation state await voting.createRootPoll(FAKE, domain1Key, domain1Value, domain1Mask2, domain1Siblings2); const pollId2 = await voting.getPollCount(); - await voting.stakePoll(pollId2, 1, 0, 1, true, REQUIRED_STAKE, user0Key, user0Value2, user0Mask2, user0Siblings2, { from: USER0 }); - await voting.stakePoll(pollId2, 1, 0, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask2, user1Siblings2, { from: USER1 }); + await voting.stakePoll(pollId2, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value2, user0Mask2, user0Siblings2, { from: USER0 }); + await voting.stakePoll(pollId2, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask2, user1Siblings2, { from: USER1 }); await voting.submitVote(pollId2, soliditySha3(SALT, false), { from: USER0 }); await forwardTime(VOTE_WINDOW, this); @@ -490,7 +491,7 @@ contract("Voting Reputation", (accounts) => { }); }); - describe("executing polls", async () => { + describe.only("executing polls", async () => { let pollId; beforeEach(async () => { @@ -500,7 +501,9 @@ contract("Voting Reputation", (accounts) => { }); it("cannot take an action if there is insufficient support", async () => { - await voting.stakePoll(pollId, 1, 0, 1, true, REQUIRED_STAKE.subn(1), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE.subn(1), user0Key, user0Value, user0Mask, user0Siblings, { + from: USER0, + }); await forwardTime(STAKE_WINDOW, this); @@ -508,8 +511,10 @@ contract("Voting Reputation", (accounts) => { }); it("can take an action if there is insufficient opposition", async () => { - await voting.stakePoll(pollId, 1, 0, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, 0, 1, false, REQUIRED_STAKE.subn(1), user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, REQUIRED_STAKE.subn(1), user1Key, user1Value, user1Mask, user1Siblings, { + from: USER1, + }); await forwardTime(STAKE_WINDOW, this); @@ -518,7 +523,7 @@ contract("Voting Reputation", (accounts) => { }); it("cannot take an action twice", async () => { - await voting.stakePoll(pollId, 1, 0, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await forwardTime(STAKE_WINDOW, this); @@ -529,8 +534,8 @@ contract("Voting Reputation", (accounts) => { }); it("can take an action if the poll passes", async () => { - await voting.stakePoll(pollId, 1, 0, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, 0, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await voting.submitVote(pollId, soliditySha3(SALT, true), { from: USER0 }); @@ -549,8 +554,8 @@ contract("Voting Reputation", (accounts) => { }); it("cannot take an action if the poll fails", async () => { - await voting.stakePoll(pollId, 1, 0, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, 0, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await voting.submitVote(pollId, soliditySha3(SALT, false), { from: USER0 }); @@ -575,11 +580,11 @@ contract("Voting Reputation", (accounts) => { await voting.createDomainPoll(1, UINT256_MAX, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); const pollId2 = await voting.getPollCount(); - await voting.stakePoll(pollId1, 1, 0, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId1, 1, 0, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId1, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId1, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await voting.stakePoll(pollId2, 1, 0, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId2, 1, 0, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId2, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId2, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await voting.submitVote(pollId1, soliditySha3(SALT, true), { from: USER0 }); await voting.submitVote(pollId2, soliditySha3(SALT, true), { from: USER0 }); @@ -606,7 +611,7 @@ contract("Voting Reputation", (accounts) => { await voting.createDomainPoll(1, UINT256_MAX, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); pollId = await voting.getPollCount(); - await voting.stakePoll(pollId, 1, 0, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await forwardTime(STAKE_WINDOW, this); @@ -622,8 +627,8 @@ contract("Voting Reputation", (accounts) => { await voting.createDomainPoll(1, UINT256_MAX, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); pollId = await voting.getPollCount(); - await voting.stakePoll(pollId, 1, 0, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, 0, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await voting.submitVote(pollId, soliditySha3(SALT, true), { from: USER0 }); await forwardTime(VOTE_WINDOW, this); @@ -640,7 +645,7 @@ contract("Voting Reputation", (accounts) => { }); }); - describe("claiming rewards", async () => { + describe.only("claiming rewards", async () => { let pollId; beforeEach(async () => { @@ -650,8 +655,14 @@ contract("Voting Reputation", (accounts) => { }); it("can let stakers claim rewards, based on the stake outcome", async () => { - await voting.stakePoll(pollId, 1, 0, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, 0, 1, false, REQUIRED_STAKE.divn(2), user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + const addr = await colonyNetwork.getReputationMiningCycle(false); + const repCycle = await IReputationMiningCycle.at(addr); + const numEntriesPrev = await repCycle.getReputationUpdateLogLength(); + + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, REQUIRED_STAKE.divn(2), user1Key, user1Value, user1Mask, user1Siblings, { + from: USER1, + }); await forwardTime(STAKE_WINDOW, this); @@ -660,8 +671,8 @@ contract("Voting Reputation", (accounts) => { const user0LockPre = await tokenLocking.getUserLock(token.address, USER0); const user1LockPre = await tokenLocking.getUserLock(token.address, USER1); - await voting.claimReward(pollId, true, { from: USER0 }); - await voting.claimReward(pollId, false, { from: USER1 }); + await voting.claimReward(pollId, 1, UINT256_MAX, 1, USER0, true); + await voting.claimReward(pollId, 1, UINT256_MAX, 1, USER1, false); const user0LockPost = await tokenLocking.getUserLock(token.address, USER0); const user1LockPost = await tokenLocking.getUserLock(token.address, USER1); @@ -672,11 +683,23 @@ contract("Voting Reputation", (accounts) => { expect(new BN(user0LockPost.balance).sub(new BN(user0LockPre.balance))).to.eq.BN(expectedReward0); expect(new BN(user1LockPost.balance).sub(new BN(user1LockPre.balance))).to.eq.BN(expectedReward1); + + // Now check that user0 has no penalty, while user1 has a 10% penalty + const numEntriesPost = await repCycle.getReputationUpdateLogLength(); + expect(numEntriesPost.sub(numEntriesPrev)).to.eq.BN(1); + + const repUpdate = await repCycle.getReputationUpdateLogEntry(numEntriesPost.subn(1)); + expect(repUpdate.user).to.equal(USER1); + expect(repUpdate.amount).to.eq.BN(REQUIRED_STAKE.divn(20).neg()); }); it("can let stakers claim rewards, based on the vote outcome", async () => { - await voting.stakePoll(pollId, 1, 0, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, 0, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + const addr = await colonyNetwork.getReputationMiningCycle(false); + const repCycle = await IReputationMiningCycle.at(addr); + const numEntriesPrev = await repCycle.getReputationUpdateLogLength(); + + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await voting.submitVote(pollId, soliditySha3(SALT, true), { from: USER0 }); await voting.submitVote(pollId, soliditySha3(SALT, false), { from: USER1 }); @@ -694,8 +717,8 @@ contract("Voting Reputation", (accounts) => { const user0LockPre = await tokenLocking.getUserLock(token.address, USER0); const user1LockPre = await tokenLocking.getUserLock(token.address, USER1); - await voting.claimReward(pollId, true, { from: USER0 }); - await voting.claimReward(pollId, false, { from: USER1 }); + await voting.claimReward(pollId, 1, UINT256_MAX, 1, USER0, true); + await voting.claimReward(pollId, 1, UINT256_MAX, 1, USER1, false); const user0LockPost = await tokenLocking.getUserLock(token.address, USER0); const user1LockPost = await tokenLocking.getUserLock(token.address, USER1); @@ -706,11 +729,19 @@ contract("Voting Reputation", (accounts) => { expect(new BN(user0LockPost.balance).sub(new BN(user0LockPre.balance))).to.eq.BN(expectedReward0); expect(new BN(user1LockPost.balance).sub(new BN(user1LockPre.balance))).to.eq.BN(expectedReward1); + + // Now check that user1 has no penalty, while user0 has a 1/3 penalty + const numEntriesPost = await repCycle.getReputationUpdateLogLength(); + expect(numEntriesPost.sub(numEntriesPrev)).to.eq.BN(1); + + const repUpdate = await repCycle.getReputationUpdateLogEntry(numEntriesPost.subn(1)); + expect(repUpdate.user).to.equal(USER0); + expect(repUpdate.amount).to.eq.BN(REQUIRED_STAKE.divn(3).neg()); }); it("cannot claim rewards twice", async () => { - await voting.stakePoll(pollId, 1, 0, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, 0, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await voting.submitVote(pollId, soliditySha3(SALT, true), { from: USER0 }); @@ -723,19 +754,19 @@ contract("Voting Reputation", (accounts) => { await voting.executePoll(pollId); - await voting.claimReward(pollId, true, { from: USER0 }); + await voting.claimReward(pollId, 1, UINT256_MAX, 1, USER0, true); const userLock0 = await tokenLocking.getUserLock(token.address, USER0); - await voting.claimReward(pollId, true, { from: USER0 }); + await voting.claimReward(pollId, 1, UINT256_MAX, 1, USER0, true); const userLock1 = await tokenLocking.getUserLock(token.address, USER0); expect(userLock0.balance).to.eq.BN(userLock1.balance); }); it("cannot claim rewards before a poll is executed", async () => { - await checkErrorRevert(voting.claimReward(pollId, true, { from: USER0 }), "voting-rep-not-executed"); + await checkErrorRevert(voting.claimReward(pollId, 1, UINT256_MAX, 1, USER0, true), "voting-rep-not-executed"); }); }); - describe("escalating polls", async () => { + describe.only("escalating polls", async () => { let pollId; beforeEach(async () => { @@ -773,21 +804,14 @@ contract("Voting Reputation", (accounts) => { await forwardTime(REVEAL_WINDOW, this); }); - it("can internally escalate a domain poll after a vote, as NAY lead", async () => { + it("can internally escalate a domain poll after a vote", async () => { await voting.escalatePoll(pollId, 1, 0, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); }); - it("can internally escalate a domain poll after a vote, as YAY lead", async () => { + it("can internally escalate a domain poll after a vote", async () => { await voting.escalatePoll(pollId, 1, 0, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER1 }); }); - it("cannot internally escalate a domain poll if not a lead", async () => { - await checkErrorRevert( - voting.escalatePoll(pollId, 1, 0, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER2 }), - "voting-rep-user-not-lead" - ); - }); - it("cannot internally escalate a domain poll if not in a 'closed' state", async () => { await forwardTime(STAKE_WINDOW, this); @@ -822,8 +846,8 @@ contract("Voting Reputation", (accounts) => { [user1Mask, user1Siblings] = await reputationTree.getProof(user1Key); const remainingStake = REQUIRED_STAKE.sub(WAD.divn(1000)); - await voting.stakePoll(pollId, 1, 0, 1, true, remainingStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, 0, 1, false, remainingStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, remainingStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, remainingStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); }); it("can execute after internally escalating a domain poll, if there is insufficient opposition", async () => { @@ -834,7 +858,7 @@ contract("Voting Reputation", (accounts) => { [user0Mask, user0Siblings] = await reputationTree.getProof(user0Key); const remainingStake = REQUIRED_STAKE.sub(WAD.divn(1000)); - await voting.stakePoll(pollId, 1, 0, 1, true, remainingStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, remainingStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await forwardTime(STAKE_WINDOW, this); @@ -850,7 +874,7 @@ contract("Voting Reputation", (accounts) => { [user1Mask, user1Siblings] = await reputationTree.getProof(user1Key); const remainingStake = REQUIRED_STAKE.sub(WAD.divn(1000)); - await voting.stakePoll(pollId, 1, 0, 1, false, remainingStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, remainingStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await forwardTime(STAKE_WINDOW, this); @@ -880,8 +904,8 @@ contract("Voting Reputation", (accounts) => { [user1Mask, user1Siblings] = await reputationTree.getProof(user1Key); const remainingStake = REQUIRED_STAKE.sub(WAD.divn(1000)); - await voting.stakePoll(pollId, 1, 0, 1, true, remainingStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, 0, 1, false, remainingStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, remainingStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, remainingStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); // Make the vote fail this time (everyone votes against) await voting.submitVote(pollId, soliditySha3(SALT, false), { from: USER0 }); From 6d0f9b22e260151c53295cc87500edb1972c4213 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Tue, 9 Jun 2020 14:53:27 -0700 Subject: [PATCH 15/61] Create activePolls to track ongoing polls --- contracts/extensions/VotingReputation.sol | 59 +++++++++++++++-------- test/extensions/voting-rep.js | 41 +++++++++++----- 2 files changed, 67 insertions(+), 33 deletions(-) diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index 658f7bcf6f..efea592bff 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -49,7 +49,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { uint256 constant VOTE_PERIOD = 2 days; uint256 constant REVEAL_PERIOD = 2 days; - bytes4 constant CHANGE_FUNC = bytes4( + bytes4 constant CHANGE_FUNCTION = bytes4( keccak256("setExpenditureState(uint256,uint256,uint256,uint256,bool[],bytes32[],bytes32)") ); @@ -84,11 +84,12 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { // Storage uint256 pollCount; mapping (uint256 => Poll) polls; - mapping (uint256 => mapping (address => mapping (bool => uint256))) stakers; + mapping (uint256 => mapping (address => mapping (bool => uint256))) stakes; mapping (address => mapping (uint256 => bytes32)) voteSecrets; - mapping (bytes32 => uint256) pastVotes; + mapping (bytes32 => uint256) pastPolls; + mapping (bytes32 => uint256) activePolls; // Public functions (interface) @@ -148,7 +149,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { // TODO: can we keep the domainId on the poll somewhere? This seems like a wasteful external call. // But if it's 10 external calls per word of storage, then < 10 stakers makes this cheaper. require(colony.getDomain(_domainId).skillId == poll.skillId, "voting-rep-bad-stake-domain"); - require(add(stakers[_pollId][msg.sender][_vote], _amount) <= stakerRep, "voting-rep-insufficient-rep"); + require(add(stakes[_pollId][msg.sender][_vote], _amount) <= stakerRep, "voting-rep-insufficient-rep"); require(add(poll.stakes[toInt(_vote)], _amount) <= getRequiredStake(_pollId), "voting-rep-stake-too-large"); require(getPollState(_pollId) == PollState.Staking, "voting-rep-staking-closed"); @@ -159,7 +160,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { // Update the stake poll.unpaidRewards = add(poll.unpaidRewards, _amount); poll.stakes[toInt(_vote)] = add(poll.stakes[toInt(_vote)], _amount); - stakers[_pollId][msg.sender][_vote] = add(stakers[_pollId][msg.sender][_vote], _amount); + stakes[_pollId][msg.sender][_vote] = add(stakes[_pollId][msg.sender][_vote], _amount); // Update timestamp if fully staked if ( @@ -245,6 +246,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { function executePoll(uint256 _pollId) public { Poll storage poll = polls[_pollId]; PollState pollState = getPollState(_pollId); + bytes32 slot = encodeSlot(poll.action); require(pollState != PollState.Failed, "voting-rep-poll-failed"); require(pollState != PollState.Closed, "voting-rep-poll-escalation-window-open"); @@ -252,9 +254,14 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { require(pollState == PollState.Executable, "voting-rep-poll-not-executable"); poll.executed = true; - bool canExecute = poll.stakes[NAY] <= poll.stakes[YAY] && poll.votes[NAY] <= poll.votes[YAY]; + delete activePolls[slot]; - if (getSig(poll.action) == CHANGE_FUNC) { + bool canExecute = ( + poll.stakes[NAY] <= poll.stakes[YAY] && + poll.votes[NAY] <= poll.votes[YAY] + ); + + if (getSig(poll.action) == CHANGE_FUNCTION) { // Conditions: // - Yay side staked and nay side did not, and doman has sufficient vote power @@ -267,9 +274,8 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { votePower = poll.votes[YAY]; } - bytes32 slot = encodeSlot(poll.action); - if (pastVotes[slot] < votePower) { - pastVotes[slot] = votePower; + if (pastPolls[slot] < votePower) { + pastPolls[slot] = votePower; canExecute = canExecute && true; } else { canExecute = canExecute && false; @@ -301,7 +307,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { require(colony.getDomain(_domainId).skillId == poll.skillId, "voting-rep-bad-stake-domain"); // Calculate how much of the stake is left after voter compensation (>= 90%) - uint256 stake = stakers[_pollId][_user][_vote]; + uint256 stake = stakes[_pollId][_user][_vote]; uint256 totalStake = add(poll.stakes[NAY], poll.stakes[YAY]); uint256 rewardFraction = wdiv(poll.unpaidRewards, totalStake); uint256 rewardStake = wmul(stake, rewardFraction); @@ -337,7 +343,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { stakerReward = rewardStake; } - delete stakers[_pollId][_user][_vote]; + delete stakes[_pollId][_user][_vote]; tokenLocking.transfer(token, stakerReward, _user, true); if (repPenalty > 0) { @@ -364,11 +370,11 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { } function getStake(uint256 _pollId, address _staker, bool _vote) public view returns (uint256) { - return stakers[_pollId][_staker][_vote]; + return stakes[_pollId][_staker][_vote]; } - function getPastVotes(bytes32 _slot) public view returns (uint256) { - return pastVotes[_slot]; + function getPastPoll(bytes32 _slot) public view returns (uint256) { + return pastPolls[_slot]; } function getPollState(uint256 _pollId) public view returns (PollState) { @@ -419,6 +425,8 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { ) internal { + require(activePolls[encodeSlot(_action)] == 0, "voting-rep-already-active"); + pollCount += 1; polls[pollCount].lastEvent = now; polls[pollCount].rootHash = colonyNetwork.getReputationRootHash(); @@ -426,6 +434,8 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { polls[pollCount].skillRep = checkReputation(pollCount, address(0x0), _key, _value, _branchMask, _siblings); polls[pollCount].action = _action; + activePolls[encodeSlot(_action)] = pollCount; + emit PollCreated(pollCount, _skillId); } @@ -509,11 +519,20 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { } function encodeSlot(bytes memory action) internal returns (bytes32 slot) { - assembly { - // Hash all but last (value) bytes32 - // Recall: mload(action) gives length of bytes array - // So skip past the first bytes32 (length), and the last bytes32 (value) - slot := keccak256(add(action, 0x20), sub(mload(action), 0x20)) + if (getSig(action) == CHANGE_FUNCTION) { + assembly { + // Hash all but last (value) bytes32 + // Recall: mload(action) gives length of bytes array + // So skip past the first bytes32 (length), and the last bytes32 (value) + slot := keccak256(add(action, 0x20), sub(mload(action), 0x20)) + } + } else { + assembly { + // Hash entire action + // Recall: mload(action) gives length of bytes array + // So skip past the first bytes32 (length) + slot := keccak256(add(action, 0x20), mload(action)) + } } } } diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index 9a097bb985..f84f766ef0 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -225,6 +225,13 @@ contract("Voting Reputation", (accounts) => { expect(poll.skillId).to.eq.BN(domain2.skillId); }); + it("cannot create a poll for an action if there is an active poll for the same action", async () => { + const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); + await voting.createRootPoll(action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + + await checkErrorRevert(voting.createRootPoll(action, domain1Key, domain1Value, domain1Mask, domain1Siblings), "voting-rep-already-active"); + }); + it("can externally escalate a domain poll", async () => { // Create poll in parent domain (1) of action (2) const action = await encodeTxData(colony, "makeTask", [1, 0, FAKE, 2, 0, 0]); @@ -573,34 +580,42 @@ contract("Voting Reputation", (accounts) => { it("cannot take an action if there is insufficient voting power (state change actions)", async () => { // Set first slot of first expenditure struct to 0x0 const action = await encodeTxData(colony, "setExpenditureState", [1, UINT256_MAX, 1, 0, [], [], ethers.constants.HashZero]); + let logs; - // Create two polls for same variable await voting.createDomainPoll(1, UINT256_MAX, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); const pollId1 = await voting.getPollCount(); - await voting.createDomainPoll(1, UINT256_MAX, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); - const pollId2 = await voting.getPollCount(); await voting.stakePoll(pollId1, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await voting.stakePoll(pollId1, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await voting.stakePoll(pollId2, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId2, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await voting.submitVote(pollId1, soliditySha3(SALT, true), { from: USER0 }); - await voting.submitVote(pollId2, soliditySha3(SALT, true), { from: USER0 }); await forwardTime(VOTE_WINDOW, this); await voting.revealVote(pollId1, SALT, true, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.revealVote(pollId2, SALT, true, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await forwardTime(REVEAL_WINDOW, this); await forwardTime(STAKE_WINDOW, this); - let logs; ({ logs } = await voting.executePoll(pollId1)); expect(logs[0].args.success).to.be.true; + // Create another poll for the same variable + await voting.createDomainPoll(1, UINT256_MAX, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + const pollId2 = await voting.getPollCount(); + + await voting.stakePoll(pollId2, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId2, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + + await voting.submitVote(pollId2, soliditySha3(SALT, true), { from: USER0 }); + + await forwardTime(VOTE_WINDOW, this); + + await voting.revealVote(pollId2, SALT, true, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + + await forwardTime(REVEAL_WINDOW, this); + await forwardTime(STAKE_WINDOW, this); + ({ logs } = await voting.executePoll(pollId2)); expect(logs[0].args.success).to.be.false; }); @@ -617,8 +632,8 @@ contract("Voting Reputation", (accounts) => { await voting.executePoll(pollId); const slot = soliditySha3(action.slice(0, action.length - 64)); - const pastVotes = await voting.getPastVotes(slot); - expect(pastVotes).to.eq.BN(WAD.muln(2).subn(2)); // ~66.6% of 3 WAD + const pastPoll = await voting.getPastPoll(slot); + expect(pastPoll).to.eq.BN(WAD.muln(2).subn(2)); // ~66.6% of 3 WAD }); it("can set vote power correctly after a vote", async () => { @@ -640,8 +655,8 @@ contract("Voting Reputation", (accounts) => { await voting.executePoll(pollId); const slot = soliditySha3(action.slice(0, action.length - 64)); - const pastVotes = await voting.getPastVotes(slot); - expect(pastVotes).to.eq.BN(WAD); // USER0 had 1 WAD of reputation + const pastPoll = await voting.getPastPoll(slot); + expect(pastPoll).to.eq.BN(WAD); // USER0 had 1 WAD of reputation }); }); From d0b589a3e4bdd1ac4335f25aeaae39abca9f3599 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Fri, 12 Jun 2020 11:45:55 -0700 Subject: [PATCH 16/61] Extend claimDelay for expenditure state polls --- contracts/colony/ColonyDataTypes.sol | 1 + contracts/colony/ColonyExpenditure.sol | 18 +- contracts/extensions/VotingReputation.sol | 152 ++++++++++-- helpers/test-helper.js | 4 + test/contracts-network/colony-expenditure.js | 6 +- test/extensions/voting-rep.js | 242 +++++++++++++++++-- 6 files changed, 379 insertions(+), 44 deletions(-) diff --git a/contracts/colony/ColonyDataTypes.sol b/contracts/colony/ColonyDataTypes.sol index b442028379..4c41747bb9 100755 --- a/contracts/colony/ColonyDataTypes.sol +++ b/contracts/colony/ColonyDataTypes.sol @@ -210,6 +210,7 @@ contract ColonyDataTypes { uint256 fundingPotId; uint256 domainId; uint256 finalizedTimestamp; + uint256 globalClaimDelay; } struct ExpenditureSlot { diff --git a/contracts/colony/ColonyExpenditure.sol b/contracts/colony/ColonyExpenditure.sol index 915bac783b..6f60645584 100644 --- a/contracts/colony/ColonyExpenditure.sol +++ b/contracts/colony/ColonyExpenditure.sol @@ -47,7 +47,8 @@ contract ColonyExpenditure is ColonyStorage { owner: msg.sender, fundingPotId: fundingPotCount, domainId: _domainId, - finalizedTimestamp: 0 + finalizedTimestamp: 0, + globalClaimDelay: 0 }); emit FundingPotAdded(fundingPotCount); @@ -183,7 +184,7 @@ contract ColonyExpenditure is ColonyStorage { uint256 _permissionDomainId, uint256 _childSkillIndex, uint256 _id, - uint256 _slot, + uint256 _storageSlot, bool[] memory _mask, bytes32[] memory _keys, bytes32 _value @@ -196,20 +197,21 @@ contract ColonyExpenditure is ColonyStorage { require(_keys.length > 0, "colony-expenditure-no-keys"); require( - _slot == EXPENDITURES_SLOT || - _slot == EXPENDITURESLOTS_SLOT || - _slot == EXPENDITURESLOTPAYOUTS_SLOT, + _storageSlot == EXPENDITURES_SLOT || + _storageSlot == EXPENDITURESLOTS_SLOT || + _storageSlot == EXPENDITURESLOTPAYOUTS_SLOT, "colony-expenditure-bad-slot" ); // Only allow editing expenditure status, owner, and finalizedTimestamp // Note that status + owner occupy one slot - if (_slot == EXPENDITURES_SLOT) { - uint256 offset = uint256(_keys[_keys.length - 1]); + if (_storageSlot == EXPENDITURES_SLOT) { + uint256 offset = uint256(_keys[0]); + require(_keys.length == 1, "colony-expenditure-bad-keys"); require(offset == 0 || offset == 3, "colony-expenditure-bad-offset"); } - executeStateChange(keccak256(abi.encode(_id, _slot)), _mask, _keys, _value); + executeStateChange(keccak256(abi.encode(_id, _storageSlot)), _mask, _keys, _value); } // Public view functions diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index efea592bff..4fe66bfdea 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -88,8 +88,9 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { mapping (address => mapping (uint256 => bytes32)) voteSecrets; - mapping (bytes32 => uint256) pastPolls; - mapping (bytes32 => uint256) activePolls; + mapping (bytes32 => uint256) pastPolls; // action signature => voting power + mapping (bytes32 => uint256) activePolls; // action signature => pollId + mapping (bytes32 => uint256) expenditurePollCounts; // expenditure signature => count // Public functions (interface) @@ -144,6 +145,12 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { { Poll storage poll = polls[_pollId]; + require( + activePolls[hashAction(poll.action)] == 0 || + activePolls[hashAction(poll.action)] == _pollId, + "voting-rep-competing-poll-exists" + ); + uint256 stakerRep = checkReputation(_pollId, msg.sender, _key, _value, _branchMask, _siblings); // TODO: can we keep the domainId on the poll somewhere? This seems like a wasteful external call. @@ -162,6 +169,20 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { poll.stakes[toInt(_vote)] = add(poll.stakes[toInt(_vote)], _amount); stakes[_pollId][msg.sender][_vote] = add(stakes[_pollId][msg.sender][_vote], _amount); + // Activate poll to prevent competing polls from being activated + if (poll.stakes[YAY] == getRequiredStake(_pollId) && toInt(_vote) == YAY) { + activePolls[hashAction(poll.action)] = _pollId; + + // Increment counter & extend claim delay if staking for an expenditure state change + if (getSig(poll.action) == CHANGE_FUNCTION) { + bytes32 expenditureHash = hashExpenditureAction(poll.action); + expenditurePollCounts[expenditureHash] = add(expenditurePollCounts[expenditureHash], 1); + + bytes memory claimDelayAction = createClaimDelayAction(poll.action, UINT256_MAX); + executeCall(address(colony), claimDelayAction); + } + } + // Update timestamp if fully staked if ( poll.stakes[YAY] == getRequiredStake(_pollId) && @@ -246,7 +267,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { function executePoll(uint256 _pollId) public { Poll storage poll = polls[_pollId]; PollState pollState = getPollState(_pollId); - bytes32 slot = encodeSlot(poll.action); + bytes32 actionHash = hashAction(poll.action); require(pollState != PollState.Failed, "voting-rep-poll-failed"); require(pollState != PollState.Closed, "voting-rep-poll-escalation-window-open"); @@ -254,7 +275,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { require(pollState == PollState.Executable, "voting-rep-poll-not-executable"); poll.executed = true; - delete activePolls[slot]; + delete activePolls[actionHash]; bool canExecute = ( poll.stakes[NAY] <= poll.stakes[YAY] && @@ -262,6 +283,14 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { ); if (getSig(poll.action) == CHANGE_FUNCTION) { + bytes32 expenditureHash = hashExpenditureAction(poll.action); + expenditurePollCounts[expenditureHash] = sub(expenditurePollCounts[expenditureHash], 1); + + // Release the claimDelay if this is the last active poll + if (expenditurePollCounts[expenditureHash] == 0) { + bytes memory claimDelayAction = createClaimDelayAction(poll.action, 0); + executeCall(address(colony), claimDelayAction); + } // Conditions: // - Yay side staked and nay side did not, and doman has sufficient vote power @@ -274,8 +303,8 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { votePower = poll.votes[YAY]; } - if (pastPolls[slot] < votePower) { - pastPolls[slot] = votePower; + if (pastPolls[actionHash] < votePower) { + pastPolls[actionHash] = votePower; canExecute = canExecute && true; } else { canExecute = canExecute && false; @@ -369,6 +398,14 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { poll = polls[_pollId]; } + function getActivePoll(bytes32 _actionHash) public view returns (uint256) { + return activePolls[_actionHash]; + } + + function getExpenditurePollCount(bytes32 _expenditureHash) public view returns (uint256) { + return expenditurePollCounts[_expenditureHash]; + } + function getStake(uint256 _pollId, address _staker, bool _vote) public view returns (uint256) { return stakes[_pollId][_staker][_vote]; } @@ -425,8 +462,6 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { ) internal { - require(activePolls[encodeSlot(_action)] == 0, "voting-rep-already-active"); - pollCount += 1; polls[pollCount].lastEvent = now; polls[pollCount].rootHash = colonyNetwork.getReputationRootHash(); @@ -434,8 +469,6 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { polls[pollCount].skillRep = checkReputation(pollCount, address(0x0), _key, _value, _branchMask, _siblings); polls[pollCount].action = _action; - activePolls[encodeSlot(_action)] = pollCount; - emit PollCreated(pollCount, _skillId); } @@ -501,7 +534,9 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { } } - function executeCall(address to, bytes memory action) internal returns (bool success) { + function executeCall(address to, bytes memory action) internal { + 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)) @@ -510,6 +545,8 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { // call(g, a, v, in, insize, out, outsize) success := call(gas, to, 0, add(action, 0x20), mload(action), 0, 0) } + + // require(success, "voting-rep-call-failed"); } function getSig(bytes memory action) internal returns (bytes4 sig) { @@ -518,21 +555,108 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { } } - function encodeSlot(bytes memory action) internal returns (bytes32 slot) { + function hashAction(bytes memory action) internal returns (bytes32 hash) { if (getSig(action) == CHANGE_FUNCTION) { assembly { // Hash all but last (value) bytes32 // Recall: mload(action) gives length of bytes array // So skip past the first bytes32 (length), and the last bytes32 (value) - slot := keccak256(add(action, 0x20), sub(mload(action), 0x20)) + hash := keccak256(add(action, 0x20), sub(mload(action), 0x20)) } } else { assembly { // Hash entire action // Recall: mload(action) gives length of bytes array // So skip past the first bytes32 (length) - slot := keccak256(add(action, 0x20), mload(action)) + hash := keccak256(add(action, 0x20), mload(action)) } } } + + function hashExpenditureAction(bytes memory action) internal returns (bytes32 hash) { + require(getSig(action) == CHANGE_FUNCTION, "voting-rep-bad-sig"); + + uint256 expenditureId; + uint256 storageSlot; + uint256 expenditureSlot; + + assembly { + expenditureId := mload(add(action, 0x64)) + storageSlot := mload(add(action, 0x84)) + expenditureSlot := mload(add(action, 0x184)) + } + + if (storageSlot == 25) { + hash = keccak256(abi.encodePacked(expenditureId)); + } else { + hash = keccak256(abi.encodePacked(expenditureId, expenditureSlot)); + } + } + + function createClaimDelayAction(bytes memory action, uint256 value) + public + returns (bytes memory) + { + bytes32 functionSignature; + uint256 permissionDomainId; + uint256 childSkillIndex; + uint256 expenditureId; + uint256 storageSlot; + + assembly { + functionSignature := mload(add(action, 0x20)) + permissionDomainId := mload(add(action, 0x24)) + childSkillIndex := mload(add(action, 0x44)) + expenditureId := mload(add(action, 0x64)) + storageSlot := mload(add(action, 0x84)) + } + + // If we are editing the main expenditure struct + if (storageSlot == 25) { + + bytes memory claimDelayAction = new bytes(4 + 32 * 11); // 356 bytes + assembly { + mstore(add(claimDelayAction, 0x20), functionSignature) + mstore(add(claimDelayAction, 0x24), permissionDomainId) + mstore(add(claimDelayAction, 0x44), childSkillIndex) + mstore(add(claimDelayAction, 0x64), expenditureId) + mstore(add(claimDelayAction, 0x84), 25) // expenditure storage slot + mstore(add(claimDelayAction, 0xa4), 0xe0) // mask location + mstore(add(claimDelayAction, 0xc4), 0x120) // keys location + mstore(add(claimDelayAction, 0xe4), value) + mstore(add(claimDelayAction, 0x104), 1) // mask length + mstore(add(claimDelayAction, 0x124), 1) // offset + mstore(add(claimDelayAction, 0x144), 1) // keys length + mstore(add(claimDelayAction, 0x164), 4) // globalClaimDelay offset + } + return claimDelayAction; + + // If we are editing an expenditure slot + } else { + + bytes memory claimDelayAction = new bytes(4 + 32 * 13); // 420 bytes + uint256 expenditureSlot; + + assembly { + expenditureSlot := mload(add(action, 0x184)) + + mstore(add(claimDelayAction, 0x20), functionSignature) + mstore(add(claimDelayAction, 0x24), permissionDomainId) + mstore(add(claimDelayAction, 0x44), childSkillIndex) + mstore(add(claimDelayAction, 0x64), expenditureId) + mstore(add(claimDelayAction, 0x84), 26) // expenditureSlot storage slot + mstore(add(claimDelayAction, 0xa4), 0xe0) // mask location + mstore(add(claimDelayAction, 0xc4), 0x140) // keys location + mstore(add(claimDelayAction, 0xe4), value) + mstore(add(claimDelayAction, 0x104), 2) // mask length + mstore(add(claimDelayAction, 0x124), 0) // mapping + mstore(add(claimDelayAction, 0x144), 1) // offset + mstore(add(claimDelayAction, 0x164), 2) // keys length + mstore(add(claimDelayAction, 0x184), expenditureSlot) + mstore(add(claimDelayAction, 0x1a4), 1) // claimDelay offset + } + return claimDelayAction; + + } + } } diff --git a/helpers/test-helper.js b/helpers/test-helper.js index a463f9166b..3ccca4001c 100644 --- a/helpers/test-helper.js +++ b/helpers/test-helper.js @@ -879,3 +879,7 @@ export async function getRewardClaimSquareRootsAndProofs(client, tokenLocking, c return { squareRoots, userProof }; } + +export function bn2bytes32(x, size = 64) { + return `0x${x.toString(16, size)}`; +} diff --git a/test/contracts-network/colony-expenditure.js b/test/contracts-network/colony-expenditure.js index 505aaaebdd..f56ac8165d 100644 --- a/test/contracts-network/colony-expenditure.js +++ b/test/contracts-network/colony-expenditure.js @@ -4,7 +4,7 @@ import bnChai from "bn-chai"; import { BN } from "bn.js"; import { UINT256_MAX, INT128_MAX, WAD, SECONDS_PER_DAY, MAX_PAYOUT, GLOBAL_SKILL_ID } from "../../helpers/constants"; -import { checkErrorRevert, getTokenArgs, forwardTime, getBlockTime } from "../../helpers/test-helper"; +import { checkErrorRevert, getTokenArgs, forwardTime, getBlockTime, bn2bytes32 } from "../../helpers/test-helper"; import { fundColonyWithTokens, setupRandomColony } from "../../helpers/test-data-generator"; const { expect } = chai; @@ -555,10 +555,6 @@ contract("Colony Expenditure", (accounts) => { const MAPPING = false; const OFFSET = true; - function bn2bytes32(x, size = 64) { - return `0x${x.toString(16, size)}`; - } - beforeEach(async () => { await colony.makeExpenditure(1, UINT256_MAX, 1, { from: ADMIN }); expenditureId = await colony.getExpenditureCount(); diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index f84f766ef0..81793aa52f 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -8,7 +8,15 @@ import { ethers } from "ethers"; import { soliditySha3 } from "web3-utils"; import { UINT256_MAX, WAD, MINING_CYCLE_DURATION, SECONDS_PER_DAY, DEFAULT_STAKE } from "../../helpers/constants"; -import { checkErrorRevert, makeReputationKey, makeReputationValue, getActiveRepCycle, forwardTime, encodeTxData } from "../../helpers/test-helper"; +import { + checkErrorRevert, + makeReputationKey, + makeReputationValue, + getActiveRepCycle, + forwardTime, + encodeTxData, + bn2bytes32, +} from "../../helpers/test-helper"; import { setupColonyNetwork, @@ -181,7 +189,7 @@ contract("Voting Reputation", (accounts) => { await repCycle.confirmNewHash(0); }); - describe.only("using the extension factory", async () => { + describe("using the extension factory", async () => { it("can install the extension factory once if root and uninstall", async () => { ({ colony } = await setupRandomColony(colonyNetwork)); await checkErrorRevert(votingFactory.deployExtension(colony.address, { from: USER1 }), "colony-extension-user-not-root"); @@ -191,7 +199,7 @@ contract("Voting Reputation", (accounts) => { }); }); - describe.only("creating polls", async () => { + describe("creating polls", async () => { it("can create a root poll", async () => { const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); await voting.createRootPoll(action, domain1Key, domain1Value, domain1Mask, domain1Siblings); @@ -225,13 +233,6 @@ contract("Voting Reputation", (accounts) => { expect(poll.skillId).to.eq.BN(domain2.skillId); }); - it("cannot create a poll for an action if there is an active poll for the same action", async () => { - const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); - await voting.createRootPoll(action, domain1Key, domain1Value, domain1Mask, domain1Siblings); - - await checkErrorRevert(voting.createRootPoll(action, domain1Key, domain1Value, domain1Mask, domain1Siblings), "voting-rep-already-active"); - }); - it("can externally escalate a domain poll", async () => { // Create poll in parent domain (1) of action (2) const action = await encodeTxData(colony, "makeTask", [1, 0, FAKE, 2, 0, 0]); @@ -253,7 +254,7 @@ contract("Voting Reputation", (accounts) => { }); }); - describe.only("staking on polls", async () => { + describe("staking on polls", async () => { let pollId; beforeEach(async () => { @@ -276,7 +277,7 @@ contract("Voting Reputation", (accounts) => { expect(stake1).to.eq.BN(100); }); - it("updates the poll states correctly", async () => { + it("can update the poll states correctly", async () => { let pollState = await voting.getPollState(pollId); expect(pollState).to.eq.BN(STAKING); @@ -289,6 +290,213 @@ contract("Voting Reputation", (accounts) => { expect(pollState).to.eq.BN(OPEN); }); + it("cannot stake for an action while there is an active poll for the same action", async () => { + let activePoll; + + const { action } = await voting.getPoll(pollId); + activePoll = await voting.getActivePoll(soliditySha3(action)); + expect(activePoll).to.be.zero; + + await forwardTime(STAKE_WINDOW / 2, this); + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + + activePoll = await voting.getActivePoll(soliditySha3(action)); + expect(activePoll).to.eq.BN(pollId); + + await voting.createRootPoll(action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + const otherPollId = await voting.getPollCount(); + + await checkErrorRevert( + voting.stakePoll(otherPollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), + "voting-rep-competing-poll-exists" + ); + + // But, can stake once the first poll is executed. + await forwardTime(STAKE_WINDOW / 2, this); + await voting.executePoll(pollId); + + activePoll = await voting.getActivePoll(soliditySha3(action)); + expect(activePoll).to.be.zero; + + await voting.stakePoll(otherPollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + + activePoll = await voting.getActivePoll(soliditySha3(action)); + expect(activePoll).to.eq.BN(otherPollId); + }); + + it("can update the expenditure globalClaimDelay if voting on expenditure state", async () => { + await colony.makeExpenditure(1, UINT256_MAX, 1); + const expenditureId = await colony.getExpenditureCount(); + + // Set payoutModifier to 1 for expenditure slot 0 + const action = await encodeTxData(colony, "setExpenditureState", [ + 1, + UINT256_MAX, + expenditureId, + 25, + [true], + [bn2bytes32(new BN(3))], + bn2bytes32(WAD), + ]); + + await voting.createDomainPoll(1, UINT256_MAX, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + pollId = await voting.getPollCount(); + + let expenditurePollCount; + expenditurePollCount = await voting.getExpenditurePollCount(soliditySha3(expenditureId)); + expect(expenditurePollCount).to.be.zero; + + let expenditureSlot; + expenditureSlot = await colony.getExpenditure(expenditureId); + expect(expenditureSlot.globalClaimDelay).to.be.zero; + + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + + expenditurePollCount = await voting.getExpenditurePollCount(soliditySha3(expenditureId)); + expect(expenditurePollCount).to.eq.BN(1); + + expenditureSlot = await colony.getExpenditure(expenditureId); + expect(expenditureSlot.globalClaimDelay).to.eq.BN(UINT256_MAX); + }); + + it("can update the expenditure slot claimDelay if voting on expenditure slot state", async () => { + await colony.makeExpenditure(1, UINT256_MAX, 1); + const expenditureId = await colony.getExpenditureCount(); + + // Set payoutModifier to 1 for expenditure slot 0 + const action = await encodeTxData(colony, "setExpenditureState", [ + 1, + UINT256_MAX, + expenditureId, + 26, + [false, true], + ["0x0", bn2bytes32(new BN(2))], + bn2bytes32(WAD), + ]); + + await voting.createDomainPoll(1, UINT256_MAX, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + pollId = await voting.getPollCount(); + + let expenditurePollCount; + expenditurePollCount = await voting.getExpenditurePollCount(soliditySha3(expenditureId, 0)); + expect(expenditurePollCount).to.be.zero; + + let expenditureSlot; + expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); + expect(expenditureSlot.claimDelay).to.be.zero; + + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + + expenditurePollCount = await voting.getExpenditurePollCount(soliditySha3(expenditureId, 0)); + expect(expenditurePollCount).to.eq.BN(1); + + expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); + expect(expenditureSlot.claimDelay).to.eq.BN(UINT256_MAX); + }); + + it("can update the expenditure slot claimDelay if voting on expenditure payout state", async () => { + await colony.makeExpenditure(1, UINT256_MAX, 1); + const expenditureId = await colony.getExpenditureCount(); + + // Set payout to WAD for expenditure slot 0, internal token + const action = await encodeTxData(colony, "setExpenditureState", [ + 1, + UINT256_MAX, + expenditureId, + 27, + [false, false], + ["0x0", bn2bytes32(new BN(token.address.slice(2), 16))], + bn2bytes32(WAD), + ]); + + await voting.createDomainPoll(1, UINT256_MAX, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + pollId = await voting.getPollCount(); + + let expenditurePollCount; + expenditurePollCount = await voting.getExpenditurePollCount(soliditySha3(expenditureId, 0)); + expect(expenditurePollCount).to.be.zero; + + let expenditureSlot; + expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); + expect(expenditureSlot.claimDelay).to.be.zero; + + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + + expenditurePollCount = await voting.getExpenditurePollCount(soliditySha3(expenditureId, 0)); + expect(expenditurePollCount).to.eq.BN(1); + + expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); + expect(expenditureSlot.claimDelay).to.eq.BN(UINT256_MAX); + }); + + it("can accurately track the number of polls for a single expenditure", async () => { + await colony.makeExpenditure(1, UINT256_MAX, 1); + const expenditureId = await colony.getExpenditureCount(); + const expenditureHash = soliditySha3(expenditureId, 0); + + // Set payoutModifier to 1 for expenditure slot 0 + const action1 = await encodeTxData(colony, "setExpenditureState", [ + 1, + UINT256_MAX, + expenditureId, + 26, + [false, true], + ["0x0", bn2bytes32(new BN(2))], + bn2bytes32(WAD), + ]); + + // Set payout to WAD for expenditure slot 0, internal token + const action2 = await encodeTxData(colony, "setExpenditureState", [ + 1, + UINT256_MAX, + expenditureId, + 27, + [false, false], + ["0x0", bn2bytes32(new BN(token.address.slice(2), 16))], + bn2bytes32(WAD), + ]); + + await voting.createDomainPoll(1, UINT256_MAX, action1, domain1Key, domain1Value, domain1Mask, domain1Siblings); + const pollId1 = await voting.getPollCount(); + + await voting.createDomainPoll(1, UINT256_MAX, action2, domain1Key, domain1Value, domain1Mask, domain1Siblings); + const pollId2 = await voting.getPollCount(); + + let expenditurePollCount; + expenditurePollCount = await voting.getExpenditurePollCount(expenditureHash); + expect(expenditurePollCount).to.be.zero; + + let expenditureSlot; + expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); + expect(expenditureSlot.claimDelay).to.be.zero; + + await voting.stakePoll(pollId1, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId2, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + + expenditurePollCount = await voting.getExpenditurePollCount(expenditureHash); + expect(expenditurePollCount).to.eq.BN(2); + + expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); + expect(expenditureSlot.claimDelay).to.eq.BN(UINT256_MAX); + + await forwardTime(STAKE_WINDOW, this); + await voting.executePoll(pollId1); + + expenditurePollCount = await voting.getExpenditurePollCount(expenditureHash); + expect(expenditurePollCount).to.eq.BN(1); + + expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); + expect(expenditureSlot.claimDelay).to.eq.BN(UINT256_MAX); + + await voting.executePoll(pollId2); + + expenditurePollCount = await voting.getExpenditurePollCount(expenditureHash); + expect(expenditurePollCount).to.be.zero; + + expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); + expect(expenditureSlot.claimDelay).to.be.zero; + }); + it("cannot stake with someone else's reputation", async () => { await checkErrorRevert( voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER1 }), @@ -336,7 +544,7 @@ contract("Voting Reputation", (accounts) => { }); }); - describe.only("voting on polls", async () => { + describe("voting on polls", async () => { let pollId; beforeEach(async () => { @@ -371,7 +579,7 @@ contract("Voting Reputation", (accounts) => { expect(votes[1]).to.eq.BN(WAD.muln(2)); }); - it("can update votes, but only last one counts", async () => { + it("can update votes, but just the last one counts", async () => { await voting.submitVote(pollId, soliditySha3(SALT, false), { from: USER0 }); await voting.submitVote(pollId, soliditySha3(SALT, true), { from: USER0 }); @@ -498,7 +706,7 @@ contract("Voting Reputation", (accounts) => { }); }); - describe.only("executing polls", async () => { + describe("executing polls", async () => { let pollId; beforeEach(async () => { @@ -660,7 +868,7 @@ contract("Voting Reputation", (accounts) => { }); }); - describe.only("claiming rewards", async () => { + describe("claiming rewards", async () => { let pollId; beforeEach(async () => { @@ -781,7 +989,7 @@ contract("Voting Reputation", (accounts) => { }); }); - describe.only("escalating polls", async () => { + describe("escalating polls", async () => { let pollId; beforeEach(async () => { From 679137c717650380cdd1fba665d333639d634f61 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Wed, 17 Jun 2020 14:43:10 -0700 Subject: [PATCH 17/61] Add fold & call --- contracts/extensions/VotingReputation.sol | 96 ++++++++++--- test/extensions/voting-rep.js | 162 +++++++++++++++++++++- 2 files changed, 232 insertions(+), 26 deletions(-) diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index 4fe66bfdea..796ad38c01 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -68,15 +68,18 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { // Data structures enum PollState { Staking, Voting, Reveal, Closed, Executable, Executed, Failed } + enum Response { None, Fold, Call } struct Poll { - uint256 lastEvent; // Set at creation / escalation & when fully staked + uint256 lastEvent; // Set at creation / escalation & when staked + responded bytes32 rootHash; uint256 skillId; uint256 skillRep; uint256 unpaidRewards; uint256[2] stakes; // [nay, yay] uint256[2] votes; // [nay, yay] + address[2] leads; // [nay, yay] + Response[2] responses; // [nay, yay] bytes action; bool executed; } @@ -155,7 +158,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { // TODO: can we keep the domainId on the poll somewhere? This seems like a wasteful external call. // But if it's 10 external calls per word of storage, then < 10 stakers makes this cheaper. - require(colony.getDomain(_domainId).skillId == poll.skillId, "voting-rep-bad-stake-domain"); + require(colony.getDomain(_domainId).skillId == poll.skillId, "voting-rep-bad-domain-id"); require(add(stakes[_pollId][msg.sender][_vote], _amount) <= stakerRep, "voting-rep-insufficient-rep"); require(add(poll.stakes[toInt(_vote)], _amount) <= getRequiredStake(_pollId), "voting-rep-stake-too-large"); @@ -169,6 +172,11 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { poll.stakes[toInt(_vote)] = add(poll.stakes[toInt(_vote)], _amount); stakes[_pollId][msg.sender][_vote] = add(stakes[_pollId][msg.sender][_vote], _amount); + // Update the lead if the stake is larger + if (stakes[_pollId][poll.leads[toInt(_vote)]][_vote] < _amount) { + poll.leads[toInt(_vote)] = msg.sender; + } + // Activate poll to prevent competing polls from being activated if (poll.stakes[YAY] == getRequiredStake(_pollId) && toInt(_vote) == YAY) { activePolls[hashAction(poll.action)] = _pollId; @@ -183,18 +191,30 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { } } - // Update timestamp if fully staked - if ( - poll.stakes[YAY] == getRequiredStake(_pollId) && - poll.stakes[NAY] == getRequiredStake(_pollId) - ) { - poll.lastEvent = now; + // Claim tokens if fully staked + if (poll.stakes[YAY] == getRequiredStake(_pollId) && poll.stakes[NAY] == getRequiredStake(_pollId)) { tokenLocking.claim(token, true); } emit PollStaked(_pollId, msg.sender, _vote, _amount); } + function respondToStake(uint256 _pollId, bool _vote, Response _response) public { + Poll storage poll = polls[_pollId]; + uint256 requiredStake = getRequiredStake(_pollId); + + require(poll.stakes[YAY] == requiredStake && poll.stakes[NAY] == requiredStake, "voting-rep-not-fully-staked"); + require(getPollState(_pollId) == PollState.Staking, "voting-rep-not-staking"); + require(poll.leads[toInt(_vote)] == msg.sender, "voting-rep-not-lead"); + require(poll.responses[toInt(_vote)] == Response.None, "voting-rep-already-responded"); + + poll.responses[toInt(_vote)] = _response; + + if (poll.responses[YAY] != Response.None && poll.responses[NAY] != Response.None) { + poll.lastEvent = now; + } + } + function submitVote(uint256 _pollId, bytes32 _voteSecret) public { require(getPollState(_pollId) == PollState.Voting, "voting-rep-poll-not-open"); voteSecrets[msg.sender][_pollId] = _voteSecret; @@ -262,6 +282,8 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { poll.lastEvent = now; poll.skillId = newDomainSkillId; poll.skillRep = checkReputation(_pollId, address(0x0), _key, _value, _branchMask, _siblings); + + delete poll.responses; } function executePoll(uint256 _pollId) public { @@ -329,11 +351,15 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { public { Poll storage poll = polls[_pollId]; - require(getPollState(_pollId) == PollState.Executed, "voting-rep-not-executed"); + require( + getPollState(_pollId) == PollState.Executed || + getPollState(_pollId) == PollState.Failed, + "voting-rep-not-failed-or-executed" + ); // TODO: can we keep the domainId on the poll somewhere? This seems like a wasteful external call. // But if it's 10 external calls per word of storage, then < 10 stakers makes this cheaper. - require(colony.getDomain(_domainId).skillId == poll.skillId, "voting-rep-bad-stake-domain"); + require(colony.getDomain(_domainId).skillId == poll.skillId, "voting-rep-bad-domain-id"); // Calculate how much of the stake is left after voter compensation (>= 90%) uint256 stake = stakes[_pollId][_user][_vote]; @@ -422,12 +448,15 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { if (poll.executed) { return PollState.Executed; - // Not fully staked, not (yet) going to a vote - } else if (poll.stakes[YAY] < requiredStake || poll.stakes[NAY] < requiredStake) { + // Not fully staked + } else if ( + poll.stakes[YAY] < requiredStake || + poll.stakes[NAY] < requiredStake + ) { // Are we still staking? if (now < poll.lastEvent + STAKE_PERIOD) { return PollState.Staking; - // If not, did the YAY side reach a full stake? + // If not, did the YAY side stake? } else if (poll.stakes[YAY] == requiredStake) { return PollState.Executable; // If not, was there a prior vote we can fall back on? @@ -438,15 +467,38 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { return PollState.Failed; } - // Fully staked, going to a vote - } else if (now < poll.lastEvent + VOTE_PERIOD) { - return PollState.Voting; - } else if (now < poll.lastEvent + (VOTE_PERIOD + REVEAL_PERIOD)) { - return PollState.Reveal; - } else if (now < poll.lastEvent + (VOTE_PERIOD + REVEAL_PERIOD + STAKE_PERIOD)) { - return PollState.Closed; - } else { + // Fully staked, check for any folds + } else if (poll.responses[YAY] == Response.Fold) { + return PollState.Failed; + } else if (poll.responses[NAY] == Response.Fold) { return PollState.Executable; + + // Do we need to keep waiting? + } else if ( + now < poll.lastEvent + STAKE_PERIOD && + (poll.responses[YAY] == Response.None || + poll.responses[NAY] == Response.None) + ) { + return PollState.Staking; + + // Fully staked, no folds, go to a vote + } else { + + // Infer the right timestamp (since only updated if both parties respond) + uint256 lastEvent = poll.lastEvent + (( + poll.responses[YAY] == Response.None || + poll.responses[YAY] == Response.None + ) ? STAKE_PERIOD : 0); + + if (now < lastEvent + VOTE_PERIOD) { + return PollState.Voting; + } else if (now < lastEvent + (VOTE_PERIOD + REVEAL_PERIOD)) { + return PollState.Reveal; + } else if (now < lastEvent + (VOTE_PERIOD + REVEAL_PERIOD + STAKE_PERIOD)) { + return PollState.Closed; + } else { + return PollState.Executable; + } } } @@ -574,7 +626,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { } function hashExpenditureAction(bytes memory action) internal returns (bytes32 hash) { - require(getSig(action) == CHANGE_FUNCTION, "voting-rep-bad-sig"); + assert(getSig(action) == CHANGE_FUNCTION); uint256 expenditureId; uint256 storageSlot; diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index 81793aa52f..b6d0cd4da3 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -78,10 +78,15 @@ contract("Voting Reputation", (accounts) => { const FAKE = soliditySha3(shortid.generate()); const STAKING = 0; - const OPEN = 1; + const VOTING = 1; // const REVEAL = 2; // const CLOSED = 3; - // const EXECUTED = 4; + const EXECUTABLE = 4; + // const EXECUTED = 5; + const FAILED = 6; + + const FOLD = 1; + const CALL = 2; const REQUIRED_STAKE = WAD.muln(3).divn(1000); @@ -287,7 +292,74 @@ contract("Voting Reputation", (accounts) => { await voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); pollState = await voting.getPollState(pollId); - expect(pollState).to.eq.BN(OPEN); + expect(pollState).to.eq.BN(STAKING); + + await voting.respondToStake(pollId, true, CALL, { from: USER0 }); + pollState = await voting.getPollState(pollId); + expect(pollState).to.eq.BN(STAKING); + + await voting.respondToStake(pollId, false, CALL, { from: USER1 }); + pollState = await voting.getPollState(pollId); + expect(pollState).to.eq.BN(VOTING); + }); + + it("can go to a vote even if both sides do not call", async () => { + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + + await forwardTime(STAKE_WINDOW, this); + + const pollState = await voting.getPollState(pollId); + expect(pollState).to.eq.BN(VOTING); + }); + + it("cannot execute if the YAY side stakes and folds", async () => { + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + + await voting.respondToStake(pollId, true, FOLD, { from: USER0 }); + + const pollState = await voting.getPollState(pollId); + expect(pollState).to.eq.BN(FAILED); + }); + + it("can execute if the NAY side stakes and folds", async () => { + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + + await voting.respondToStake(pollId, true, FOLD, { from: USER1 }); + + const pollState = await voting.getPollState(pollId); + expect(pollState).to.eq.BN(EXECUTABLE); + }); + + it("cannot respond to a stake if not fully staked", async () => { + await checkErrorRevert(voting.respondToStake(pollId, true, CALL, { from: USER0 }), "voting-rep-not-fully-staked"); + }); + + it("cannot respond to a stake if not in the staking phase", async () => { + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + + await forwardTime(STAKE_WINDOW, this); + + await checkErrorRevert(voting.respondToStake(pollId, true, CALL, { from: USER0 }), "voting-rep-not-staking"); + }); + + it("cannot respond to a stake if not the lead", async () => { + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + + await checkErrorRevert(voting.respondToStake(pollId, true, CALL, { from: USER2 }), "voting-rep-not-lead"); + }); + + it("cannot respond to a stake twice", async () => { + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + + await voting.respondToStake(pollId, true, CALL, { from: USER0 }); + + await checkErrorRevert(voting.respondToStake(pollId, true, CALL, { from: USER0 }), "voting-rep-already-responded"); }); it("cannot stake for an action while there is an active poll for the same action", async () => { @@ -525,7 +597,7 @@ contract("Voting Reputation", (accounts) => { it("cannot stake with an invalid domainId", async () => { await checkErrorRevert( voting.stakePoll(pollId, 1, 0, 2, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), - "voting-rep-bad-stake-domain" + "voting-rep-bad-domain-id" ); }); @@ -554,6 +626,9 @@ contract("Voting Reputation", (accounts) => { await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + + await voting.respondToStake(pollId, true, CALL, { from: USER0 }); + await voting.respondToStake(pollId, false, CALL, { from: USER1 }); }); it("can rate and reveal for a poll", async () => { @@ -647,6 +722,9 @@ contract("Voting Reputation", (accounts) => { const pollId2 = await voting.getPollCount(); await voting.stakePoll(pollId2, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value2, user0Mask2, user0Siblings2, { from: USER0 }); await voting.stakePoll(pollId2, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask2, user1Siblings2, { from: USER1 }); + await voting.respondToStake(pollId2, true, CALL, { from: USER0 }); + await voting.respondToStake(pollId2, false, CALL, { from: USER1 }); + await voting.submitVote(pollId2, soliditySha3(SALT, false), { from: USER0 }); await forwardTime(VOTE_WINDOW, this); @@ -737,6 +815,23 @@ contract("Voting Reputation", (accounts) => { expect(logs[0].args.success).to.be.true; }); + it("cannot take an action during staking or voting", async () => { + let pollState; + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + + pollState = await voting.getPollState(pollId); + expect(pollState).to.eq.BN(STAKING); + await checkErrorRevert(voting.executePoll(pollId), "voting-rep-poll-not-executable"); + + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.respondToStake(pollId, true, CALL, { from: USER0 }); + await voting.respondToStake(pollId, false, CALL, { from: USER1 }); + + pollState = await voting.getPollState(pollId); + expect(pollState).to.eq.BN(VOTING); + await checkErrorRevert(voting.executePoll(pollId), "voting-rep-poll-not-executable"); + }); + it("cannot take an action twice", async () => { await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); @@ -751,6 +846,8 @@ contract("Voting Reputation", (accounts) => { it("can take an action if the poll passes", async () => { await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.respondToStake(pollId, true, CALL, { from: USER0 }); + await voting.respondToStake(pollId, false, CALL, { from: USER1 }); await voting.submitVote(pollId, soliditySha3(SALT, true), { from: USER0 }); @@ -771,6 +868,8 @@ contract("Voting Reputation", (accounts) => { it("cannot take an action if the poll fails", async () => { await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.respondToStake(pollId, true, CALL, { from: USER0 }); + await voting.respondToStake(pollId, false, CALL, { from: USER1 }); await voting.submitVote(pollId, soliditySha3(SALT, false), { from: USER0 }); @@ -795,6 +894,8 @@ contract("Voting Reputation", (accounts) => { await voting.stakePoll(pollId1, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await voting.stakePoll(pollId1, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.respondToStake(pollId1, true, CALL, { from: USER0 }); + await voting.respondToStake(pollId1, false, CALL, { from: USER1 }); await voting.submitVote(pollId1, soliditySha3(SALT, true), { from: USER0 }); @@ -814,6 +915,8 @@ contract("Voting Reputation", (accounts) => { await voting.stakePoll(pollId2, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await voting.stakePoll(pollId2, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.respondToStake(pollId2, true, CALL, { from: USER0 }); + await voting.respondToStake(pollId2, false, CALL, { from: USER1 }); await voting.submitVote(pollId2, soliditySha3(SALT, true), { from: USER0 }); @@ -852,6 +955,9 @@ contract("Voting Reputation", (accounts) => { await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.respondToStake(pollId, true, CALL, { from: USER0 }); + await voting.respondToStake(pollId, false, CALL, { from: USER1 }); + await voting.submitVote(pollId, soliditySha3(SALT, true), { from: USER0 }); await forwardTime(VOTE_WINDOW, this); @@ -924,6 +1030,9 @@ contract("Voting Reputation", (accounts) => { await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.respondToStake(pollId, true, CALL, { from: USER0 }); + await voting.respondToStake(pollId, false, CALL, { from: USER1 }); + await voting.submitVote(pollId, soliditySha3(SALT, true), { from: USER0 }); await voting.submitVote(pollId, soliditySha3(SALT, false), { from: USER1 }); @@ -962,10 +1071,39 @@ contract("Voting Reputation", (accounts) => { expect(repUpdate.amount).to.eq.BN(REQUIRED_STAKE.divn(3).neg()); }); + it("can let stakers claim their original stake if neither side fully staked", async () => { + const addr = await colonyNetwork.getReputationMiningCycle(false); + const repCycle = await IReputationMiningCycle.at(addr); + const numEntriesPrev = await repCycle.getReputationUpdateLogLength(); + + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, 100, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, 100, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + + await forwardTime(STAKE_WINDOW, this); + + const user0LockPre = await tokenLocking.getUserLock(token.address, USER0); + const user1LockPre = await tokenLocking.getUserLock(token.address, USER1); + + await voting.claimReward(pollId, 1, UINT256_MAX, 1, USER0, true); + await voting.claimReward(pollId, 1, UINT256_MAX, 1, USER1, false); + + const numEntriesPost = await repCycle.getReputationUpdateLogLength(); + + const user0LockPost = await tokenLocking.getUserLock(token.address, USER0); + const user1LockPost = await tokenLocking.getUserLock(token.address, USER1); + + expect(numEntriesPrev).to.eq.BN(numEntriesPost); + expect(new BN(user0LockPost.balance).sub(new BN(user0LockPre.balance))).to.eq.BN(100); + expect(new BN(user1LockPost.balance).sub(new BN(user1LockPre.balance))).to.eq.BN(100); + }); + it("cannot claim rewards twice", async () => { await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.respondToStake(pollId, true, CALL, { from: USER0 }); + await voting.respondToStake(pollId, false, CALL, { from: USER1 }); + await voting.submitVote(pollId, soliditySha3(SALT, true), { from: USER0 }); await forwardTime(VOTE_WINDOW, this); @@ -987,6 +1125,16 @@ contract("Voting Reputation", (accounts) => { it("cannot claim rewards before a poll is executed", async () => { await checkErrorRevert(voting.claimReward(pollId, 1, UINT256_MAX, 1, USER0, true), "voting-rep-not-executed"); }); + + it("cannot claim rewards with a bad domainId", async () => { + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + + await forwardTime(STAKE_WINDOW, this); + + await voting.executePoll(pollId); + + await checkErrorRevert(voting.claimReward(pollId, 2, UINT256_MAX, 2, USER0, true), "voting-rep-bad-domain-id"); + }); }); describe("escalating polls", async () => { @@ -1015,6 +1163,9 @@ contract("Voting Reputation", (accounts) => { await voting.stakePoll(pollId, 1, 0, 2, true, WAD.divn(1000), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await voting.stakePoll(pollId, 1, 0, 2, false, WAD.divn(1000), user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.respondToStake(pollId, true, CALL, { from: USER0 }); + await voting.respondToStake(pollId, false, CALL, { from: USER1 }); + // Note that this is a passing vote await voting.submitVote(pollId, soliditySha3(SALT, false), { from: USER0 }); await voting.submitVote(pollId, soliditySha3(SALT, true), { from: USER1 }); @@ -1130,6 +1281,9 @@ contract("Voting Reputation", (accounts) => { await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, remainingStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, remainingStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.respondToStake(pollId, true, CALL, { from: USER0 }); + await voting.respondToStake(pollId, false, CALL, { from: USER1 }); + // Make the vote fail this time (everyone votes against) await voting.submitVote(pollId, soliditySha3(SALT, false), { from: USER0 }); await voting.submitVote(pollId, soliditySha3(SALT, false), { from: USER1 }); From 8346a6f3a41131b3b40a182311241e06331c8688 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Tue, 23 Jun 2020 13:54:50 -0700 Subject: [PATCH 18/61] Add target --- contracts/extensions/VotingReputation.sol | 20 +++++-- docs/_Interface_IColonyNetwork.md | 20 +++---- test/extensions/voting-rep.js | 68 +++++++++++++++-------- 3 files changed, 69 insertions(+), 39 deletions(-) diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index 796ad38c01..4b509b01d3 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -80,6 +80,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { uint256[2] votes; // [nay, yay] address[2] leads; // [nay, yay] Response[2] responses; // [nay, yay] + address target; bytes action; bool executed; } @@ -98,6 +99,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { // Public functions (interface) function createRootPoll( + address _target, bytes memory _action, bytes memory _key, bytes memory _value, @@ -107,12 +109,13 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { public { uint256 rootSkillId = colony.getDomain(1).skillId; - createPoll(_action, rootSkillId, _key, _value, _branchMask, _siblings); + createPoll(_target, _action, rootSkillId, _key, _value, _branchMask, _siblings); } function createDomainPoll( uint256 _domainId, uint256 _childSkillIndex, + address _target, bytes memory _action, bytes memory _key, bytes memory _value, @@ -129,7 +132,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { require(childSkillId == actionDomainSkillId, "voting-rep-invalid-domain-id"); } - createPoll(_action, domainSkillId, _key, _value, _branchMask, _siblings); + createPoll(_target, _action, domainSkillId, _key, _value, _branchMask, _siblings); } function stakePoll( @@ -187,7 +190,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { expenditurePollCounts[expenditureHash] = add(expenditurePollCounts[expenditureHash], 1); bytes memory claimDelayAction = createClaimDelayAction(poll.action, UINT256_MAX); - executeCall(address(colony), claimDelayAction); + executeCall(_pollId, claimDelayAction); } } @@ -311,7 +314,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { // Release the claimDelay if this is the last active poll if (expenditurePollCounts[expenditureHash] == 0) { bytes memory claimDelayAction = createClaimDelayAction(poll.action, 0); - executeCall(address(colony), claimDelayAction); + executeCall(_pollId, claimDelayAction); } // Conditions: @@ -334,7 +337,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { } if (canExecute) { - executeCall(address(colony), poll.action); + executeCall(_pollId, poll.action); } emit PollExecuted(_pollId, poll.action, canExecute); @@ -505,6 +508,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { // Internal functions function createPoll( + address _target, bytes memory _action, uint256 _skillId, bytes memory _key, @@ -519,6 +523,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { polls[pollCount].rootHash = colonyNetwork.getReputationRootHash(); polls[pollCount].skillId = _skillId; polls[pollCount].skillRep = checkReputation(pollCount, address(0x0), _key, _value, _branchMask, _siblings); + polls[pollCount].target = _target; polls[pollCount].action = _action; emit PollCreated(pollCount, _skillId); @@ -586,7 +591,10 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { } } - function executeCall(address to, bytes memory action) internal { + function executeCall(uint256 pollId, bytes memory action) internal { + address target = polls[pollId].target; + address to = (target == address(0x0)) ? address(colony) : target; + bool success; assembly { diff --git a/docs/_Interface_IColonyNetwork.md b/docs/_Interface_IColonyNetwork.md index 8990fa3f13..e82352ab06 100644 --- a/docs/_Interface_IColonyNetwork.md +++ b/docs/_Interface_IColonyNetwork.md @@ -116,15 +116,19 @@ Used by a user to claim any mining rewards due to them. This will place them in ### `createColony` -Creates a new colony in the network, at version 3 +Overload of the simpler `createColony` -- creates a new colony in the network with a variety of options -*Note: This is now deprecated and will be removed in a future version* +*Note: For the colony to mint tokens, token ownership must be transferred to the new colony* **Parameters** |Name|Type|Description| |---|---|---| -|_tokenAddress|address|Address of an ERC20 token to serve as the colony token. +|_tokenAddress|address|Address of an ERC20 token to serve as the colony token +|_version|uint256|The version of colony to deploy (pass 0 for the current version) +|_colonyName|string|The label to register (if null, no label is registered) +|_orbitdb|string|The path of the orbitDB database associated with the user profile +|_useExtensionManager|bool|If true, give the ExtensionManager the root role in the colony **Return Parameters** @@ -134,19 +138,15 @@ Creates a new colony in the network, at version 3 ### `createColony` -Overload of the simpler `createColony` -- creates a new colony in the network with a variety of options +Creates a new colony in the network, at version 3 -*Note: For the colony to mint tokens, token ownership must be transferred to the new colony* +*Note: This is now deprecated and will be removed in a future version* **Parameters** |Name|Type|Description| |---|---|---| -|_tokenAddress|address|Address of an ERC20 token to serve as the colony token -|_version|uint256|The version of colony to deploy (pass 0 for the current version) -|_colonyName|string|The label to register (if null, no label is registered) -|_orbitdb|string|The path of the orbitDB database associated with the user profile -|_useExtensionManager|bool|If true, give the ExtensionManager the root role in the colony +|_tokenAddress|address|Address of an ERC20 token to serve as the colony token. **Return Parameters** diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index b6d0cd4da3..b4893cb28f 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -88,6 +88,7 @@ contract("Voting Reputation", (accounts) => { const FOLD = 1; const CALL = 2; + const ADDRESS_ZERO = ethers.constants.AddressZero; const REQUIRED_STAKE = WAD.muln(3).divn(1000); before(async () => { @@ -207,7 +208,7 @@ contract("Voting Reputation", (accounts) => { describe("creating polls", async () => { it("can create a root poll", async () => { const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); - await voting.createRootPoll(action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + await voting.createRootPoll(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); const pollId = await voting.getPollCount(); const poll = await voting.getPoll(pollId); @@ -217,7 +218,7 @@ contract("Voting Reputation", (accounts) => { it("can create a domain poll in the root domain", async () => { // Create poll in domain of action (1) const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); - await voting.createDomainPoll(1, UINT256_MAX, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + await voting.createDomainPoll(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); const pollId = await voting.getPollCount(); const poll = await voting.getPoll(pollId); @@ -231,7 +232,7 @@ contract("Voting Reputation", (accounts) => { // Create poll in domain of action (2) const action = await encodeTxData(colony, "makeTask", [1, 0, FAKE, 2, 0, 0]); - await voting.createDomainPoll(2, UINT256_MAX, action, key, value, mask, siblings); + await voting.createDomainPoll(2, UINT256_MAX, ADDRESS_ZERO, action, key, value, mask, siblings); const pollId = await voting.getPollCount(); const poll = await voting.getPoll(pollId); @@ -241,7 +242,7 @@ contract("Voting Reputation", (accounts) => { it("can externally escalate a domain poll", async () => { // Create poll in parent domain (1) of action (2) const action = await encodeTxData(colony, "makeTask", [1, 0, FAKE, 2, 0, 0]); - await voting.createDomainPoll(1, 0, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + await voting.createDomainPoll(1, 0, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); const pollId = await voting.getPollCount(); const poll = await voting.getPoll(pollId); @@ -255,7 +256,7 @@ contract("Voting Reputation", (accounts) => { // Provide proof for (3) instead of (2) const action = await encodeTxData(colony, "makeTask", [1, 0, FAKE, 2, 0, 0]); - await checkErrorRevert(voting.createDomainPoll(1, 1, action, key, value, mask, siblings), "voting-rep-invalid-domain-id"); + await checkErrorRevert(voting.createDomainPoll(1, 1, ADDRESS_ZERO, action, key, value, mask, siblings), "voting-rep-invalid-domain-id"); }); }); @@ -264,7 +265,7 @@ contract("Voting Reputation", (accounts) => { beforeEach(async () => { const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); - await voting.createRootPoll(action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + await voting.createRootPoll(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); pollId = await voting.getPollCount(); }); @@ -327,7 +328,7 @@ contract("Voting Reputation", (accounts) => { await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await voting.respondToStake(pollId, true, FOLD, { from: USER1 }); + await voting.respondToStake(pollId, false, FOLD, { from: USER1 }); const pollState = await voting.getPollState(pollId); expect(pollState).to.eq.BN(EXECUTABLE); @@ -375,7 +376,7 @@ contract("Voting Reputation", (accounts) => { activePoll = await voting.getActivePoll(soliditySha3(action)); expect(activePoll).to.eq.BN(pollId); - await voting.createRootPoll(action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + await voting.createRootPoll(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); const otherPollId = await voting.getPollCount(); await checkErrorRevert( @@ -411,7 +412,7 @@ contract("Voting Reputation", (accounts) => { bn2bytes32(WAD), ]); - await voting.createDomainPoll(1, UINT256_MAX, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + await voting.createDomainPoll(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); pollId = await voting.getPollCount(); let expenditurePollCount; @@ -446,7 +447,7 @@ contract("Voting Reputation", (accounts) => { bn2bytes32(WAD), ]); - await voting.createDomainPoll(1, UINT256_MAX, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + await voting.createDomainPoll(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); pollId = await voting.getPollCount(); let expenditurePollCount; @@ -481,7 +482,7 @@ contract("Voting Reputation", (accounts) => { bn2bytes32(WAD), ]); - await voting.createDomainPoll(1, UINT256_MAX, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + await voting.createDomainPoll(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); pollId = await voting.getPollCount(); let expenditurePollCount; @@ -528,10 +529,10 @@ contract("Voting Reputation", (accounts) => { bn2bytes32(WAD), ]); - await voting.createDomainPoll(1, UINT256_MAX, action1, domain1Key, domain1Value, domain1Mask, domain1Siblings); + await voting.createDomainPoll(1, UINT256_MAX, ADDRESS_ZERO, action1, domain1Key, domain1Value, domain1Mask, domain1Siblings); const pollId1 = await voting.getPollCount(); - await voting.createDomainPoll(1, UINT256_MAX, action2, domain1Key, domain1Value, domain1Mask, domain1Siblings); + await voting.createDomainPoll(1, UINT256_MAX, ADDRESS_ZERO, action2, domain1Key, domain1Value, domain1Mask, domain1Siblings); const pollId2 = await voting.getPollCount(); let expenditurePollCount; @@ -621,7 +622,7 @@ contract("Voting Reputation", (accounts) => { beforeEach(async () => { const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); - await voting.createRootPoll(action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + await voting.createRootPoll(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); pollId = await voting.getPollCount(); await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); @@ -718,7 +719,7 @@ contract("Voting Reputation", (accounts) => { await repCycle.confirmNewHash(0); // Create new poll with new reputation state - await voting.createRootPoll(FAKE, domain1Key, domain1Value, domain1Mask2, domain1Siblings2); + await voting.createRootPoll(ADDRESS_ZERO, FAKE, domain1Key, domain1Value, domain1Mask2, domain1Siblings2); const pollId2 = await voting.getPollCount(); await voting.stakePoll(pollId2, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value2, user0Mask2, user0Siblings2, { from: USER0 }); await voting.stakePoll(pollId2, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask2, user1Siblings2, { from: USER1 }); @@ -789,7 +790,7 @@ contract("Voting Reputation", (accounts) => { beforeEach(async () => { const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); - await voting.createRootPoll(action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + await voting.createRootPoll(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); pollId = await voting.getPollCount(); }); @@ -815,6 +816,27 @@ contract("Voting Reputation", (accounts) => { expect(logs[0].args.success).to.be.true; }); + it("can take an action with an arbitrary target", async () => { + const { colony: otherColony } = await setupRandomColony(colonyNetwork); + await token.mint(otherColony.address, WAD, { from: USER0 }); + + const action = await encodeTxData(colony, "claimColonyFunds", [token.address]); + await voting.createRootPoll(otherColony.address, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + pollId = await voting.getPollCount(); + + await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + + await forwardTime(STAKE_WINDOW, this); + + const balanceBefore = await otherColony.getFundingPotBalance(1, token.address); + expect(balanceBefore).to.be.zero; + + await voting.executePoll(pollId); + + const balanceAfter = await otherColony.getFundingPotBalance(1, token.address); + expect(balanceAfter).to.eq.BN(WAD); + }); + it("cannot take an action during staking or voting", async () => { let pollState; await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); @@ -889,7 +911,7 @@ contract("Voting Reputation", (accounts) => { const action = await encodeTxData(colony, "setExpenditureState", [1, UINT256_MAX, 1, 0, [], [], ethers.constants.HashZero]); let logs; - await voting.createDomainPoll(1, UINT256_MAX, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + await voting.createDomainPoll(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); const pollId1 = await voting.getPollCount(); await voting.stakePoll(pollId1, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); @@ -910,7 +932,7 @@ contract("Voting Reputation", (accounts) => { expect(logs[0].args.success).to.be.true; // Create another poll for the same variable - await voting.createDomainPoll(1, UINT256_MAX, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + await voting.createDomainPoll(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); const pollId2 = await voting.getPollCount(); await voting.stakePoll(pollId2, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); @@ -934,7 +956,7 @@ contract("Voting Reputation", (accounts) => { it("can set vote power correctly if there is insufficient opposition", async () => { const action = await encodeTxData(colony, "setExpenditureState", [1, UINT256_MAX, 1, 0, [], [], ethers.constants.HashZero]); - await voting.createDomainPoll(1, UINT256_MAX, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + await voting.createDomainPoll(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); pollId = await voting.getPollCount(); await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); @@ -950,7 +972,7 @@ contract("Voting Reputation", (accounts) => { it("can set vote power correctly after a vote", async () => { const action = await encodeTxData(colony, "setExpenditureState", [1, UINT256_MAX, 1, 0, [], [], ethers.constants.HashZero]); - await voting.createDomainPoll(1, UINT256_MAX, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + await voting.createDomainPoll(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); pollId = await voting.getPollCount(); await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); @@ -979,7 +1001,7 @@ contract("Voting Reputation", (accounts) => { beforeEach(async () => { const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); - await voting.createRootPoll(action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + await voting.createRootPoll(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); pollId = await voting.getPollCount(); }); @@ -1123,7 +1145,7 @@ contract("Voting Reputation", (accounts) => { }); it("cannot claim rewards before a poll is executed", async () => { - await checkErrorRevert(voting.claimReward(pollId, 1, UINT256_MAX, 1, USER0, true), "voting-rep-not-executed"); + await checkErrorRevert(voting.claimReward(pollId, 1, UINT256_MAX, 1, USER0, true), "voting-rep-not-failed-or-executed"); }); it("cannot claim rewards with a bad domainId", async () => { @@ -1154,7 +1176,7 @@ contract("Voting Reputation", (accounts) => { [user1Mask, user1Siblings] = await reputationTree.getProof(user1Key); const action = await encodeTxData(colony, "makeTask", [1, 0, FAKE, 2, 0, 0]); - await voting.createDomainPoll(2, UINT256_MAX, action, domain2Key, domain2Value, domain2Mask, domain2Siblings); + await voting.createDomainPoll(2, UINT256_MAX, ADDRESS_ZERO, action, domain2Key, domain2Value, domain2Mask, domain2Siblings); pollId = await voting.getPollCount(); await colony.approveStake(voting.address, 2, WAD, { from: USER0 }); From 0aa118f77dc4c659986f06745b77f73a188d5871 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Tue, 23 Jun 2020 15:36:19 -0700 Subject: [PATCH 19/61] Add initialise and deprecate functionality --- contracts/extensions/VotingReputation.sol | 46 +++++++++++++++++++---- test/extensions/voting-rep.js | 15 +++++++- 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index 4b509b01d3..5fcfc17d4c 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -19,6 +19,7 @@ pragma solidity 0.5.8; pragma experimental ABIEncoderV2; import "./../../lib/dappsys/math.sol"; +import "./../colony/ColonyDataTypes.sol"; import "./../colony/IColony.sol"; import "./../colonyNetwork/IColonyNetwork.sol"; import "./../common/ERC20Extended.sol"; @@ -29,6 +30,8 @@ import "./../tokenLocking/ITokenLocking.sol"; contract VotingReputation is DSMath, PatriciaTreeProofs { // Events + event ExtensionInitialised(); + event ExtensionDeprecated(); event PollCreated(uint256 indexed pollId, uint256 indexed skillId); event PollStaked(uint256 indexed pollId, address indexed staker, bool indexed side, uint256 amount); event PollVoteSubmitted(uint256 indexed pollId, address indexed voter); @@ -41,10 +44,6 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { uint256 constant NAY = 0; uint256 constant YAY = 1; - uint256 constant STAKE_FRACTION = WAD / 1000; // 0.1% - uint256 constant VOTER_REWARD_FRACTION = WAD / 10; // 10% - uint256 constant VOTE_POWER_FRACTION = (WAD * 2) / 3; // 66.6% - uint256 constant STAKE_PERIOD = 3 days; uint256 constant VOTE_PERIOD = 2 days; uint256 constant REVEAL_PERIOD = 2 days; @@ -53,12 +52,20 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { keccak256("setExpenditureState(uint256,uint256,uint256,uint256,bool[],bytes32[],bytes32)") ); + enum ExtensionState { Deployed, Active, Deprecated } + // Initialization data + ExtensionState state; + IColony colony; IColonyNetwork colonyNetwork; ITokenLocking tokenLocking; address token; + uint256 stakeFraction; // WAD / 1000 (0.1%) + uint256 voterRewardFraction; // WAD / 10 (10%) + uint256 votePowerFraction; // (WAD * 2) / 3 (66.6%) + constructor(address _colony) public { colony = IColony(_colony); colonyNetwork = IColonyNetwork(colony.getColonyNetwork()); @@ -66,6 +73,27 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { token = colony.getToken(); } + function initialise(uint256 _stakeFraction, uint256 _voterRewardFraction, uint256 _votePowerFraction) public { + require(state == ExtensionState.Deployed, "voting-rep-already-initialised"); + require(colony.hasUserRole(msg.sender, 1, ColonyDataTypes.ColonyRole.Root), "voting-rep-caller-not-root"); + + state = ExtensionState.Active; + + stakeFraction = _stakeFraction; + voterRewardFraction = _voterRewardFraction; + votePowerFraction = _votePowerFraction; + + emit ExtensionInitialised(); + } + + function deprecate() public { + require(colony.hasUserRole(msg.sender, 1, ColonyDataTypes.ColonyRole.Root), "voting-rep-caller-not-root"); + + state = ExtensionState.Deprecated; + + emit ExtensionDeprecated(); + } + // Data structures enum PollState { Staking, Voting, Reveal, Closed, Executable, Executed, Failed } enum Response { None, Fold, Call } @@ -164,8 +192,8 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { require(colony.getDomain(_domainId).skillId == poll.skillId, "voting-rep-bad-domain-id"); require(add(stakes[_pollId][msg.sender][_vote], _amount) <= stakerRep, "voting-rep-insufficient-rep"); - require(add(poll.stakes[toInt(_vote)], _amount) <= getRequiredStake(_pollId), "voting-rep-stake-too-large"); require(getPollState(_pollId) == PollState.Staking, "voting-rep-staking-closed"); + require(add(poll.stakes[toInt(_vote)], _amount) <= getRequiredStake(_pollId), "voting-rep-stake-too-large"); colony.obligateStake(msg.sender, _domainId, _amount); colony.transferStake(_permissionDomainId, _childSkillIndex, address(this), msg.sender, _domainId, _amount, address(this)); @@ -256,7 +284,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { uint256 pctReputation = wdiv(userRep, poll.skillRep); uint256 totalStake = add(poll.stakes[YAY], poll.stakes[NAY]); - uint256 voterReward = wmul(wmul(pctReputation, totalStake), VOTER_REWARD_FRACTION); + uint256 voterReward = wmul(wmul(pctReputation, totalStake), voterRewardFraction); poll.unpaidRewards = sub(poll.unpaidRewards, voterReward); tokenLocking.transfer(token, voterReward, msg.sender, true); @@ -323,7 +351,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { uint256 votePower; if (poll.stakes[NAY] < getRequiredStake(_pollId)) { - votePower = wmul(poll.skillRep, VOTE_POWER_FRACTION); + votePower = wmul(poll.skillRep, votePowerFraction); } else { votePower = poll.votes[YAY]; } @@ -518,6 +546,8 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { ) internal { + require(state == ExtensionState.Active, "voting-rep-not-active"); + pollCount += 1; polls[pollCount].lastEvent = now; polls[pollCount].rootHash = colonyNetwork.getReputationRootHash(); @@ -538,7 +568,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { } function getRequiredStake(uint256 _pollId) internal view returns (uint256) { - return wmul(polls[_pollId].skillRep, STAKE_FRACTION); + return wmul(polls[_pollId].skillRep, stakeFraction); } function checkReputation( diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index b4893cb28f..e092530a12 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -118,6 +118,7 @@ contract("Voting Reputation", (accounts) => { await votingFactory.deployExtension(colony.address); const votingAddress = await votingFactory.deployedExtensions(colony.address); voting = await VotingReputation.at(votingAddress); + await voting.initialise(WAD.divn(1000), WAD.divn(10), WAD.muln(2).divn(3)); await colony.setArbitrationRole(1, UINT256_MAX, voting.address, 1, true); await colony.setAdministrationRole(1, UINT256_MAX, voting.address, 1, true); @@ -195,7 +196,7 @@ contract("Voting Reputation", (accounts) => { await repCycle.confirmNewHash(0); }); - describe("using the extension factory", async () => { + describe("deploying the extension", async () => { it("can install the extension factory once if root and uninstall", async () => { ({ colony } = await setupRandomColony(colonyNetwork)); await checkErrorRevert(votingFactory.deployExtension(colony.address, { from: USER1 }), "colony-extension-user-not-root"); @@ -203,6 +204,18 @@ contract("Voting Reputation", (accounts) => { await checkErrorRevert(votingFactory.deployExtension(colony.address, { from: USER0 }), "colony-extension-already-deployed"); await votingFactory.removeExtension(colony.address, { from: USER0 }); }); + + it("can deprecate the extension", async () => { + const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); + await voting.createRootPoll(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + + await voting.deprecate(); + + await checkErrorRevert( + voting.createRootPoll(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings), + "voting-rep-not-active" + ); + }); }); describe("creating polls", async () => { From ed1c4b109e6eee07f361589885c6d9b840380487 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Tue, 23 Jun 2020 15:36:36 -0700 Subject: [PATCH 20/61] Add crowdfunding option --- contracts/extensions/VotingReputation.sol | 20 ++++++++++++--- test/extensions/voting-rep.js | 30 +++++++++++++++++++++-- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index 5fcfc17d4c..ae3e0d8cf2 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -65,6 +65,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { uint256 stakeFraction; // WAD / 1000 (0.1%) uint256 voterRewardFraction; // WAD / 10 (10%) uint256 votePowerFraction; // (WAD * 2) / 3 (66.6%) + bool crowdFunding; constructor(address _colony) public { colony = IColony(_colony); @@ -73,21 +74,29 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { token = colony.getToken(); } - function initialise(uint256 _stakeFraction, uint256 _voterRewardFraction, uint256 _votePowerFraction) public { + function initialise( + uint256 _stakeFraction, + uint256 _voterRewardFraction, + uint256 _votePowerFraction, + bool _crowdFunding + ) + public + { + require(colony.hasUserRole(msg.sender, 1, ColonyDataTypes.ColonyRole.Root), "voting-rep-user-not-root"); require(state == ExtensionState.Deployed, "voting-rep-already-initialised"); - require(colony.hasUserRole(msg.sender, 1, ColonyDataTypes.ColonyRole.Root), "voting-rep-caller-not-root"); state = ExtensionState.Active; stakeFraction = _stakeFraction; voterRewardFraction = _voterRewardFraction; votePowerFraction = _votePowerFraction; + crowdFunding = _crowdFunding; emit ExtensionInitialised(); } function deprecate() public { - require(colony.hasUserRole(msg.sender, 1, ColonyDataTypes.ColonyRole.Root), "voting-rep-caller-not-root"); + require(colony.hasUserRole(msg.sender, 1, ColonyDataTypes.ColonyRole.Root), "voting-rep-user-not-root"); state = ExtensionState.Deprecated; @@ -195,6 +204,11 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { require(getPollState(_pollId) == PollState.Staking, "voting-rep-staking-closed"); require(add(poll.stakes[toInt(_vote)], _amount) <= getRequiredStake(_pollId), "voting-rep-stake-too-large"); + require( + crowdFunding || add(poll.stakes[toInt(_vote)], _amount) == getRequiredStake(_pollId), + "voting-rep-stake-must-be-exact" + ); + colony.obligateStake(msg.sender, _domainId, _amount); colony.transferStake(_permissionDomainId, _childSkillIndex, address(this), msg.sender, _domainId, _amount, address(this)); diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index e092530a12..a55bdc63a2 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -118,7 +118,7 @@ contract("Voting Reputation", (accounts) => { await votingFactory.deployExtension(colony.address); const votingAddress = await votingFactory.deployedExtensions(colony.address); voting = await VotingReputation.at(votingAddress); - await voting.initialise(WAD.divn(1000), WAD.divn(10), WAD.muln(2).divn(3)); + await voting.initialise(WAD.divn(1000), WAD.divn(10), WAD.muln(2).divn(3), true); await colony.setArbitrationRole(1, UINT256_MAX, voting.address, 1, true); await colony.setAdministrationRole(1, UINT256_MAX, voting.address, 1, true); @@ -205,17 +205,26 @@ contract("Voting Reputation", (accounts) => { await votingFactory.removeExtension(colony.address, { from: USER0 }); }); - it("can deprecate the extension", async () => { + it("can deprecate the extension if root", async () => { const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); await voting.createRootPoll(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + // Must be root + await checkErrorRevert(voting.deprecate({ from: USER2 }), "voting-rep-user-not-root"); + await voting.deprecate(); + // Cant make new polls! await checkErrorRevert( voting.createRootPoll(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings), "voting-rep-not-active" ); }); + + it("cannot initialise twice or if not root", async () => { + await checkErrorRevert(voting.initialise(WAD, WAD, WAD, true), "voting-rep-already-initialised"); + await checkErrorRevert(voting.initialise(WAD, WAD, WAD, true, { from: USER2 }), "voting-rep-user-not-root"); + }); }); describe("creating polls", async () => { @@ -628,6 +637,23 @@ contract("Voting Reputation", (accounts) => { "voting-rep-staking-closed" ); }); + + it("cannot crowdfund if prohibited in initialisation", async () => { + await votingFactory.removeExtension(colony.address); + await votingFactory.deployExtension(colony.address); + const votingAddress = await votingFactory.deployedExtensions(colony.address); + voting = await VotingReputation.at(votingAddress); + await voting.initialise(WAD.divn(1000), WAD.divn(10), WAD.muln(2).divn(3), false); + + const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); + await voting.createRootPoll(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + pollId = await voting.getPollCount(); + + await checkErrorRevert( + voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE.subn(1), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), + "voting-rep-stake-must-be-exact" + ); + }); }); describe("voting on polls", async () => { From 7ff395de9af0d04646f6d390ad3ae4704d256706 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Mon, 29 Jun 2020 13:16:48 -0700 Subject: [PATCH 21/61] Respond to review comments --- contracts/extensions/VotingReputation.sol | 59 +++--- test/extensions/voting-rep.js | 235 ++++++++++++---------- 2 files changed, 163 insertions(+), 131 deletions(-) diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index ae3e0d8cf2..017679f433 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -62,10 +62,10 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { ITokenLocking tokenLocking; address token; - uint256 stakeFraction; // WAD / 1000 (0.1%) - uint256 voterRewardFraction; // WAD / 10 (10%) - uint256 votePowerFraction; // (WAD * 2) / 3 (66.6%) - bool crowdFunding; + uint256 stakeFraction; // Percent of domain reputation needed for staking + uint256 voterRewardFraction; // Percent of stake paid out to voters as rewards + uint256 votePowerFraction; // Percent of domain rep used as vote power if no-contest + bool crowdFunding; // Flag allowing or disallowing crowdfunding polls constructor(address _colony) public { colony = IColony(_colony); @@ -85,6 +85,10 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { require(colony.hasUserRole(msg.sender, 1, ColonyDataTypes.ColonyRole.Root), "voting-rep-user-not-root"); require(state == ExtensionState.Deployed, "voting-rep-already-initialised"); + require(_stakeFraction <= WAD, "voting-rep-must-be-wad"); + require(_voterRewardFraction <= WAD, "voting-rep-must-be-wad"); + require(_votePowerFraction <= WAD, "voting-rep-must-be-wad"); + state = ExtensionState.Active; stakeFraction = _stakeFraction; @@ -110,6 +114,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { struct Poll { uint256 lastEvent; // Set at creation / escalation & when staked + responded bytes32 rootHash; + uint256 domainId; uint256 skillId; uint256 skillRep; uint256 unpaidRewards; @@ -117,9 +122,9 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { uint256[2] votes; // [nay, yay] address[2] leads; // [nay, yay] Response[2] responses; // [nay, yay] + bool executed; address target; bytes action; - bool executed; } // Storage @@ -146,7 +151,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { public { uint256 rootSkillId = colony.getDomain(1).skillId; - createPoll(_target, _action, rootSkillId, _key, _value, _branchMask, _siblings); + createPoll(_target, _action, 1, rootSkillId, _key, _value, _branchMask, _siblings); } function createDomainPoll( @@ -169,14 +174,13 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { require(childSkillId == actionDomainSkillId, "voting-rep-invalid-domain-id"); } - createPoll(_target, _action, domainSkillId, _key, _value, _branchMask, _siblings); + createPoll(_target, _action, _domainId, domainSkillId, _key, _value, _branchMask, _siblings); } function stakePoll( uint256 _pollId, uint256 _permissionDomainId, // For extension's arbitration permission uint256 _childSkillIndex, // For extension's arbitration permission - uint256 _domainId, bool _vote, uint256 _amount, bytes memory _key, @@ -187,6 +191,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { public { Poll storage poll = polls[_pollId]; + require(getPollState(_pollId) == PollState.Staking, "voting-rep-staking-closed"); require( activePolls[hashAction(poll.action)] == 0 || @@ -194,31 +199,27 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { "voting-rep-competing-poll-exists" ); + uint256 amount = min(_amount, sub(getRequiredStake(_pollId), poll.stakes[toInt(_vote)])); uint256 stakerRep = checkReputation(_pollId, msg.sender, _key, _value, _branchMask, _siblings); - // TODO: can we keep the domainId on the poll somewhere? This seems like a wasteful external call. - // But if it's 10 external calls per word of storage, then < 10 stakers makes this cheaper. - require(colony.getDomain(_domainId).skillId == poll.skillId, "voting-rep-bad-domain-id"); - require(add(stakes[_pollId][msg.sender][_vote], _amount) <= stakerRep, "voting-rep-insufficient-rep"); - - require(getPollState(_pollId) == PollState.Staking, "voting-rep-staking-closed"); - require(add(poll.stakes[toInt(_vote)], _amount) <= getRequiredStake(_pollId), "voting-rep-stake-too-large"); + require(add(stakes[_pollId][msg.sender][_vote], amount) <= stakerRep, "voting-rep-insufficient-rep"); require( - crowdFunding || add(poll.stakes[toInt(_vote)], _amount) == getRequiredStake(_pollId), + crowdFunding || add(poll.stakes[toInt(_vote)], amount) == getRequiredStake(_pollId), "voting-rep-stake-must-be-exact" ); - colony.obligateStake(msg.sender, _domainId, _amount); - colony.transferStake(_permissionDomainId, _childSkillIndex, address(this), msg.sender, _domainId, _amount, address(this)); + + colony.obligateStake(msg.sender, poll.domainId, amount); + colony.transferStake(_permissionDomainId, _childSkillIndex, address(this), msg.sender, poll.domainId, amount, address(this)); // Update the stake - poll.unpaidRewards = add(poll.unpaidRewards, _amount); - poll.stakes[toInt(_vote)] = add(poll.stakes[toInt(_vote)], _amount); - stakes[_pollId][msg.sender][_vote] = add(stakes[_pollId][msg.sender][_vote], _amount); + poll.unpaidRewards = add(poll.unpaidRewards, amount); + poll.stakes[toInt(_vote)] = add(poll.stakes[toInt(_vote)], amount); + stakes[_pollId][msg.sender][_vote] = add(stakes[_pollId][msg.sender][_vote], amount); // Update the lead if the stake is larger - if (stakes[_pollId][poll.leads[toInt(_vote)]][_vote] < _amount) { + if (stakes[_pollId][poll.leads[toInt(_vote)]][_vote] < amount) { poll.leads[toInt(_vote)] = msg.sender; } @@ -241,7 +242,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { tokenLocking.claim(token, true); } - emit PollStaked(_pollId, msg.sender, _vote, _amount); + emit PollStaked(_pollId, msg.sender, _vote, amount); } function respondToStake(uint256 _pollId, bool _vote, Response _response) public { @@ -325,6 +326,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { require(childSkillId == poll.skillId, "voting-rep-invalid-domain-proof"); poll.lastEvent = now; + poll.domainId = _newDomainId; poll.skillId = newDomainSkillId; poll.skillRep = checkReputation(_pollId, address(0x0), _key, _value, _branchMask, _siblings); @@ -389,7 +391,6 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { uint256 _pollId, uint256 _permissionDomainId, // For extension's arbitration permission uint256 _childSkillIndex, // For extension's arbitration permission - uint256 _domainId, address _user, bool _vote ) @@ -402,10 +403,6 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { "voting-rep-not-failed-or-executed" ); - // TODO: can we keep the domainId on the poll somewhere? This seems like a wasteful external call. - // But if it's 10 external calls per word of storage, then < 10 stakers makes this cheaper. - require(colony.getDomain(_domainId).skillId == poll.skillId, "voting-rep-bad-domain-id"); - // Calculate how much of the stake is left after voter compensation (>= 90%) uint256 stake = stakes[_pollId][_user][_vote]; uint256 totalStake = add(poll.stakes[NAY], poll.stakes[YAY]); @@ -450,7 +447,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { colony.emitDomainReputationPenalty( _permissionDomainId, _childSkillIndex, - _domainId, + poll.domainId, _user, -int256(repPenalty) ); @@ -532,7 +529,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { // Infer the right timestamp (since only updated if both parties respond) uint256 lastEvent = poll.lastEvent + (( poll.responses[YAY] == Response.None || - poll.responses[YAY] == Response.None + poll.responses[NAY] == Response.None ) ? STAKE_PERIOD : 0); if (now < lastEvent + VOTE_PERIOD) { @@ -552,6 +549,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { function createPoll( address _target, bytes memory _action, + uint256 _domainId, uint256 _skillId, bytes memory _key, bytes memory _value, @@ -565,6 +563,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { pollCount += 1; polls[pollCount].lastEvent = now; polls[pollCount].rootHash = colonyNetwork.getReputationRootHash(); + polls[pollCount].domainId = _domainId; polls[pollCount].skillId = _skillId; polls[pollCount].skillRep = checkReputation(pollCount, address(0x0), _key, _value, _branchMask, _siblings); polls[pollCount].target = _target; diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index a55bdc63a2..92eb7ba08e 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -79,7 +79,7 @@ contract("Voting Reputation", (accounts) => { const STAKING = 0; const VOTING = 1; - // const REVEAL = 2; + const REVEAL = 2; // const CLOSED = 3; const EXECUTABLE = 4; // const EXECUTED = 5; @@ -225,6 +225,19 @@ contract("Voting Reputation", (accounts) => { await checkErrorRevert(voting.initialise(WAD, WAD, WAD, true), "voting-rep-already-initialised"); await checkErrorRevert(voting.initialise(WAD, WAD, WAD, true, { from: USER2 }), "voting-rep-user-not-root"); }); + + it("cannot initialise with invalid values", async () => { + await votingFactory.removeExtension(colony.address, { from: USER0 }); + await votingFactory.deployExtension(colony.address); + const votingAddress = await votingFactory.deployedExtensions(colony.address); + voting = await VotingReputation.at(votingAddress); + + await checkErrorRevert(voting.initialise(WAD.addn(1), WAD, WAD, true), "voting-rep-must-be-wad"); + await checkErrorRevert(voting.initialise(WAD, WAD.addn(1), WAD, true), "voting-rep-must-be-wad"); + await checkErrorRevert(voting.initialise(WAD, WAD, WAD.addn(1), true), "voting-rep-must-be-wad"); + + await voting.initialise(WAD, WAD, WAD, true); + }); }); describe("creating polls", async () => { @@ -292,8 +305,8 @@ contract("Voting Reputation", (accounts) => { }); it("can stake on a poll", async () => { - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, 100, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, 100, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, true, 100, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, true, 100, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); const poll = await voting.getPoll(pollId); expect(poll.stakes[0]).to.be.zero; @@ -309,11 +322,11 @@ contract("Voting Reputation", (accounts) => { let pollState = await voting.getPollState(pollId); expect(pollState).to.eq.BN(STAKING); - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); pollState = await voting.getPollState(pollId); expect(pollState).to.eq.BN(STAKING); - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); pollState = await voting.getPollState(pollId); expect(pollState).to.eq.BN(STAKING); @@ -327,8 +340,8 @@ contract("Voting Reputation", (accounts) => { }); it("can go to a vote even if both sides do not call", async () => { - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await forwardTime(STAKE_WINDOW, this); @@ -337,8 +350,8 @@ contract("Voting Reputation", (accounts) => { }); it("cannot execute if the YAY side stakes and folds", async () => { - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await voting.respondToStake(pollId, true, FOLD, { from: USER0 }); @@ -347,8 +360,8 @@ contract("Voting Reputation", (accounts) => { }); it("can execute if the NAY side stakes and folds", async () => { - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await voting.respondToStake(pollId, false, FOLD, { from: USER1 }); @@ -361,8 +374,8 @@ contract("Voting Reputation", (accounts) => { }); it("cannot respond to a stake if not in the staking phase", async () => { - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await forwardTime(STAKE_WINDOW, this); @@ -370,15 +383,15 @@ contract("Voting Reputation", (accounts) => { }); it("cannot respond to a stake if not the lead", async () => { - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await checkErrorRevert(voting.respondToStake(pollId, true, CALL, { from: USER2 }), "voting-rep-not-lead"); }); it("cannot respond to a stake twice", async () => { - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await voting.respondToStake(pollId, true, CALL, { from: USER0 }); @@ -393,7 +406,7 @@ contract("Voting Reputation", (accounts) => { expect(activePoll).to.be.zero; await forwardTime(STAKE_WINDOW / 2, this); - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); activePoll = await voting.getActivePoll(soliditySha3(action)); expect(activePoll).to.eq.BN(pollId); @@ -402,7 +415,7 @@ contract("Voting Reputation", (accounts) => { const otherPollId = await voting.getPollCount(); await checkErrorRevert( - voting.stakePoll(otherPollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), + voting.stakePoll(otherPollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), "voting-rep-competing-poll-exists" ); @@ -413,7 +426,7 @@ contract("Voting Reputation", (accounts) => { activePoll = await voting.getActivePoll(soliditySha3(action)); expect(activePoll).to.be.zero; - await voting.stakePoll(otherPollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(otherPollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); activePoll = await voting.getActivePoll(soliditySha3(action)); expect(activePoll).to.eq.BN(otherPollId); @@ -445,7 +458,7 @@ contract("Voting Reputation", (accounts) => { expenditureSlot = await colony.getExpenditure(expenditureId); expect(expenditureSlot.globalClaimDelay).to.be.zero; - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); expenditurePollCount = await voting.getExpenditurePollCount(soliditySha3(expenditureId)); expect(expenditurePollCount).to.eq.BN(1); @@ -480,7 +493,7 @@ contract("Voting Reputation", (accounts) => { expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); expect(expenditureSlot.claimDelay).to.be.zero; - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); expenditurePollCount = await voting.getExpenditurePollCount(soliditySha3(expenditureId, 0)); expect(expenditurePollCount).to.eq.BN(1); @@ -515,7 +528,7 @@ contract("Voting Reputation", (accounts) => { expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); expect(expenditureSlot.claimDelay).to.be.zero; - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); expenditurePollCount = await voting.getExpenditurePollCount(soliditySha3(expenditureId, 0)); expect(expenditurePollCount).to.eq.BN(1); @@ -565,8 +578,8 @@ contract("Voting Reputation", (accounts) => { expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); expect(expenditureSlot.claimDelay).to.be.zero; - await voting.stakePoll(pollId1, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId2, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId1, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId2, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); expenditurePollCount = await voting.getExpenditurePollCount(expenditureHash); expect(expenditurePollCount).to.eq.BN(2); @@ -594,7 +607,7 @@ contract("Voting Reputation", (accounts) => { it("cannot stake with someone else's reputation", async () => { await checkErrorRevert( - voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER1 }), + voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER1 }), "voting-rep-invalid-user-address" ); }); @@ -605,35 +618,21 @@ contract("Voting Reputation", (accounts) => { const [user2Mask, user2Siblings] = await reputationTree.getProof(user2Key); await checkErrorRevert( - voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user2Key, user2Value, user2Mask, user2Siblings, { from: USER2 }), + voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user2Key, user2Value, user2Mask, user2Siblings, { from: USER2 }), "voting-rep-insufficient-rep" ); }); - it("cannot stake more than the required stake", async () => { - await checkErrorRevert( - voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE.addn(1), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), - "voting-rep-stake-too-large" - ); - }); - - it("cannot stake with an invalid domainId", async () => { - await checkErrorRevert( - voting.stakePoll(pollId, 1, 0, 2, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), - "voting-rep-bad-domain-id" - ); - }); - it("cannot stake once time runs out", async () => { await forwardTime(STAKE_WINDOW, this); await checkErrorRevert( - voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), + voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), "voting-rep-staking-closed" ); await checkErrorRevert( - voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }), + voting.stakePoll(pollId, 1, UINT256_MAX, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }), "voting-rep-staking-closed" ); }); @@ -650,10 +649,54 @@ contract("Voting Reputation", (accounts) => { pollId = await voting.getPollCount(); await checkErrorRevert( - voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE.subn(1), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), + voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE.subn(1), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), "voting-rep-stake-must-be-exact" ); }); + + it("can go to a vote even if the nay side fails to respond", async () => { + await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + + // Yay side responds + await voting.respondToStake(pollId, true, CALL, { from: USER0 }); + + let pollState; + pollState = await voting.getPollState(pollId); + expect(pollState).to.eq.BN(STAKING); + + await forwardTime(STAKE_WINDOW, this); + + pollState = await voting.getPollState(pollId); + expect(pollState).to.eq.BN(VOTING); + + await forwardTime(VOTE_WINDOW, this); + + pollState = await voting.getPollState(pollId); + expect(pollState).to.eq.BN(REVEAL); + }); + + it("can go to a vote even if the yay side fails to respond", async () => { + await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + + // Nay side responds + await voting.respondToStake(pollId, false, CALL, { from: USER1 }); + + let pollState; + pollState = await voting.getPollState(pollId); + expect(pollState).to.eq.BN(STAKING); + + await forwardTime(STAKE_WINDOW, this); + + pollState = await voting.getPollState(pollId); + expect(pollState).to.eq.BN(VOTING); + + await forwardTime(VOTE_WINDOW, this); + + pollState = await voting.getPollState(pollId); + expect(pollState).to.eq.BN(REVEAL); + }); }); describe("voting on polls", async () => { @@ -664,8 +707,8 @@ contract("Voting Reputation", (accounts) => { await voting.createRootPoll(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); pollId = await voting.getPollCount(); - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await voting.respondToStake(pollId, true, CALL, { from: USER0 }); await voting.respondToStake(pollId, false, CALL, { from: USER1 }); @@ -760,8 +803,8 @@ contract("Voting Reputation", (accounts) => { // Create new poll with new reputation state await voting.createRootPoll(ADDRESS_ZERO, FAKE, domain1Key, domain1Value, domain1Mask2, domain1Siblings2); const pollId2 = await voting.getPollCount(); - await voting.stakePoll(pollId2, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value2, user0Mask2, user0Siblings2, { from: USER0 }); - await voting.stakePoll(pollId2, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask2, user1Siblings2, { from: USER1 }); + await voting.stakePoll(pollId2, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value2, user0Mask2, user0Siblings2, { from: USER0 }); + await voting.stakePoll(pollId2, 1, UINT256_MAX, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask2, user1Siblings2, { from: USER1 }); await voting.respondToStake(pollId2, true, CALL, { from: USER0 }); await voting.respondToStake(pollId2, false, CALL, { from: USER1 }); @@ -834,7 +877,7 @@ contract("Voting Reputation", (accounts) => { }); it("cannot take an action if there is insufficient support", async () => { - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE.subn(1), user0Key, user0Value, user0Mask, user0Siblings, { + await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE.subn(1), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0, }); @@ -844,8 +887,8 @@ contract("Voting Reputation", (accounts) => { }); it("can take an action if there is insufficient opposition", async () => { - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, REQUIRED_STAKE.subn(1), user1Key, user1Value, user1Mask, user1Siblings, { + await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, false, REQUIRED_STAKE.subn(1), user1Key, user1Value, user1Mask, user1Siblings, { from: USER1, }); @@ -863,7 +906,7 @@ contract("Voting Reputation", (accounts) => { await voting.createRootPoll(otherColony.address, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); pollId = await voting.getPollCount(); - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await forwardTime(STAKE_WINDOW, this); @@ -878,13 +921,13 @@ contract("Voting Reputation", (accounts) => { it("cannot take an action during staking or voting", async () => { let pollState; - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); pollState = await voting.getPollState(pollId); expect(pollState).to.eq.BN(STAKING); await checkErrorRevert(voting.executePoll(pollId), "voting-rep-poll-not-executable"); - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await voting.respondToStake(pollId, true, CALL, { from: USER0 }); await voting.respondToStake(pollId, false, CALL, { from: USER1 }); @@ -894,7 +937,7 @@ contract("Voting Reputation", (accounts) => { }); it("cannot take an action twice", async () => { - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await forwardTime(STAKE_WINDOW, this); @@ -905,8 +948,8 @@ contract("Voting Reputation", (accounts) => { }); it("can take an action if the poll passes", async () => { - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await voting.respondToStake(pollId, true, CALL, { from: USER0 }); await voting.respondToStake(pollId, false, CALL, { from: USER1 }); @@ -927,8 +970,8 @@ contract("Voting Reputation", (accounts) => { }); it("cannot take an action if the poll fails", async () => { - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await voting.respondToStake(pollId, true, CALL, { from: USER0 }); await voting.respondToStake(pollId, false, CALL, { from: USER1 }); @@ -953,8 +996,8 @@ contract("Voting Reputation", (accounts) => { await voting.createDomainPoll(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); const pollId1 = await voting.getPollCount(); - await voting.stakePoll(pollId1, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId1, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId1, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId1, 1, UINT256_MAX, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await voting.respondToStake(pollId1, true, CALL, { from: USER0 }); await voting.respondToStake(pollId1, false, CALL, { from: USER1 }); @@ -974,8 +1017,8 @@ contract("Voting Reputation", (accounts) => { await voting.createDomainPoll(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); const pollId2 = await voting.getPollCount(); - await voting.stakePoll(pollId2, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId2, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId2, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId2, 1, UINT256_MAX, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await voting.respondToStake(pollId2, true, CALL, { from: USER0 }); await voting.respondToStake(pollId2, false, CALL, { from: USER1 }); @@ -998,7 +1041,7 @@ contract("Voting Reputation", (accounts) => { await voting.createDomainPoll(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); pollId = await voting.getPollCount(); - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await forwardTime(STAKE_WINDOW, this); @@ -1014,8 +1057,8 @@ contract("Voting Reputation", (accounts) => { await voting.createDomainPoll(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); pollId = await voting.getPollCount(); - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await voting.respondToStake(pollId, true, CALL, { from: USER0 }); await voting.respondToStake(pollId, false, CALL, { from: USER1 }); @@ -1049,8 +1092,8 @@ contract("Voting Reputation", (accounts) => { const repCycle = await IReputationMiningCycle.at(addr); const numEntriesPrev = await repCycle.getReputationUpdateLogLength(); - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, REQUIRED_STAKE.divn(2), user1Key, user1Value, user1Mask, user1Siblings, { + await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, false, REQUIRED_STAKE.divn(2), user1Key, user1Value, user1Mask, user1Siblings, { from: USER1, }); @@ -1061,8 +1104,8 @@ contract("Voting Reputation", (accounts) => { const user0LockPre = await tokenLocking.getUserLock(token.address, USER0); const user1LockPre = await tokenLocking.getUserLock(token.address, USER1); - await voting.claimReward(pollId, 1, UINT256_MAX, 1, USER0, true); - await voting.claimReward(pollId, 1, UINT256_MAX, 1, USER1, false); + await voting.claimReward(pollId, 1, UINT256_MAX, USER0, true); + await voting.claimReward(pollId, 1, UINT256_MAX, USER1, false); const user0LockPost = await tokenLocking.getUserLock(token.address, USER0); const user1LockPost = await tokenLocking.getUserLock(token.address, USER1); @@ -1088,8 +1131,8 @@ contract("Voting Reputation", (accounts) => { const repCycle = await IReputationMiningCycle.at(addr); const numEntriesPrev = await repCycle.getReputationUpdateLogLength(); - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await voting.respondToStake(pollId, true, CALL, { from: USER0 }); await voting.respondToStake(pollId, false, CALL, { from: USER1 }); @@ -1110,8 +1153,8 @@ contract("Voting Reputation", (accounts) => { const user0LockPre = await tokenLocking.getUserLock(token.address, USER0); const user1LockPre = await tokenLocking.getUserLock(token.address, USER1); - await voting.claimReward(pollId, 1, UINT256_MAX, 1, USER0, true); - await voting.claimReward(pollId, 1, UINT256_MAX, 1, USER1, false); + await voting.claimReward(pollId, 1, UINT256_MAX, USER0, true); + await voting.claimReward(pollId, 1, UINT256_MAX, USER1, false); const user0LockPost = await tokenLocking.getUserLock(token.address, USER0); const user1LockPost = await tokenLocking.getUserLock(token.address, USER1); @@ -1137,16 +1180,16 @@ contract("Voting Reputation", (accounts) => { const repCycle = await IReputationMiningCycle.at(addr); const numEntriesPrev = await repCycle.getReputationUpdateLogLength(); - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, 100, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, 100, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, true, 100, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, false, 100, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await forwardTime(STAKE_WINDOW, this); const user0LockPre = await tokenLocking.getUserLock(token.address, USER0); const user1LockPre = await tokenLocking.getUserLock(token.address, USER1); - await voting.claimReward(pollId, 1, UINT256_MAX, 1, USER0, true); - await voting.claimReward(pollId, 1, UINT256_MAX, 1, USER1, false); + await voting.claimReward(pollId, 1, UINT256_MAX, USER0, true); + await voting.claimReward(pollId, 1, UINT256_MAX, USER1, false); const numEntriesPost = await repCycle.getReputationUpdateLogLength(); @@ -1159,8 +1202,8 @@ contract("Voting Reputation", (accounts) => { }); it("cannot claim rewards twice", async () => { - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await voting.respondToStake(pollId, true, CALL, { from: USER0 }); await voting.respondToStake(pollId, false, CALL, { from: USER1 }); @@ -1176,25 +1219,15 @@ contract("Voting Reputation", (accounts) => { await voting.executePoll(pollId); - await voting.claimReward(pollId, 1, UINT256_MAX, 1, USER0, true); + await voting.claimReward(pollId, 1, UINT256_MAX, USER0, true); const userLock0 = await tokenLocking.getUserLock(token.address, USER0); - await voting.claimReward(pollId, 1, UINT256_MAX, 1, USER0, true); + await voting.claimReward(pollId, 1, UINT256_MAX, USER0, true); const userLock1 = await tokenLocking.getUserLock(token.address, USER0); expect(userLock0.balance).to.eq.BN(userLock1.balance); }); it("cannot claim rewards before a poll is executed", async () => { - await checkErrorRevert(voting.claimReward(pollId, 1, UINT256_MAX, 1, USER0, true), "voting-rep-not-failed-or-executed"); - }); - - it("cannot claim rewards with a bad domainId", async () => { - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - - await forwardTime(STAKE_WINDOW, this); - - await voting.executePoll(pollId); - - await checkErrorRevert(voting.claimReward(pollId, 2, UINT256_MAX, 2, USER0, true), "voting-rep-bad-domain-id"); + await checkErrorRevert(voting.claimReward(pollId, 1, UINT256_MAX, USER0, true), "voting-rep-not-failed-or-executed"); }); }); @@ -1221,8 +1254,8 @@ contract("Voting Reputation", (accounts) => { await colony.approveStake(voting.address, 2, WAD, { from: USER0 }); await colony.approveStake(voting.address, 2, WAD, { from: USER1 }); - await voting.stakePoll(pollId, 1, 0, 2, true, WAD.divn(1000), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, 0, 2, false, WAD.divn(1000), user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, 0, true, WAD.divn(1000), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, 0, false, WAD.divn(1000), user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await voting.respondToStake(pollId, true, CALL, { from: USER0 }); await voting.respondToStake(pollId, false, CALL, { from: USER1 }); @@ -1281,8 +1314,8 @@ contract("Voting Reputation", (accounts) => { [user1Mask, user1Siblings] = await reputationTree.getProof(user1Key); const remainingStake = REQUIRED_STAKE.sub(WAD.divn(1000)); - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, remainingStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, remainingStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, true, remainingStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, false, remainingStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); }); it("can execute after internally escalating a domain poll, if there is insufficient opposition", async () => { @@ -1293,7 +1326,7 @@ contract("Voting Reputation", (accounts) => { [user0Mask, user0Siblings] = await reputationTree.getProof(user0Key); const remainingStake = REQUIRED_STAKE.sub(WAD.divn(1000)); - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, remainingStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, true, remainingStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await forwardTime(STAKE_WINDOW, this); @@ -1309,7 +1342,7 @@ contract("Voting Reputation", (accounts) => { [user1Mask, user1Siblings] = await reputationTree.getProof(user1Key); const remainingStake = REQUIRED_STAKE.sub(WAD.divn(1000)); - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, remainingStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, false, remainingStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await forwardTime(STAKE_WINDOW, this); @@ -1339,8 +1372,8 @@ contract("Voting Reputation", (accounts) => { [user1Mask, user1Siblings] = await reputationTree.getProof(user1Key); const remainingStake = REQUIRED_STAKE.sub(WAD.divn(1000)); - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, true, remainingStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, 1, false, remainingStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, true, remainingStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, false, remainingStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await voting.respondToStake(pollId, true, CALL, { from: USER0 }); await voting.respondToStake(pollId, false, CALL, { from: USER1 }); From e1d77ae3ce1e1b2ae99a371636a5e901016b2bdc Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Tue, 30 Jun 2020 17:05:58 -0700 Subject: [PATCH 22/61] Convert votes from bool to uint256 --- contracts/extensions/VotingReputation.sol | 70 ++-- test/extensions/voting-rep.js | 380 +++++++++++----------- 2 files changed, 226 insertions(+), 224 deletions(-) diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index 017679f433..446de71d4c 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -33,11 +33,11 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { event ExtensionInitialised(); event ExtensionDeprecated(); event PollCreated(uint256 indexed pollId, uint256 indexed skillId); - event PollStaked(uint256 indexed pollId, address indexed staker, bool indexed side, uint256 amount); + event PollStaked(uint256 indexed pollId, address indexed staker, uint256 indexed vote, uint256 amount); event PollVoteSubmitted(uint256 indexed pollId, address indexed voter); - event PollVoteRevealed(uint256 indexed pollId, address indexed voter, bool indexed side); + event PollVoteRevealed(uint256 indexed pollId, address indexed voter, uint256 indexed vote); event PollExecuted(uint256 indexed pollId, bytes action, bool success); - event PollRewardClaimed(uint256 indexed pollId, address indexed staker, bool indexed side, uint256 amount); + event PollRewardClaimed(uint256 indexed pollId, address indexed staker, uint256 indexed vote, uint256 amount); // Constants uint256 constant UINT256_MAX = 2**256 - 1; @@ -130,7 +130,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { // Storage uint256 pollCount; mapping (uint256 => Poll) polls; - mapping (uint256 => mapping (address => mapping (bool => uint256))) stakes; + mapping (uint256 => mapping (address => mapping (uint256 => uint256))) stakes; mapping (address => mapping (uint256 => bytes32)) voteSecrets; @@ -181,7 +181,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { uint256 _pollId, uint256 _permissionDomainId, // For extension's arbitration permission uint256 _childSkillIndex, // For extension's arbitration permission - bool _vote, + uint256 _vote, uint256 _amount, bytes memory _key, bytes memory _value, @@ -191,6 +191,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { public { Poll storage poll = polls[_pollId]; + require(_vote <= 1, "voting-rep-bad-vote"); require(getPollState(_pollId) == PollState.Staking, "voting-rep-staking-closed"); require( @@ -199,13 +200,13 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { "voting-rep-competing-poll-exists" ); - uint256 amount = min(_amount, sub(getRequiredStake(_pollId), poll.stakes[toInt(_vote)])); + uint256 amount = min(_amount, sub(getRequiredStake(_pollId), poll.stakes[_vote])); uint256 stakerRep = checkReputation(_pollId, msg.sender, _key, _value, _branchMask, _siblings); require(add(stakes[_pollId][msg.sender][_vote], amount) <= stakerRep, "voting-rep-insufficient-rep"); require( - crowdFunding || add(poll.stakes[toInt(_vote)], amount) == getRequiredStake(_pollId), + crowdFunding || add(poll.stakes[_vote], amount) == getRequiredStake(_pollId), "voting-rep-stake-must-be-exact" ); @@ -215,16 +216,16 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { // Update the stake poll.unpaidRewards = add(poll.unpaidRewards, amount); - poll.stakes[toInt(_vote)] = add(poll.stakes[toInt(_vote)], amount); + poll.stakes[_vote] = add(poll.stakes[_vote], amount); stakes[_pollId][msg.sender][_vote] = add(stakes[_pollId][msg.sender][_vote], amount); // Update the lead if the stake is larger - if (stakes[_pollId][poll.leads[toInt(_vote)]][_vote] < amount) { - poll.leads[toInt(_vote)] = msg.sender; + if (stakes[_pollId][poll.leads[_vote]][_vote] < amount) { + poll.leads[_vote] = msg.sender; } // Activate poll to prevent competing polls from being activated - if (poll.stakes[YAY] == getRequiredStake(_pollId) && toInt(_vote) == YAY) { + if (poll.stakes[YAY] == getRequiredStake(_pollId) && _vote == YAY) { activePolls[hashAction(poll.action)] = _pollId; // Increment counter & extend claim delay if staking for an expenditure state change @@ -245,16 +246,16 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { emit PollStaked(_pollId, msg.sender, _vote, amount); } - function respondToStake(uint256 _pollId, bool _vote, Response _response) public { + function respondToStake(uint256 _pollId, uint256 _vote, Response _response) public { Poll storage poll = polls[_pollId]; uint256 requiredStake = getRequiredStake(_pollId); require(poll.stakes[YAY] == requiredStake && poll.stakes[NAY] == requiredStake, "voting-rep-not-fully-staked"); require(getPollState(_pollId) == PollState.Staking, "voting-rep-not-staking"); - require(poll.leads[toInt(_vote)] == msg.sender, "voting-rep-not-lead"); - require(poll.responses[toInt(_vote)] == Response.None, "voting-rep-already-responded"); + require(poll.leads[_vote] == msg.sender, "voting-rep-not-lead"); + require(poll.responses[_vote] == Response.None, "voting-rep-already-responded"); - poll.responses[toInt(_vote)] = _response; + poll.responses[_vote] = _response; if (poll.responses[YAY] != Response.None && poll.responses[NAY] != Response.None) { poll.lastEvent = now; @@ -271,7 +272,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { function revealVote( uint256 _pollId, bytes32 _salt, - bool _vote, + uint256 _vote, bytes memory _key, bytes memory _value, uint256 _branchMask, @@ -294,7 +295,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { // Increment the vote if poll in reveal, otherwise skip // NOTE: since there's no locking, we could just `require` PollState.Reveal if (getPollState(_pollId) == PollState.Reveal) { - poll.votes[toInt(_vote)] = add(poll.votes[toInt(_vote)], userRep); + poll.votes[_vote] = add(poll.votes[_vote], userRep); } uint256 pctReputation = wdiv(userRep, poll.skillRep); @@ -380,11 +381,12 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { } } + bool success; if (canExecute) { - executeCall(_pollId, poll.action); + success = executeCall(_pollId, poll.action); } - emit PollExecuted(_pollId, poll.action, canExecute); + emit PollExecuted(_pollId, poll.action, success); } function claimReward( @@ -392,7 +394,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { uint256 _permissionDomainId, // For extension's arbitration permission uint256 _childSkillIndex, // For extension's arbitration permission address _user, - bool _vote + uint256 _vote ) public { @@ -417,7 +419,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { poll.stakes[NAY] == getRequiredStake(_pollId) && poll.stakes[YAY] == getRequiredStake(_pollId) ) { - uint256 stakerVotes = poll.votes[toInt(_vote)]; + uint256 stakerVotes = poll.votes[_vote]; uint256 totalVotes = add(poll.votes[NAY], poll.votes[YAY]); uint256 winPercent = wdiv(stakerVotes, totalVotes); uint256 winShare = wmul(winPercent, 2 * WAD); @@ -425,13 +427,13 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { repPenalty = (winShare < WAD) ? sub(stake, wmul(winShare, stake)) : 0; // Your side fully staked, receive 10% (proportional) of loser's stake - } else if (poll.stakes[toInt(_vote)] == getRequiredStake(_pollId)) { - uint256 stakePercent = wdiv(stake, poll.stakes[toInt(_vote)]); - uint256 totalPenalty = wmul(poll.stakes[toInt(!_vote)], WAD / 10); + } else if (poll.stakes[_vote] == getRequiredStake(_pollId)) { + uint256 stakePercent = wdiv(stake, poll.stakes[_vote]); + uint256 totalPenalty = wmul(poll.stakes[flip(_vote)], WAD / 10); stakerReward = add(rewardStake, wmul(stakePercent, totalPenalty)); // Opponent's side fully staked, pay 10% penalty - } else if (poll.stakes[toInt(!_vote)] == getRequiredStake(_pollId)) { + } else if (poll.stakes[flip(_vote)] == getRequiredStake(_pollId)) { stakerReward = wmul(rewardStake, (WAD / 10) * 9); repPenalty = stake / 10; @@ -474,7 +476,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { return expenditurePollCounts[_expenditureHash]; } - function getStake(uint256 _pollId, address _staker, bool _vote) public view returns (uint256) { + function getStake(uint256 _pollId, address _staker, uint256 _vote) public view returns (uint256) { return stakes[_pollId][_staker][_vote]; } @@ -572,18 +574,18 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { emit PollCreated(pollCount, _skillId); } - function getVoteSecret(bytes32 _salt, bool _vote) internal pure returns (bytes32) { + function getVoteSecret(bytes32 _salt, uint256 _vote) internal pure returns (bytes32) { return keccak256(abi.encodePacked(_salt, _vote)); } - function toInt(bool _vote) internal pure returns (uint256) { - return _vote ? YAY : NAY; - } - function getRequiredStake(uint256 _pollId) internal view returns (uint256) { return wmul(polls[_pollId].skillRep, stakeFraction); } + function flip(uint256 _vote) internal pure returns (uint256) { + return 1 - _vote; + } + function checkReputation( uint256 _pollId, address _who, @@ -634,12 +636,10 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { } } - function executeCall(uint256 pollId, bytes memory action) internal { + function executeCall(uint256 pollId, bytes memory action) internal returns (bool success) { address target = polls[pollId].target; address to = (target == address(0x0)) ? address(colony) : target; - 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)) @@ -649,7 +649,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { success := call(gas, to, 0, add(action, 0x20), mload(action), 0, 0) } - // require(success, "voting-rep-call-failed"); + return success; } function getSig(bytes memory action) internal returns (bytes4 sig) { diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index 92eb7ba08e..5c18b10018 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -77,6 +77,9 @@ contract("Voting Reputation", (accounts) => { const SALT = soliditySha3(shortid.generate()); const FAKE = soliditySha3(shortid.generate()); + const NAY = 0; + const YAY = 1; + const STAKING = 0; const VOTING = 1; const REVEAL = 2; @@ -90,6 +93,7 @@ contract("Voting Reputation", (accounts) => { const ADDRESS_ZERO = ethers.constants.AddressZero; const REQUIRED_STAKE = WAD.muln(3).divn(1000); + const WAD32 = bn2bytes32(WAD); before(async () => { colonyNetwork = await setupColonyNetwork(); @@ -305,15 +309,15 @@ contract("Voting Reputation", (accounts) => { }); it("can stake on a poll", async () => { - await voting.stakePoll(pollId, 1, UINT256_MAX, true, 100, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, true, 100, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, 100, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, 100, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); const poll = await voting.getPoll(pollId); expect(poll.stakes[0]).to.be.zero; expect(poll.stakes[1]).to.eq.BN(200); - const stake0 = await voting.getStake(pollId, USER0, true); - const stake1 = await voting.getStake(pollId, USER1, true); + const stake0 = await voting.getStake(pollId, USER0, YAY); + const stake1 = await voting.getStake(pollId, USER1, YAY); expect(stake0).to.eq.BN(100); expect(stake1).to.eq.BN(100); }); @@ -322,26 +326,26 @@ contract("Voting Reputation", (accounts) => { let pollState = await voting.getPollState(pollId); expect(pollState).to.eq.BN(STAKING); - await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); pollState = await voting.getPollState(pollId); expect(pollState).to.eq.BN(STAKING); - await voting.stakePoll(pollId, 1, UINT256_MAX, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); pollState = await voting.getPollState(pollId); expect(pollState).to.eq.BN(STAKING); - await voting.respondToStake(pollId, true, CALL, { from: USER0 }); + await voting.respondToStake(pollId, YAY, CALL, { from: USER0 }); pollState = await voting.getPollState(pollId); expect(pollState).to.eq.BN(STAKING); - await voting.respondToStake(pollId, false, CALL, { from: USER1 }); + await voting.respondToStake(pollId, NAY, CALL, { from: USER1 }); pollState = await voting.getPollState(pollId); expect(pollState).to.eq.BN(VOTING); }); it("can go to a vote even if both sides do not call", async () => { - await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await forwardTime(STAKE_WINDOW, this); @@ -350,52 +354,59 @@ contract("Voting Reputation", (accounts) => { }); it("cannot execute if the YAY side stakes and folds", async () => { - await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await voting.respondToStake(pollId, true, FOLD, { from: USER0 }); + await voting.respondToStake(pollId, YAY, FOLD, { from: USER0 }); const pollState = await voting.getPollState(pollId); expect(pollState).to.eq.BN(FAILED); }); it("can execute if the NAY side stakes and folds", async () => { - await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await voting.respondToStake(pollId, false, FOLD, { from: USER1 }); + await voting.respondToStake(pollId, NAY, FOLD, { from: USER1 }); const pollState = await voting.getPollState(pollId); expect(pollState).to.eq.BN(EXECUTABLE); }); + it("cannot stake a nonexistent side", async () => { + await checkErrorRevert( + voting.stakePoll(pollId, 1, UINT256_MAX, 2, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), + "voting-rep-bad-vote" + ); + }); + it("cannot respond to a stake if not fully staked", async () => { - await checkErrorRevert(voting.respondToStake(pollId, true, CALL, { from: USER0 }), "voting-rep-not-fully-staked"); + await checkErrorRevert(voting.respondToStake(pollId, YAY, CALL, { from: USER0 }), "voting-rep-not-fully-staked"); }); it("cannot respond to a stake if not in the staking phase", async () => { - await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await forwardTime(STAKE_WINDOW, this); - await checkErrorRevert(voting.respondToStake(pollId, true, CALL, { from: USER0 }), "voting-rep-not-staking"); + await checkErrorRevert(voting.respondToStake(pollId, YAY, CALL, { from: USER0 }), "voting-rep-not-staking"); }); it("cannot respond to a stake if not the lead", async () => { - await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await checkErrorRevert(voting.respondToStake(pollId, true, CALL, { from: USER2 }), "voting-rep-not-lead"); + await checkErrorRevert(voting.respondToStake(pollId, YAY, CALL, { from: USER2 }), "voting-rep-not-lead"); }); it("cannot respond to a stake twice", async () => { - await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await voting.respondToStake(pollId, true, CALL, { from: USER0 }); + await voting.respondToStake(pollId, YAY, CALL, { from: USER0 }); - await checkErrorRevert(voting.respondToStake(pollId, true, CALL, { from: USER0 }), "voting-rep-already-responded"); + await checkErrorRevert(voting.respondToStake(pollId, YAY, CALL, { from: USER0 }), "voting-rep-already-responded"); }); it("cannot stake for an action while there is an active poll for the same action", async () => { @@ -406,7 +417,7 @@ contract("Voting Reputation", (accounts) => { expect(activePoll).to.be.zero; await forwardTime(STAKE_WINDOW / 2, this); - await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); activePoll = await voting.getActivePoll(soliditySha3(action)); expect(activePoll).to.eq.BN(pollId); @@ -415,7 +426,7 @@ contract("Voting Reputation", (accounts) => { const otherPollId = await voting.getPollCount(); await checkErrorRevert( - voting.stakePoll(otherPollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), + voting.stakePoll(otherPollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), "voting-rep-competing-poll-exists" ); @@ -426,7 +437,7 @@ contract("Voting Reputation", (accounts) => { activePoll = await voting.getActivePoll(soliditySha3(action)); expect(activePoll).to.be.zero; - await voting.stakePoll(otherPollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(otherPollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); activePoll = await voting.getActivePoll(soliditySha3(action)); expect(activePoll).to.eq.BN(otherPollId); @@ -437,15 +448,7 @@ contract("Voting Reputation", (accounts) => { const expenditureId = await colony.getExpenditureCount(); // Set payoutModifier to 1 for expenditure slot 0 - const action = await encodeTxData(colony, "setExpenditureState", [ - 1, - UINT256_MAX, - expenditureId, - 25, - [true], - [bn2bytes32(new BN(3))], - bn2bytes32(WAD), - ]); + const action = await encodeTxData(colony, "setExpenditureState", [1, UINT256_MAX, expenditureId, 25, [true], [bn2bytes32(new BN(3))], WAD32]); await voting.createDomainPoll(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); pollId = await voting.getPollCount(); @@ -458,7 +461,7 @@ contract("Voting Reputation", (accounts) => { expenditureSlot = await colony.getExpenditure(expenditureId); expect(expenditureSlot.globalClaimDelay).to.be.zero; - await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); expenditurePollCount = await voting.getExpenditurePollCount(soliditySha3(expenditureId)); expect(expenditurePollCount).to.eq.BN(1); @@ -479,7 +482,7 @@ contract("Voting Reputation", (accounts) => { 26, [false, true], ["0x0", bn2bytes32(new BN(2))], - bn2bytes32(WAD), + WAD32, ]); await voting.createDomainPoll(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); @@ -493,7 +496,7 @@ contract("Voting Reputation", (accounts) => { expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); expect(expenditureSlot.claimDelay).to.be.zero; - await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); expenditurePollCount = await voting.getExpenditurePollCount(soliditySha3(expenditureId, 0)); expect(expenditurePollCount).to.eq.BN(1); @@ -514,7 +517,7 @@ contract("Voting Reputation", (accounts) => { 27, [false, false], ["0x0", bn2bytes32(new BN(token.address.slice(2), 16))], - bn2bytes32(WAD), + WAD32, ]); await voting.createDomainPoll(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); @@ -528,7 +531,7 @@ contract("Voting Reputation", (accounts) => { expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); expect(expenditureSlot.claimDelay).to.be.zero; - await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); expenditurePollCount = await voting.getExpenditurePollCount(soliditySha3(expenditureId, 0)); expect(expenditurePollCount).to.eq.BN(1); @@ -550,7 +553,7 @@ contract("Voting Reputation", (accounts) => { 26, [false, true], ["0x0", bn2bytes32(new BN(2))], - bn2bytes32(WAD), + WAD32, ]); // Set payout to WAD for expenditure slot 0, internal token @@ -561,7 +564,7 @@ contract("Voting Reputation", (accounts) => { 27, [false, false], ["0x0", bn2bytes32(new BN(token.address.slice(2), 16))], - bn2bytes32(WAD), + WAD32, ]); await voting.createDomainPoll(1, UINT256_MAX, ADDRESS_ZERO, action1, domain1Key, domain1Value, domain1Mask, domain1Siblings); @@ -578,8 +581,8 @@ contract("Voting Reputation", (accounts) => { expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); expect(expenditureSlot.claimDelay).to.be.zero; - await voting.stakePoll(pollId1, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId2, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId1, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId2, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); expenditurePollCount = await voting.getExpenditurePollCount(expenditureHash); expect(expenditurePollCount).to.eq.BN(2); @@ -607,7 +610,7 @@ contract("Voting Reputation", (accounts) => { it("cannot stake with someone else's reputation", async () => { await checkErrorRevert( - voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER1 }), + voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER1 }), "voting-rep-invalid-user-address" ); }); @@ -618,7 +621,7 @@ contract("Voting Reputation", (accounts) => { const [user2Mask, user2Siblings] = await reputationTree.getProof(user2Key); await checkErrorRevert( - voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user2Key, user2Value, user2Mask, user2Siblings, { from: USER2 }), + voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user2Key, user2Value, user2Mask, user2Siblings, { from: USER2 }), "voting-rep-insufficient-rep" ); }); @@ -627,12 +630,12 @@ contract("Voting Reputation", (accounts) => { await forwardTime(STAKE_WINDOW, this); await checkErrorRevert( - voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), + voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), "voting-rep-staking-closed" ); await checkErrorRevert( - voting.stakePoll(pollId, 1, UINT256_MAX, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }), + voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }), "voting-rep-staking-closed" ); }); @@ -649,17 +652,17 @@ contract("Voting Reputation", (accounts) => { pollId = await voting.getPollCount(); await checkErrorRevert( - voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE.subn(1), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), + voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE.subn(1), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), "voting-rep-stake-must-be-exact" ); }); it("can go to a vote even if the nay side fails to respond", async () => { - await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); // Yay side responds - await voting.respondToStake(pollId, true, CALL, { from: USER0 }); + await voting.respondToStake(pollId, YAY, CALL, { from: USER0 }); let pollState; pollState = await voting.getPollState(pollId); @@ -677,11 +680,11 @@ contract("Voting Reputation", (accounts) => { }); it("can go to a vote even if the yay side fails to respond", async () => { - await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); // Nay side responds - await voting.respondToStake(pollId, false, CALL, { from: USER1 }); + await voting.respondToStake(pollId, NAY, CALL, { from: USER1 }); let pollState; pollState = await voting.getPollState(pollId); @@ -707,29 +710,29 @@ contract("Voting Reputation", (accounts) => { await voting.createRootPoll(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); pollId = await voting.getPollCount(); - await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await voting.respondToStake(pollId, true, CALL, { from: USER0 }); - await voting.respondToStake(pollId, false, CALL, { from: USER1 }); + await voting.respondToStake(pollId, YAY, CALL, { from: USER0 }); + await voting.respondToStake(pollId, NAY, CALL, { from: USER1 }); }); it("can rate and reveal for a poll", async () => { - await voting.submitVote(pollId, soliditySha3(SALT, false), { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, NAY), { from: USER0 }); await forwardTime(VOTE_WINDOW, this); - await voting.revealVote(pollId, SALT, false, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.revealVote(pollId, SALT, NAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); }); it("can tally votes from two users", async () => { - await voting.submitVote(pollId, soliditySha3(SALT, false), { from: USER0 }); - await voting.submitVote(pollId, soliditySha3(SALT, true), { from: USER1 }); + await voting.submitVote(pollId, soliditySha3(SALT, NAY), { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, YAY), { from: USER1 }); await forwardTime(VOTE_WINDOW, this); - await voting.revealVote(pollId, SALT, false, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.revealVote(pollId, SALT, true, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.revealVote(pollId, SALT, NAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.revealVote(pollId, SALT, YAY, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); // See final counts const { votes } = await voting.getPoll(pollId); @@ -738,28 +741,28 @@ contract("Voting Reputation", (accounts) => { }); it("can update votes, but just the last one counts", async () => { - await voting.submitVote(pollId, soliditySha3(SALT, false), { from: USER0 }); - await voting.submitVote(pollId, soliditySha3(SALT, true), { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, NAY), { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, YAY), { from: USER0 }); await forwardTime(VOTE_WINDOW, this); // Revealing first vote fails await checkErrorRevert( - voting.revealVote(pollId, SALT, false, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), + voting.revealVote(pollId, SALT, NAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), "voting-rep-secret-no-match" ); // Revealing second succeeds - await voting.revealVote(pollId, SALT, true, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.revealVote(pollId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); }); it("can reveal votes after poll closes, but doesn't count", async () => { - await voting.submitVote(pollId, soliditySha3(SALT, false), { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, NAY), { from: USER0 }); await forwardTime(VOTE_WINDOW, this); await forwardTime(REVEAL_WINDOW, this); - await voting.revealVote(pollId, SALT, false, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.revealVote(pollId, SALT, NAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); // Vote didn't count const { votes } = await voting.getPoll(pollId); @@ -768,20 +771,20 @@ contract("Voting Reputation", (accounts) => { }); it("cannot reveal a vote twice, and so cannot vote twice", async () => { - await voting.submitVote(pollId, soliditySha3(SALT, false), { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, NAY), { from: USER0 }); await forwardTime(VOTE_WINDOW, this); - await voting.revealVote(pollId, SALT, false, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.revealVote(pollId, SALT, NAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await checkErrorRevert( - voting.revealVote(pollId, SALT, false, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), + voting.revealVote(pollId, SALT, NAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), "voting-rep-secret-no-match" ); }); it("can vote in two polls with two reputation states, with different proofs", async () => { - await voting.submitVote(pollId, soliditySha3(SALT, false), { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, NAY), { from: USER0 }); // Update reputation state const user0Value2 = makeReputationValue(WAD.muln(3), 2); @@ -803,65 +806,62 @@ contract("Voting Reputation", (accounts) => { // Create new poll with new reputation state await voting.createRootPoll(ADDRESS_ZERO, FAKE, domain1Key, domain1Value, domain1Mask2, domain1Siblings2); const pollId2 = await voting.getPollCount(); - await voting.stakePoll(pollId2, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value2, user0Mask2, user0Siblings2, { from: USER0 }); - await voting.stakePoll(pollId2, 1, UINT256_MAX, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask2, user1Siblings2, { from: USER1 }); - await voting.respondToStake(pollId2, true, CALL, { from: USER0 }); - await voting.respondToStake(pollId2, false, CALL, { from: USER1 }); + await voting.stakePoll(pollId2, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value2, user0Mask2, user0Siblings2, { from: USER0 }); + await voting.stakePoll(pollId2, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask2, user1Siblings2, { from: USER1 }); + await voting.respondToStake(pollId2, YAY, CALL, { from: USER0 }); + await voting.respondToStake(pollId2, NAY, CALL, { from: USER1 }); - await voting.submitVote(pollId2, soliditySha3(SALT, false), { from: USER0 }); + await voting.submitVote(pollId2, soliditySha3(SALT, NAY), { from: USER0 }); await forwardTime(VOTE_WINDOW, this); - await voting.revealVote(pollId, SALT, false, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.revealVote(pollId2, SALT, false, user0Key, user0Value2, user0Mask2, user0Siblings2, { from: USER0 }); + await voting.revealVote(pollId, SALT, NAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.revealVote(pollId2, SALT, NAY, user0Key, user0Value2, user0Mask2, user0Siblings2, { from: USER0 }); }); it("cannot submit a vote if voting is closed", async () => { await forwardTime(VOTE_WINDOW, this); - await checkErrorRevert(voting.submitVote(pollId, soliditySha3(SALT, false)), "voting-rep-poll-not-open"); + await checkErrorRevert(voting.submitVote(pollId, soliditySha3(SALT, NAY)), "voting-rep-poll-not-open"); }); it("cannot reveal a vote if voting is open", async () => { - await voting.submitVote(pollId, soliditySha3(SALT, false)); - await checkErrorRevert(voting.revealVote(pollId, SALT, true, FAKE, FAKE, 0, []), "voting-rep-poll-still-open"); + await voting.submitVote(pollId, soliditySha3(SALT, NAY)); + await checkErrorRevert(voting.revealVote(pollId, SALT, YAY, FAKE, FAKE, 0, []), "voting-rep-poll-still-open"); }); it("cannot reveal a vote with a bad secret", async () => { - await voting.submitVote(pollId, soliditySha3(SALT, false)); + await voting.submitVote(pollId, soliditySha3(SALT, NAY)); await forwardTime(VOTE_WINDOW, this); - await checkErrorRevert(voting.revealVote(pollId, SALT, true, FAKE, FAKE, 0, []), "voting-rep-secret-no-match"); + await checkErrorRevert(voting.revealVote(pollId, SALT, YAY, FAKE, FAKE, 0, []), "voting-rep-secret-no-match"); }); it("cannot reveal a vote with a bad proof", async () => { - await voting.submitVote(pollId, soliditySha3(SALT, false), { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, NAY), { from: USER0 }); await forwardTime(VOTE_WINDOW, this); // Invalid proof (wrong root hash) - await checkErrorRevert(voting.revealVote(pollId, SALT, false, FAKE, FAKE, 0, [], { from: USER0 }), "voting-rep-invalid-root-hash"); + await checkErrorRevert(voting.revealVote(pollId, SALT, NAY, FAKE, FAKE, 0, [], { from: USER0 }), "voting-rep-invalid-root-hash"); // Invalid colony address let key, value, mask, siblings; // eslint-disable-line one-var key = makeReputationKey(metaColony.address, domain1.skillId, USER0); value = makeReputationValue(WAD, 3); [mask, siblings] = await reputationTree.getProof(key); - await checkErrorRevert( - voting.revealVote(pollId, SALT, false, key, value, mask, siblings, { from: USER0 }), - "voting-rep-invalid-colony-address" - ); + await checkErrorRevert(voting.revealVote(pollId, SALT, NAY, key, value, mask, siblings, { from: USER0 }), "voting-rep-invalid-colony-address"); // Invalid skill id key = makeReputationKey(colony.address, 1234, USER0); value = makeReputationValue(WAD, 4); [mask, siblings] = await reputationTree.getProof(key); - await checkErrorRevert(voting.revealVote(pollId, SALT, false, key, value, mask, siblings, { from: USER0 }), "voting-rep-invalid-skill-id"); + await checkErrorRevert(voting.revealVote(pollId, SALT, NAY, key, value, mask, siblings, { from: USER0 }), "voting-rep-invalid-skill-id"); // Invalid user address await checkErrorRevert( - voting.revealVote(pollId, SALT, false, user1Key, user1Value, user1Mask, user1Siblings, { from: USER0 }), + voting.revealVote(pollId, SALT, NAY, user1Key, user1Value, user1Mask, user1Siblings, { from: USER0 }), "voting-rep-invalid-user-address" ); }); @@ -877,7 +877,7 @@ contract("Voting Reputation", (accounts) => { }); it("cannot take an action if there is insufficient support", async () => { - await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE.subn(1), user0Key, user0Value, user0Mask, user0Siblings, { + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE.subn(1), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0, }); @@ -887,8 +887,8 @@ contract("Voting Reputation", (accounts) => { }); it("can take an action if there is insufficient opposition", async () => { - await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, false, REQUIRED_STAKE.subn(1), user1Key, user1Value, user1Mask, user1Siblings, { + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE.subn(1), user1Key, user1Value, user1Mask, user1Siblings, { from: USER1, }); @@ -906,7 +906,7 @@ contract("Voting Reputation", (accounts) => { await voting.createRootPoll(otherColony.address, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); pollId = await voting.getPollCount(); - await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await forwardTime(STAKE_WINDOW, this); @@ -921,15 +921,15 @@ contract("Voting Reputation", (accounts) => { it("cannot take an action during staking or voting", async () => { let pollState; - await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); pollState = await voting.getPollState(pollId); expect(pollState).to.eq.BN(STAKING); await checkErrorRevert(voting.executePoll(pollId), "voting-rep-poll-not-executable"); - await voting.stakePoll(pollId, 1, UINT256_MAX, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await voting.respondToStake(pollId, true, CALL, { from: USER0 }); - await voting.respondToStake(pollId, false, CALL, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.respondToStake(pollId, YAY, CALL, { from: USER0 }); + await voting.respondToStake(pollId, NAY, CALL, { from: USER1 }); pollState = await voting.getPollState(pollId); expect(pollState).to.eq.BN(VOTING); @@ -937,7 +937,7 @@ contract("Voting Reputation", (accounts) => { }); it("cannot take an action twice", async () => { - await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await forwardTime(STAKE_WINDOW, this); @@ -948,16 +948,16 @@ contract("Voting Reputation", (accounts) => { }); it("can take an action if the poll passes", async () => { - await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await voting.respondToStake(pollId, true, CALL, { from: USER0 }); - await voting.respondToStake(pollId, false, CALL, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.respondToStake(pollId, YAY, CALL, { from: USER0 }); + await voting.respondToStake(pollId, NAY, CALL, { from: USER1 }); - await voting.submitVote(pollId, soliditySha3(SALT, true), { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, YAY), { from: USER0 }); await forwardTime(VOTE_WINDOW, this); - await voting.revealVote(pollId, SALT, true, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.revealVote(pollId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await forwardTime(REVEAL_WINDOW, this); @@ -970,16 +970,16 @@ contract("Voting Reputation", (accounts) => { }); it("cannot take an action if the poll fails", async () => { - await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await voting.respondToStake(pollId, true, CALL, { from: USER0 }); - await voting.respondToStake(pollId, false, CALL, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.respondToStake(pollId, YAY, CALL, { from: USER0 }); + await voting.respondToStake(pollId, NAY, CALL, { from: USER1 }); - await voting.submitVote(pollId, soliditySha3(SALT, false), { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, NAY), { from: USER0 }); await forwardTime(VOTE_WINDOW, this); - await voting.revealVote(pollId, SALT, false, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.revealVote(pollId, SALT, NAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await forwardTime(REVEAL_WINDOW, this); await forwardTime(STAKE_WINDOW, this); @@ -989,27 +989,29 @@ contract("Voting Reputation", (accounts) => { }); it("cannot take an action if there is insufficient voting power (state change actions)", async () => { - // Set first slot of first expenditure struct to 0x0 - const action = await encodeTxData(colony, "setExpenditureState", [1, UINT256_MAX, 1, 0, [], [], ethers.constants.HashZero]); - let logs; + // Set globalClaimDelay to WAD + await colony.makeExpenditure(1, UINT256_MAX, 1); + const expenditureId = await colony.getExpenditureCount(); + const action = await encodeTxData(colony, "setExpenditureState", [1, UINT256_MAX, expenditureId, 25, [true], [bn2bytes32(new BN(4))], WAD32]); await voting.createDomainPoll(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); const pollId1 = await voting.getPollCount(); - await voting.stakePoll(pollId1, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId1, 1, UINT256_MAX, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await voting.respondToStake(pollId1, true, CALL, { from: USER0 }); - await voting.respondToStake(pollId1, false, CALL, { from: USER1 }); + await voting.stakePoll(pollId1, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId1, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.respondToStake(pollId1, YAY, CALL, { from: USER0 }); + await voting.respondToStake(pollId1, NAY, CALL, { from: USER1 }); - await voting.submitVote(pollId1, soliditySha3(SALT, true), { from: USER0 }); + await voting.submitVote(pollId1, soliditySha3(SALT, YAY), { from: USER0 }); await forwardTime(VOTE_WINDOW, this); - await voting.revealVote(pollId1, SALT, true, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.revealVote(pollId1, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await forwardTime(REVEAL_WINDOW, this); await forwardTime(STAKE_WINDOW, this); + let logs; ({ logs } = await voting.executePoll(pollId1)); expect(logs[0].args.success).to.be.true; @@ -1017,16 +1019,16 @@ contract("Voting Reputation", (accounts) => { await voting.createDomainPoll(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); const pollId2 = await voting.getPollCount(); - await voting.stakePoll(pollId2, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId2, 1, UINT256_MAX, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await voting.respondToStake(pollId2, true, CALL, { from: USER0 }); - await voting.respondToStake(pollId2, false, CALL, { from: USER1 }); + await voting.stakePoll(pollId2, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId2, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.respondToStake(pollId2, YAY, CALL, { from: USER0 }); + await voting.respondToStake(pollId2, NAY, CALL, { from: USER1 }); - await voting.submitVote(pollId2, soliditySha3(SALT, true), { from: USER0 }); + await voting.submitVote(pollId2, soliditySha3(SALT, YAY), { from: USER0 }); await forwardTime(VOTE_WINDOW, this); - await voting.revealVote(pollId2, SALT, true, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.revealVote(pollId2, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await forwardTime(REVEAL_WINDOW, this); await forwardTime(STAKE_WINDOW, this); @@ -1041,7 +1043,7 @@ contract("Voting Reputation", (accounts) => { await voting.createDomainPoll(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); pollId = await voting.getPollCount(); - await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await forwardTime(STAKE_WINDOW, this); @@ -1057,16 +1059,16 @@ contract("Voting Reputation", (accounts) => { await voting.createDomainPoll(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); pollId = await voting.getPollCount(); - await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await voting.respondToStake(pollId, true, CALL, { from: USER0 }); - await voting.respondToStake(pollId, false, CALL, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.respondToStake(pollId, YAY, CALL, { from: USER0 }); + await voting.respondToStake(pollId, NAY, CALL, { from: USER1 }); - await voting.submitVote(pollId, soliditySha3(SALT, true), { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, YAY), { from: USER0 }); await forwardTime(VOTE_WINDOW, this); - await voting.revealVote(pollId, SALT, true, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.revealVote(pollId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await forwardTime(REVEAL_WINDOW, this); await forwardTime(STAKE_WINDOW, this); @@ -1092,8 +1094,8 @@ contract("Voting Reputation", (accounts) => { const repCycle = await IReputationMiningCycle.at(addr); const numEntriesPrev = await repCycle.getReputationUpdateLogLength(); - await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, false, REQUIRED_STAKE.divn(2), user1Key, user1Value, user1Mask, user1Siblings, { + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE.divn(2), user1Key, user1Value, user1Mask, user1Siblings, { from: USER1, }); @@ -1104,8 +1106,8 @@ contract("Voting Reputation", (accounts) => { const user0LockPre = await tokenLocking.getUserLock(token.address, USER0); const user1LockPre = await tokenLocking.getUserLock(token.address, USER1); - await voting.claimReward(pollId, 1, UINT256_MAX, USER0, true); - await voting.claimReward(pollId, 1, UINT256_MAX, USER1, false); + await voting.claimReward(pollId, 1, UINT256_MAX, USER0, YAY); + await voting.claimReward(pollId, 1, UINT256_MAX, USER1, NAY); const user0LockPost = await tokenLocking.getUserLock(token.address, USER0); const user1LockPost = await tokenLocking.getUserLock(token.address, USER1); @@ -1131,19 +1133,19 @@ contract("Voting Reputation", (accounts) => { const repCycle = await IReputationMiningCycle.at(addr); const numEntriesPrev = await repCycle.getReputationUpdateLogLength(); - await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await voting.respondToStake(pollId, true, CALL, { from: USER0 }); - await voting.respondToStake(pollId, false, CALL, { from: USER1 }); + await voting.respondToStake(pollId, YAY, CALL, { from: USER0 }); + await voting.respondToStake(pollId, NAY, CALL, { from: USER1 }); - await voting.submitVote(pollId, soliditySha3(SALT, true), { from: USER0 }); - await voting.submitVote(pollId, soliditySha3(SALT, false), { from: USER1 }); + await voting.submitVote(pollId, soliditySha3(SALT, YAY), { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, NAY), { from: USER1 }); await forwardTime(VOTE_WINDOW, this); - await voting.revealVote(pollId, SALT, true, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.revealVote(pollId, SALT, false, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.revealVote(pollId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.revealVote(pollId, SALT, NAY, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await forwardTime(REVEAL_WINDOW, this); await forwardTime(STAKE_WINDOW, this); @@ -1153,8 +1155,8 @@ contract("Voting Reputation", (accounts) => { const user0LockPre = await tokenLocking.getUserLock(token.address, USER0); const user1LockPre = await tokenLocking.getUserLock(token.address, USER1); - await voting.claimReward(pollId, 1, UINT256_MAX, USER0, true); - await voting.claimReward(pollId, 1, UINT256_MAX, USER1, false); + await voting.claimReward(pollId, 1, UINT256_MAX, USER0, YAY); + await voting.claimReward(pollId, 1, UINT256_MAX, USER1, NAY); const user0LockPost = await tokenLocking.getUserLock(token.address, USER0); const user1LockPost = await tokenLocking.getUserLock(token.address, USER1); @@ -1180,16 +1182,16 @@ contract("Voting Reputation", (accounts) => { const repCycle = await IReputationMiningCycle.at(addr); const numEntriesPrev = await repCycle.getReputationUpdateLogLength(); - await voting.stakePoll(pollId, 1, UINT256_MAX, true, 100, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, false, 100, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, 100, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, 100, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await forwardTime(STAKE_WINDOW, this); const user0LockPre = await tokenLocking.getUserLock(token.address, USER0); const user1LockPre = await tokenLocking.getUserLock(token.address, USER1); - await voting.claimReward(pollId, 1, UINT256_MAX, USER0, true); - await voting.claimReward(pollId, 1, UINT256_MAX, USER1, false); + await voting.claimReward(pollId, 1, UINT256_MAX, USER0, YAY); + await voting.claimReward(pollId, 1, UINT256_MAX, USER1, NAY); const numEntriesPost = await repCycle.getReputationUpdateLogLength(); @@ -1202,32 +1204,32 @@ contract("Voting Reputation", (accounts) => { }); it("cannot claim rewards twice", async () => { - await voting.stakePoll(pollId, 1, UINT256_MAX, true, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, false, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await voting.respondToStake(pollId, true, CALL, { from: USER0 }); - await voting.respondToStake(pollId, false, CALL, { from: USER1 }); + await voting.respondToStake(pollId, YAY, CALL, { from: USER0 }); + await voting.respondToStake(pollId, NAY, CALL, { from: USER1 }); - await voting.submitVote(pollId, soliditySha3(SALT, true), { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, YAY), { from: USER0 }); await forwardTime(VOTE_WINDOW, this); - await voting.revealVote(pollId, SALT, true, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.revealVote(pollId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await forwardTime(REVEAL_WINDOW, this); await forwardTime(STAKE_WINDOW, this); await voting.executePoll(pollId); - await voting.claimReward(pollId, 1, UINT256_MAX, USER0, true); + await voting.claimReward(pollId, 1, UINT256_MAX, USER0, YAY); const userLock0 = await tokenLocking.getUserLock(token.address, USER0); - await voting.claimReward(pollId, 1, UINT256_MAX, USER0, true); + await voting.claimReward(pollId, 1, UINT256_MAX, USER0, YAY); const userLock1 = await tokenLocking.getUserLock(token.address, USER0); expect(userLock0.balance).to.eq.BN(userLock1.balance); }); it("cannot claim rewards before a poll is executed", async () => { - await checkErrorRevert(voting.claimReward(pollId, 1, UINT256_MAX, USER0, true), "voting-rep-not-failed-or-executed"); + await checkErrorRevert(voting.claimReward(pollId, 1, UINT256_MAX, USER0, YAY), "voting-rep-not-failed-or-executed"); }); }); @@ -1254,20 +1256,20 @@ contract("Voting Reputation", (accounts) => { await colony.approveStake(voting.address, 2, WAD, { from: USER0 }); await colony.approveStake(voting.address, 2, WAD, { from: USER1 }); - await voting.stakePoll(pollId, 1, 0, true, WAD.divn(1000), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, 0, false, WAD.divn(1000), user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, 0, YAY, WAD.divn(1000), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, 0, NAY, WAD.divn(1000), user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await voting.respondToStake(pollId, true, CALL, { from: USER0 }); - await voting.respondToStake(pollId, false, CALL, { from: USER1 }); + await voting.respondToStake(pollId, YAY, CALL, { from: USER0 }); + await voting.respondToStake(pollId, NAY, CALL, { from: USER1 }); // Note that this is a passing vote - await voting.submitVote(pollId, soliditySha3(SALT, false), { from: USER0 }); - await voting.submitVote(pollId, soliditySha3(SALT, true), { from: USER1 }); + await voting.submitVote(pollId, soliditySha3(SALT, NAY), { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, YAY), { from: USER1 }); await forwardTime(VOTE_WINDOW, this); - await voting.revealVote(pollId, SALT, false, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.revealVote(pollId, SALT, true, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.revealVote(pollId, SALT, NAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.revealVote(pollId, SALT, YAY, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await forwardTime(REVEAL_WINDOW, this); }); @@ -1314,8 +1316,8 @@ contract("Voting Reputation", (accounts) => { [user1Mask, user1Siblings] = await reputationTree.getProof(user1Key); const remainingStake = REQUIRED_STAKE.sub(WAD.divn(1000)); - await voting.stakePoll(pollId, 1, UINT256_MAX, true, remainingStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, false, remainingStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, remainingStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, remainingStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); }); it("can execute after internally escalating a domain poll, if there is insufficient opposition", async () => { @@ -1326,7 +1328,7 @@ contract("Voting Reputation", (accounts) => { [user0Mask, user0Siblings] = await reputationTree.getProof(user0Key); const remainingStake = REQUIRED_STAKE.sub(WAD.divn(1000)); - await voting.stakePoll(pollId, 1, UINT256_MAX, true, remainingStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, remainingStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await forwardTime(STAKE_WINDOW, this); @@ -1342,7 +1344,7 @@ contract("Voting Reputation", (accounts) => { [user1Mask, user1Siblings] = await reputationTree.getProof(user1Key); const remainingStake = REQUIRED_STAKE.sub(WAD.divn(1000)); - await voting.stakePoll(pollId, 1, UINT256_MAX, false, remainingStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, remainingStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await forwardTime(STAKE_WINDOW, this); @@ -1372,20 +1374,20 @@ contract("Voting Reputation", (accounts) => { [user1Mask, user1Siblings] = await reputationTree.getProof(user1Key); const remainingStake = REQUIRED_STAKE.sub(WAD.divn(1000)); - await voting.stakePoll(pollId, 1, UINT256_MAX, true, remainingStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, false, remainingStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, remainingStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, remainingStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await voting.respondToStake(pollId, true, CALL, { from: USER0 }); - await voting.respondToStake(pollId, false, CALL, { from: USER1 }); + await voting.respondToStake(pollId, YAY, CALL, { from: USER0 }); + await voting.respondToStake(pollId, NAY, CALL, { from: USER1 }); // Make the vote fail this time (everyone votes against) - await voting.submitVote(pollId, soliditySha3(SALT, false), { from: USER0 }); - await voting.submitVote(pollId, soliditySha3(SALT, false), { from: USER1 }); + await voting.submitVote(pollId, soliditySha3(SALT, NAY), { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, NAY), { from: USER1 }); await forwardTime(VOTE_WINDOW, this); - await voting.revealVote(pollId, SALT, false, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.revealVote(pollId, SALT, false, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.revealVote(pollId, SALT, NAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.revealVote(pollId, SALT, NAY, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await forwardTime(REVEAL_WINDOW, this); await forwardTime(STAKE_WINDOW, this); From 3bbddcf2430fff213923287e031dd15d5dbb0f70 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Wed, 1 Jul 2020 08:30:58 -0700 Subject: [PATCH 23/61] Respond to review comments II --- test/extensions/voting-rep.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index 5c18b10018..452c450c25 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -898,6 +898,20 @@ contract("Voting Reputation", (accounts) => { expect(logs[0].args.success).to.be.true; }); + it("can take an action with a return value", async () => { + // Returns a uint256 + const action = await encodeTxData(colony, "version", []); + await voting.createRootPoll(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + pollId = await voting.getPollCount(); + + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + + await forwardTime(STAKE_WINDOW, this); + + const { logs } = await voting.executePoll(pollId); + expect(logs[0].args.success).to.be.true; + }); + it("can take an action with an arbitrary target", async () => { const { colony: otherColony } = await setupRandomColony(colonyNetwork); await token.mint(otherColony.address, WAD, { from: USER0 }); @@ -919,6 +933,19 @@ contract("Voting Reputation", (accounts) => { expect(balanceAfter).to.eq.BN(WAD); }); + it("can take a nonexistent action", async () => { + const action = soliditySha3("foo"); + await voting.createRootPoll(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + pollId = await voting.getPollCount(); + + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + + await forwardTime(STAKE_WINDOW, this); + + const { logs } = await voting.executePoll(pollId); + expect(logs[0].args.success).to.be.false; + }); + it("cannot take an action during staking or voting", async () => { let pollState; await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); From 0a4a2a0510732fd441e4f0b30a0e6668c6d91183 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Mon, 6 Jul 2020 17:33:41 -0700 Subject: [PATCH 24/61] Respond to review comments III --- contracts/extensions/VotingReputation.sol | 176 ++++++++---- test/extensions/voting-rep.js | 325 ++++++++++++---------- 2 files changed, 297 insertions(+), 204 deletions(-) diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index 446de71d4c..6e94ae3f8a 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -41,12 +41,15 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { // Constants uint256 constant UINT256_MAX = 2**256 - 1; + uint256 constant UINT128_MAX = 2**128 - 1; + uint256 constant NAY = 0; uint256 constant YAY = 1; - uint256 constant STAKE_PERIOD = 3 days; - uint256 constant VOTE_PERIOD = 2 days; - uint256 constant REVEAL_PERIOD = 2 days; + uint256 constant CREATE = 0; + uint256 constant STAKE_END = 1; + uint256 constant SUBMIT_END = 2; + uint256 constant REVEAL_END = 3; bytes4 constant CHANGE_FUNCTION = bytes4( keccak256("setExpenditureState(uint256,uint256,uint256,uint256,bool[],bytes32[],bytes32)") @@ -63,9 +66,15 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { address token; uint256 stakeFraction; // Percent of domain reputation needed for staking - uint256 voterRewardFraction; // Percent of stake paid out to voters as rewards + uint256 minStakeFraction; // Minimum stake as percent of required stake (100% means single-staker) + + uint256 maxVoteFraction; // The percent of total domain rep we need before closing the vote + uint256 voterRewardFraction; // Percent of stake paid out to voters as rewards (immediately taken from the stake) uint256 votePowerFraction; // Percent of domain rep used as vote power if no-contest - bool crowdFunding; // Flag allowing or disallowing crowdfunding polls + + uint256 stakePeriod; // Length of time for staking + uint256 submitPeriod; // Length of time for submitting votes + uint256 revealPeriod; // Length of time for revealing votes constructor(address _colony) public { colony = IColony(_colony); @@ -76,9 +85,13 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { function initialise( uint256 _stakeFraction, + uint256 _minStakeFraction, + uint256 _maxVoteFraction, uint256 _voterRewardFraction, uint256 _votePowerFraction, - bool _crowdFunding + uint256 _stakePeriod, + uint256 _submitPeriod, + uint256 _revealPeriod ) public { @@ -86,15 +99,28 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { require(state == ExtensionState.Deployed, "voting-rep-already-initialised"); require(_stakeFraction <= WAD, "voting-rep-must-be-wad"); + require(_minStakeFraction <= WAD, "voting-rep-must-be-wad"); + + require(_maxVoteFraction <= WAD, "voting-rep-must-be-wad"); require(_voterRewardFraction <= WAD, "voting-rep-must-be-wad"); require(_votePowerFraction <= WAD, "voting-rep-must-be-wad"); + require(_stakePeriod <= 365 days, "voting-rep-period-too-long"); + require(_submitPeriod <= 365 days, "voting-rep-period-too-long"); + require(_revealPeriod <= 365 days, "voting-rep-period-too-long"); + state = ExtensionState.Active; stakeFraction = _stakeFraction; + minStakeFraction = _minStakeFraction; + + maxVoteFraction = _maxVoteFraction; voterRewardFraction = _voterRewardFraction; votePowerFraction = _votePowerFraction; - crowdFunding = _crowdFunding; + + stakePeriod = _stakePeriod; + submitPeriod = _submitPeriod; + revealPeriod = _revealPeriod; emit ExtensionInitialised(); } @@ -108,15 +134,17 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { } // Data structures - enum PollState { Staking, Voting, Reveal, Closed, Executable, Executed, Failed } + enum PollState { Staking, Submit, Reveal, Closed, Executable, Executed, Failed } enum Response { None, Fold, Call } struct Poll { - uint256 lastEvent; // Set at creation / escalation & when staked + responded + uint256[4] events; // Creation, Staking, Submission, Revelation bytes32 rootHash; uint256 domainId; uint256 skillId; uint256 skillRep; + uint256 repSubmitted; + uint256 repRevealed; uint256 unpaidRewards; uint256[2] stakes; // [nay, yay] uint256[2] votes; // [nay, yay] @@ -200,16 +228,18 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { "voting-rep-competing-poll-exists" ); - uint256 amount = min(_amount, sub(getRequiredStake(_pollId), poll.stakes[_vote])); - uint256 stakerRep = checkReputation(_pollId, msg.sender, _key, _value, _branchMask, _siblings); - - require(add(stakes[_pollId][msg.sender][_vote], amount) <= stakerRep, "voting-rep-insufficient-rep"); + uint256 requiredStake = getRequiredStake(_pollId); + uint256 amount = min(_amount, sub(requiredStake, poll.stakes[_vote])); + uint256 stakerTotalAmount = add(stakes[_pollId][msg.sender][_vote], amount); require( - crowdFunding || add(poll.stakes[_vote], amount) == getRequiredStake(_pollId), - "voting-rep-stake-must-be-exact" + stakerTotalAmount <= checkReputation(_pollId, msg.sender, _key, _value, _branchMask, _siblings), + "voting-rep-insufficient-rep" + ); + require( + stakerTotalAmount >= wmul(requiredStake, minStakeFraction), + "voting-rep-insufficient-stake" ); - colony.obligateStake(msg.sender, poll.domainId, amount); colony.transferStake(_permissionDomainId, _childSkillIndex, address(this), msg.sender, poll.domainId, amount, address(this)); @@ -217,7 +247,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { // Update the stake poll.unpaidRewards = add(poll.unpaidRewards, amount); poll.stakes[_vote] = add(poll.stakes[_vote], amount); - stakes[_pollId][msg.sender][_vote] = add(stakes[_pollId][msg.sender][_vote], amount); + stakes[_pollId][msg.sender][_vote] = stakerTotalAmount; // Update the lead if the stake is larger if (stakes[_pollId][poll.leads[_vote]][_vote] < amount) { @@ -225,7 +255,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { } // Activate poll to prevent competing polls from being activated - if (poll.stakes[YAY] == getRequiredStake(_pollId) && _vote == YAY) { + if (poll.stakes[YAY] == requiredStake && _vote == YAY) { activePolls[hashAction(poll.action)] = _pollId; // Increment counter & extend claim delay if staking for an expenditure state change @@ -239,7 +269,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { } // Claim tokens if fully staked - if (poll.stakes[YAY] == getRequiredStake(_pollId) && poll.stakes[NAY] == getRequiredStake(_pollId)) { + if (poll.stakes[YAY] == requiredStake && poll.stakes[NAY] == requiredStake) { tokenLocking.claim(token, true); } @@ -258,13 +288,31 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { poll.responses[_vote] = _response; if (poll.responses[YAY] != Response.None && poll.responses[NAY] != Response.None) { - poll.lastEvent = now; + poll.events[STAKE_END] = now; } } - function submitVote(uint256 _pollId, bytes32 _voteSecret) public { - require(getPollState(_pollId) == PollState.Voting, "voting-rep-poll-not-open"); + function submitVote( + uint256 _pollId, + bytes32 _voteSecret, + bytes memory _key, + bytes memory _value, + uint256 _branchMask, + bytes32[] memory _siblings + ) + public + { + Poll storage poll = polls[_pollId]; + require(getPollState(_pollId) == PollState.Submit, "voting-rep-poll-not-open"); + + uint256 userRep = checkReputation(_pollId, msg.sender, _key, _value, _branchMask, _siblings); + voteSecrets[msg.sender][_pollId] = _voteSecret; + poll.repSubmitted = add(poll.repSubmitted, userRep); + + if (poll.repSubmitted >= wmul(poll.skillRep, maxVoteFraction)) { + poll.events[SUBMIT_END] = now; + } emit PollVoteSubmitted(_pollId, msg.sender); } @@ -281,21 +329,19 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { public { Poll storage poll = polls[_pollId]; - require(getPollState(_pollId) != PollState.Voting, "voting-rep-poll-still-open"); - - bytes32 voteSecret = voteSecrets[msg.sender][_pollId]; - require(voteSecret == getVoteSecret(_salt, _vote), "voting-rep-secret-no-match"); + require(getPollState(_pollId) == PollState.Reveal, "voting-rep-poll-not-reveal"); - // Validate proof and get reputation value uint256 userRep = checkReputation(_pollId, msg.sender, _key, _value, _branchMask, _siblings); - // Remove the secret + bytes32 voteSecret = voteSecrets[msg.sender][_pollId]; + require(voteSecret == getVoteSecret(_salt, _vote), "voting-rep-secret-no-match"); delete voteSecrets[msg.sender][_pollId]; - // Increment the vote if poll in reveal, otherwise skip - // NOTE: since there's no locking, we could just `require` PollState.Reveal - if (getPollState(_pollId) == PollState.Reveal) { - poll.votes[_vote] = add(poll.votes[_vote], userRep); + poll.votes[_vote] = add(poll.votes[_vote], userRep); + poll.repRevealed = add(poll.repRevealed, userRep); + + if (poll.repRevealed >= wmul(poll.skillRep, maxVoteFraction)) { + poll.events[REVEAL_END] = now; } uint256 pctReputation = wdiv(userRep, poll.skillRep); @@ -326,12 +372,13 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { uint256 childSkillId = colonyNetwork.getChildSkillId(newDomainSkillId, _childSkillIndex); require(childSkillId == poll.skillId, "voting-rep-invalid-domain-proof"); - poll.lastEvent = now; + delete poll.events; + delete poll.responses; + + poll.events[CREATE] = now; poll.domainId = _newDomainId; poll.skillId = newDomainSkillId; poll.skillRep = checkReputation(_pollId, address(0x0), _key, _value, _branchMask, _siblings); - - delete poll.responses; } function executePoll(uint256 _pollId) public { @@ -411,6 +458,8 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { uint256 rewardFraction = wdiv(poll.unpaidRewards, totalStake); uint256 rewardStake = wmul(stake, rewardFraction); + delete stakes[_pollId][_user][_vote]; + uint256 stakerReward; uint256 repPenalty; @@ -442,7 +491,6 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { stakerReward = rewardStake; } - delete stakes[_pollId][_user][_vote]; tokenLocking.transfer(token, stakerReward, _user, true); if (repPenalty > 0) { @@ -498,7 +546,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { poll.stakes[NAY] < requiredStake ) { // Are we still staking? - if (now < poll.lastEvent + STAKE_PERIOD) { + if (now < poll.events[CREATE] + stakePeriod && poll.events[STAKE_END] == 0) { return PollState.Staking; // If not, did the YAY side stake? } else if (poll.stakes[YAY] == requiredStake) { @@ -518,31 +566,46 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { return PollState.Executable; // Do we need to keep waiting? - } else if ( - now < poll.lastEvent + STAKE_PERIOD && - (poll.responses[YAY] == Response.None || - poll.responses[NAY] == Response.None) - ) { + } else if (now < poll.events[CREATE] + stakePeriod && poll.events[STAKE_END] == 0) { return PollState.Staking; // Fully staked, no folds, go to a vote } else { - // Infer the right timestamp (since only updated if both parties respond) - uint256 lastEvent = poll.lastEvent + (( - poll.responses[YAY] == Response.None || - poll.responses[NAY] == Response.None - ) ? STAKE_PERIOD : 0); - - if (now < lastEvent + VOTE_PERIOD) { - return PollState.Voting; - } else if (now < lastEvent + (VOTE_PERIOD + REVEAL_PERIOD)) { + if (now < min( + poll.events[CREATE] + stakePeriod + submitPeriod, + min( + selfOrMax(poll.events[STAKE_END]) + submitPeriod, + selfOrMax(poll.events[SUBMIT_END]) + ) + )) { + return PollState.Submit; + } else if (now < min( + poll.events[CREATE] + stakePeriod + submitPeriod + revealPeriod, + min( + selfOrMax(poll.events[STAKE_END]) + submitPeriod + revealPeriod, + min( + selfOrMax(poll.events[SUBMIT_END]) + revealPeriod, + selfOrMax(poll.events[REVEAL_END]) + ) + ) + )) { return PollState.Reveal; - } else if (now < lastEvent + (VOTE_PERIOD + REVEAL_PERIOD + STAKE_PERIOD)) { + } else if (now < min( + poll.events[CREATE] + stakePeriod + submitPeriod + revealPeriod + stakePeriod, + min( + selfOrMax(poll.events[STAKE_END]) + submitPeriod + revealPeriod + stakePeriod, + min( + selfOrMax(poll.events[SUBMIT_END]) + revealPeriod + stakePeriod, + selfOrMax(poll.events[REVEAL_END]) + stakePeriod + ) + ) + )) { return PollState.Closed; } else { return PollState.Executable; } + } } @@ -563,7 +626,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { require(state == ExtensionState.Active, "voting-rep-not-active"); pollCount += 1; - polls[pollCount].lastEvent = now; + polls[pollCount].events[CREATE] = now; polls[pollCount].rootHash = colonyNetwork.getReputationRootHash(); polls[pollCount].domainId = _domainId; polls[pollCount].skillId = _skillId; @@ -586,6 +649,10 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { return 1 - _vote; } + function selfOrMax(uint256 _timestamp) internal pure returns (uint256) { + return (_timestamp == 0) ? UINT128_MAX : _timestamp; + } + function checkReputation( uint256 _pollId, address _who, @@ -700,6 +767,9 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { public returns (bytes memory) { + // See https://solidity.readthedocs.io/en/develop/abi-spec.html#use-of-dynamic-types + // for documentation on how the action `bytes` is encoded + bytes32 functionSignature; uint256 permissionDomainId; uint256 childSkillIndex; diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index 452c450c25..6248f7ce17 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -65,9 +65,16 @@ contract("Voting Reputation", (accounts) => { let user1Mask; let user1Siblings; - const STAKE_WINDOW = SECONDS_PER_DAY * 3; - const VOTE_WINDOW = SECONDS_PER_DAY * 2; - const REVEAL_WINDOW = SECONDS_PER_DAY * 2; + const STAKE_FRACTION = WAD.divn(1000); // 0.1 % + const MIN_STAKE_FRACTION = WAD.divn(10); // 10 % + + const MAX_VOTE_FRACTION = WAD.divn(10).muln(8); // 80 % + const VOTER_REWARD_FRACTION = WAD.divn(10); // 10 % + const VOTE_POWER_FRACTION = WAD.muln(2).divn(3); // 66 % + + const STAKE_PERIOD = SECONDS_PER_DAY * 3; + const VOTE_PERIOD = SECONDS_PER_DAY * 2; + const REVEAL_PERIOD = SECONDS_PER_DAY * 2; const USER0 = accounts[0]; const USER1 = accounts[1]; @@ -81,7 +88,7 @@ contract("Voting Reputation", (accounts) => { const YAY = 1; const STAKING = 0; - const VOTING = 1; + const SUBMIT = 1; const REVEAL = 2; // const CLOSED = 3; const EXECUTABLE = 4; @@ -122,7 +129,18 @@ contract("Voting Reputation", (accounts) => { await votingFactory.deployExtension(colony.address); const votingAddress = await votingFactory.deployedExtensions(colony.address); voting = await VotingReputation.at(votingAddress); - await voting.initialise(WAD.divn(1000), WAD.divn(10), WAD.muln(2).divn(3), true); + + await voting.initialise( + STAKE_FRACTION, + MIN_STAKE_FRACTION, + MAX_VOTE_FRACTION, + VOTER_REWARD_FRACTION, + VOTE_POWER_FRACTION, + STAKE_PERIOD, + VOTE_PERIOD, + REVEAL_PERIOD + ); + await colony.setArbitrationRole(1, UINT256_MAX, voting.address, 1, true); await colony.setAdministrationRole(1, UINT256_MAX, voting.address, 1, true); @@ -226,8 +244,12 @@ contract("Voting Reputation", (accounts) => { }); it("cannot initialise twice or if not root", async () => { - await checkErrorRevert(voting.initialise(WAD, WAD, WAD, true), "voting-rep-already-initialised"); - await checkErrorRevert(voting.initialise(WAD, WAD, WAD, true, { from: USER2 }), "voting-rep-user-not-root"); + await checkErrorRevert(voting.initialise(WAD, WAD, WAD, WAD, WAD, STAKE_PERIOD, VOTE_PERIOD, REVEAL_PERIOD), "voting-rep-already-initialised"); + + await checkErrorRevert( + voting.initialise(WAD, WAD, WAD, WAD, WAD, STAKE_PERIOD, VOTE_PERIOD, REVEAL_PERIOD, { from: USER2 }), + "voting-rep-user-not-root" + ); }); it("cannot initialise with invalid values", async () => { @@ -236,11 +258,26 @@ contract("Voting Reputation", (accounts) => { const votingAddress = await votingFactory.deployedExtensions(colony.address); voting = await VotingReputation.at(votingAddress); - await checkErrorRevert(voting.initialise(WAD.addn(1), WAD, WAD, true), "voting-rep-must-be-wad"); - await checkErrorRevert(voting.initialise(WAD, WAD.addn(1), WAD, true), "voting-rep-must-be-wad"); - await checkErrorRevert(voting.initialise(WAD, WAD, WAD.addn(1), true), "voting-rep-must-be-wad"); + await checkErrorRevert(voting.initialise(WAD.addn(1), WAD, WAD, WAD, WAD, STAKE_PERIOD, VOTE_PERIOD, REVEAL_PERIOD), "voting-rep-must-be-wad"); + await checkErrorRevert(voting.initialise(WAD, WAD.addn(1), WAD, WAD, WAD, STAKE_PERIOD, VOTE_PERIOD, REVEAL_PERIOD), "voting-rep-must-be-wad"); + await checkErrorRevert(voting.initialise(WAD, WAD, WAD.addn(1), WAD, WAD, STAKE_PERIOD, VOTE_PERIOD, REVEAL_PERIOD), "voting-rep-must-be-wad"); + await checkErrorRevert(voting.initialise(WAD, WAD, WAD, WAD.addn(1), WAD, STAKE_PERIOD, VOTE_PERIOD, REVEAL_PERIOD), "voting-rep-must-be-wad"); + await checkErrorRevert(voting.initialise(WAD, WAD, WAD, WAD, WAD.addn(1), STAKE_PERIOD, VOTE_PERIOD, REVEAL_PERIOD), "voting-rep-must-be-wad"); - await voting.initialise(WAD, WAD, WAD, true); + await checkErrorRevert( + voting.initialise(WAD, WAD, WAD, WAD, WAD, SECONDS_PER_DAY * 366, VOTE_PERIOD, REVEAL_PERIOD), + "voting-rep-period-too-long" + ); + await checkErrorRevert( + voting.initialise(WAD, WAD, WAD, WAD, WAD, STAKE_PERIOD, SECONDS_PER_DAY * 366, REVEAL_PERIOD), + "voting-rep-period-too-long" + ); + await checkErrorRevert( + voting.initialise(WAD, WAD, WAD, WAD, WAD, STAKE_PERIOD, VOTE_PERIOD, SECONDS_PER_DAY * 366), + "voting-rep-period-too-long" + ); + + await voting.initialise(WAD, WAD, WAD, WAD, WAD, STAKE_PERIOD, VOTE_PERIOD, REVEAL_PERIOD); }); }); @@ -309,17 +346,28 @@ contract("Voting Reputation", (accounts) => { }); it("can stake on a poll", async () => { - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, 100, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, 100, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE.divn(2), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE.divn(2), user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); const poll = await voting.getPoll(pollId); expect(poll.stakes[0]).to.be.zero; - expect(poll.stakes[1]).to.eq.BN(200); + expect(poll.stakes[1]).to.eq.BN(REQUIRED_STAKE); const stake0 = await voting.getStake(pollId, USER0, YAY); const stake1 = await voting.getStake(pollId, USER1, YAY); - expect(stake0).to.eq.BN(100); - expect(stake1).to.eq.BN(100); + expect(stake0).to.eq.BN(REQUIRED_STAKE.divn(2)); + expect(stake1).to.eq.BN(REQUIRED_STAKE.divn(2)); + }); + + it("cannot stake less than the minStake", async () => { + const minStake = REQUIRED_STAKE.divn(10); + + await checkErrorRevert( + voting.stakePoll(pollId, 1, UINT256_MAX, YAY, minStake.subn(1), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), + "voting-rep-insufficient-stake" + ); + + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, minStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); }); it("can update the poll states correctly", async () => { @@ -340,17 +388,17 @@ contract("Voting Reputation", (accounts) => { await voting.respondToStake(pollId, NAY, CALL, { from: USER1 }); pollState = await voting.getPollState(pollId); - expect(pollState).to.eq.BN(VOTING); + expect(pollState).to.eq.BN(SUBMIT); }); it("can go to a vote even if both sides do not call", async () => { await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await forwardTime(STAKE_WINDOW, this); + await forwardTime(STAKE_PERIOD, this); const pollState = await voting.getPollState(pollId); - expect(pollState).to.eq.BN(VOTING); + expect(pollState).to.eq.BN(SUBMIT); }); it("cannot execute if the YAY side stakes and folds", async () => { @@ -388,7 +436,7 @@ contract("Voting Reputation", (accounts) => { await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await forwardTime(STAKE_WINDOW, this); + await forwardTime(STAKE_PERIOD, this); await checkErrorRevert(voting.respondToStake(pollId, YAY, CALL, { from: USER0 }), "voting-rep-not-staking"); }); @@ -416,7 +464,7 @@ contract("Voting Reputation", (accounts) => { activePoll = await voting.getActivePoll(soliditySha3(action)); expect(activePoll).to.be.zero; - await forwardTime(STAKE_WINDOW / 2, this); + await forwardTime(STAKE_PERIOD / 2, this); await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); activePoll = await voting.getActivePoll(soliditySha3(action)); @@ -431,7 +479,7 @@ contract("Voting Reputation", (accounts) => { ); // But, can stake once the first poll is executed. - await forwardTime(STAKE_WINDOW / 2, this); + await forwardTime(STAKE_PERIOD / 2, this); await voting.executePoll(pollId); activePoll = await voting.getActivePoll(soliditySha3(action)); @@ -590,7 +638,7 @@ contract("Voting Reputation", (accounts) => { expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); expect(expenditureSlot.claimDelay).to.eq.BN(UINT256_MAX); - await forwardTime(STAKE_WINDOW, this); + await forwardTime(STAKE_PERIOD, this); await voting.executePoll(pollId1); expenditurePollCount = await voting.getExpenditurePollCount(expenditureHash); @@ -627,7 +675,7 @@ contract("Voting Reputation", (accounts) => { }); it("cannot stake once time runs out", async () => { - await forwardTime(STAKE_WINDOW, this); + await forwardTime(STAKE_PERIOD, this); await checkErrorRevert( voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), @@ -640,23 +688,6 @@ contract("Voting Reputation", (accounts) => { ); }); - it("cannot crowdfund if prohibited in initialisation", async () => { - await votingFactory.removeExtension(colony.address); - await votingFactory.deployExtension(colony.address); - const votingAddress = await votingFactory.deployedExtensions(colony.address); - voting = await VotingReputation.at(votingAddress); - await voting.initialise(WAD.divn(1000), WAD.divn(10), WAD.muln(2).divn(3), false); - - const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); - await voting.createRootPoll(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); - pollId = await voting.getPollCount(); - - await checkErrorRevert( - voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE.subn(1), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), - "voting-rep-stake-must-be-exact" - ); - }); - it("can go to a vote even if the nay side fails to respond", async () => { await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); @@ -668,12 +699,12 @@ contract("Voting Reputation", (accounts) => { pollState = await voting.getPollState(pollId); expect(pollState).to.eq.BN(STAKING); - await forwardTime(STAKE_WINDOW, this); + await forwardTime(STAKE_PERIOD, this); pollState = await voting.getPollState(pollId); - expect(pollState).to.eq.BN(VOTING); + expect(pollState).to.eq.BN(SUBMIT); - await forwardTime(VOTE_WINDOW, this); + await forwardTime(VOTE_PERIOD, this); pollState = await voting.getPollState(pollId); expect(pollState).to.eq.BN(REVEAL); @@ -690,12 +721,12 @@ contract("Voting Reputation", (accounts) => { pollState = await voting.getPollState(pollId); expect(pollState).to.eq.BN(STAKING); - await forwardTime(STAKE_WINDOW, this); + await forwardTime(STAKE_PERIOD, this); pollState = await voting.getPollState(pollId); - expect(pollState).to.eq.BN(VOTING); + expect(pollState).to.eq.BN(SUBMIT); - await forwardTime(VOTE_WINDOW, this); + await forwardTime(VOTE_PERIOD, this); pollState = await voting.getPollState(pollId); expect(pollState).to.eq.BN(REVEAL); @@ -718,33 +749,31 @@ contract("Voting Reputation", (accounts) => { }); it("can rate and reveal for a poll", async () => { - await voting.submitVote(pollId, soliditySha3(SALT, NAY), { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await forwardTime(VOTE_WINDOW, this); + await forwardTime(VOTE_PERIOD, this); await voting.revealVote(pollId, SALT, NAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); }); it("can tally votes from two users", async () => { - await voting.submitVote(pollId, soliditySha3(SALT, NAY), { from: USER0 }); - await voting.submitVote(pollId, soliditySha3(SALT, YAY), { from: USER1 }); + await voting.submitVote(pollId, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, YAY), user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await forwardTime(VOTE_WINDOW, this); - - await voting.revealVote(pollId, SALT, NAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.revealVote(pollId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await voting.revealVote(pollId, SALT, YAY, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); // See final counts const { votes } = await voting.getPoll(pollId); - expect(votes[0]).to.eq.BN(WAD); - expect(votes[1]).to.eq.BN(WAD.muln(2)); + expect(votes[0]).to.be.zero; + expect(votes[1]).to.eq.BN(WAD.muln(3)); }); it("can update votes, but just the last one counts", async () => { - await voting.submitVote(pollId, soliditySha3(SALT, NAY), { from: USER0 }); - await voting.submitVote(pollId, soliditySha3(SALT, YAY), { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await forwardTime(VOTE_WINDOW, this); + await forwardTime(VOTE_PERIOD, this); // Revealing first vote fails await checkErrorRevert( @@ -756,24 +785,10 @@ contract("Voting Reputation", (accounts) => { await voting.revealVote(pollId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); }); - it("can reveal votes after poll closes, but doesn't count", async () => { - await voting.submitVote(pollId, soliditySha3(SALT, NAY), { from: USER0 }); - - await forwardTime(VOTE_WINDOW, this); - await forwardTime(REVEAL_WINDOW, this); - - await voting.revealVote(pollId, SALT, NAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - - // Vote didn't count - const { votes } = await voting.getPoll(pollId); - expect(votes[0]).to.be.zero; - expect(votes[1]).to.be.zero; - }); - it("cannot reveal a vote twice, and so cannot vote twice", async () => { - await voting.submitVote(pollId, soliditySha3(SALT, NAY), { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await forwardTime(VOTE_WINDOW, this); + await forwardTime(VOTE_PERIOD, this); await voting.revealVote(pollId, SALT, NAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); @@ -784,10 +799,12 @@ contract("Voting Reputation", (accounts) => { }); it("can vote in two polls with two reputation states, with different proofs", async () => { - await voting.submitVote(pollId, soliditySha3(SALT, NAY), { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + + const oldRootHash = await reputationTree.getRootHash(); // Update reputation state - const user0Value2 = makeReputationValue(WAD.muln(3), 2); + const user0Value2 = makeReputationValue(WAD.muln(2), 2); await reputationTree.insert(user0Key, user0Value2); const [domain1Mask2, domain1Siblings2] = await reputationTree.getProof(domain1Key); @@ -796,10 +813,11 @@ contract("Voting Reputation", (accounts) => { // Set new rootHash const rootHash = await reputationTree.getRootHash(); - const repCycle = await getActiveRepCycle(colonyNetwork); + expect(oldRootHash).to.not.equal(rootHash); await forwardTime(MINING_CYCLE_DURATION, this); + const repCycle = await getActiveRepCycle(colonyNetwork); await repCycle.submitRootHash(rootHash, 0, "0x00", 10, { from: MINER }); await repCycle.confirmNewHash(0); @@ -811,37 +829,52 @@ contract("Voting Reputation", (accounts) => { await voting.respondToStake(pollId2, YAY, CALL, { from: USER0 }); await voting.respondToStake(pollId2, NAY, CALL, { from: USER1 }); - await voting.submitVote(pollId2, soliditySha3(SALT, NAY), { from: USER0 }); + await voting.submitVote(pollId2, soliditySha3(SALT, NAY), user0Key, user0Value2, user0Mask2, user0Siblings2, { from: USER0 }); - await forwardTime(VOTE_WINDOW, this); + await forwardTime(VOTE_PERIOD, this); await voting.revealVote(pollId, SALT, NAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await voting.revealVote(pollId2, SALT, NAY, user0Key, user0Value2, user0Mask2, user0Siblings2, { from: USER0 }); }); it("cannot submit a vote if voting is closed", async () => { - await forwardTime(VOTE_WINDOW, this); + await forwardTime(VOTE_PERIOD, this); - await checkErrorRevert(voting.submitVote(pollId, soliditySha3(SALT, NAY)), "voting-rep-poll-not-open"); + await checkErrorRevert( + voting.submitVote(pollId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), + "voting-rep-poll-not-open" + ); }); it("cannot reveal a vote if voting is open", async () => { - await voting.submitVote(pollId, soliditySha3(SALT, NAY)); - await checkErrorRevert(voting.revealVote(pollId, SALT, YAY, FAKE, FAKE, 0, []), "voting-rep-poll-still-open"); + await voting.submitVote(pollId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await checkErrorRevert(voting.revealVote(pollId, SALT, YAY, FAKE, FAKE, 0, [], { from: USER0 }), "voting-rep-poll-not-reveal"); + }); + + it("cannot reveal a vote after voting closes", async () => { + await voting.submitVote(pollId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + + await forwardTime(VOTE_PERIOD, this); + await forwardTime(REVEAL_PERIOD, this); + + await checkErrorRevert(voting.revealVote(pollId, SALT, NAY, FAKE, FAKE, 0, [], { from: USER0 }), "voting-rep-poll-not-reveal"); }); it("cannot reveal a vote with a bad secret", async () => { - await voting.submitVote(pollId, soliditySha3(SALT, NAY)); + await voting.submitVote(pollId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await forwardTime(VOTE_WINDOW, this); + await forwardTime(VOTE_PERIOD, this); - await checkErrorRevert(voting.revealVote(pollId, SALT, YAY, FAKE, FAKE, 0, []), "voting-rep-secret-no-match"); + await checkErrorRevert( + voting.revealVote(pollId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), + "voting-rep-secret-no-match" + ); }); it("cannot reveal a vote with a bad proof", async () => { - await voting.submitVote(pollId, soliditySha3(SALT, NAY), { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await forwardTime(VOTE_WINDOW, this); + await forwardTime(VOTE_PERIOD, this); // Invalid proof (wrong root hash) await checkErrorRevert(voting.revealVote(pollId, SALT, NAY, FAKE, FAKE, 0, [], { from: USER0 }), "voting-rep-invalid-root-hash"); @@ -881,7 +914,7 @@ contract("Voting Reputation", (accounts) => { from: USER0, }); - await forwardTime(STAKE_WINDOW, this); + await forwardTime(STAKE_PERIOD, this); await checkErrorRevert(voting.executePoll(pollId), "voting-rep-poll-failed"); }); @@ -892,7 +925,7 @@ contract("Voting Reputation", (accounts) => { from: USER1, }); - await forwardTime(STAKE_WINDOW, this); + await forwardTime(STAKE_PERIOD, this); const { logs } = await voting.executePoll(pollId); expect(logs[0].args.success).to.be.true; @@ -906,7 +939,7 @@ contract("Voting Reputation", (accounts) => { await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await forwardTime(STAKE_WINDOW, this); + await forwardTime(STAKE_PERIOD, this); const { logs } = await voting.executePoll(pollId); expect(logs[0].args.success).to.be.true; @@ -922,7 +955,7 @@ contract("Voting Reputation", (accounts) => { await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await forwardTime(STAKE_WINDOW, this); + await forwardTime(STAKE_PERIOD, this); const balanceBefore = await otherColony.getFundingPotBalance(1, token.address); expect(balanceBefore).to.be.zero; @@ -940,7 +973,7 @@ contract("Voting Reputation", (accounts) => { await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await forwardTime(STAKE_WINDOW, this); + await forwardTime(STAKE_PERIOD, this); const { logs } = await voting.executePoll(pollId); expect(logs[0].args.success).to.be.false; @@ -959,14 +992,14 @@ contract("Voting Reputation", (accounts) => { await voting.respondToStake(pollId, NAY, CALL, { from: USER1 }); pollState = await voting.getPollState(pollId); - expect(pollState).to.eq.BN(VOTING); + expect(pollState).to.eq.BN(SUBMIT); await checkErrorRevert(voting.executePoll(pollId), "voting-rep-poll-not-executable"); }); it("cannot take an action twice", async () => { await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await forwardTime(STAKE_WINDOW, this); + await forwardTime(STAKE_PERIOD, this); const { logs } = await voting.executePoll(pollId); expect(logs[0].args.success).to.be.true; @@ -980,17 +1013,17 @@ contract("Voting Reputation", (accounts) => { await voting.respondToStake(pollId, YAY, CALL, { from: USER0 }); await voting.respondToStake(pollId, NAY, CALL, { from: USER1 }); - await voting.submitVote(pollId, soliditySha3(SALT, YAY), { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await forwardTime(VOTE_WINDOW, this); + await forwardTime(VOTE_PERIOD, this); await voting.revealVote(pollId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await forwardTime(REVEAL_WINDOW, this); + await forwardTime(REVEAL_PERIOD, this); await checkErrorRevert(voting.executePoll(pollId), "voting-rep-poll-escalation-window-open"); - await forwardTime(STAKE_WINDOW, this); + await forwardTime(STAKE_PERIOD, this); const { logs } = await voting.executePoll(pollId); expect(logs[0].args.success).to.be.true; @@ -1002,14 +1035,14 @@ contract("Voting Reputation", (accounts) => { await voting.respondToStake(pollId, YAY, CALL, { from: USER0 }); await voting.respondToStake(pollId, NAY, CALL, { from: USER1 }); - await voting.submitVote(pollId, soliditySha3(SALT, NAY), { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await forwardTime(VOTE_WINDOW, this); + await forwardTime(VOTE_PERIOD, this); await voting.revealVote(pollId, SALT, NAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await forwardTime(REVEAL_WINDOW, this); - await forwardTime(STAKE_WINDOW, this); + await forwardTime(REVEAL_PERIOD, this); + await forwardTime(STAKE_PERIOD, this); const { logs } = await voting.executePoll(pollId); expect(logs[0].args.success).to.be.false; @@ -1029,14 +1062,14 @@ contract("Voting Reputation", (accounts) => { await voting.respondToStake(pollId1, YAY, CALL, { from: USER0 }); await voting.respondToStake(pollId1, NAY, CALL, { from: USER1 }); - await voting.submitVote(pollId1, soliditySha3(SALT, YAY), { from: USER0 }); + await voting.submitVote(pollId1, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await forwardTime(VOTE_WINDOW, this); + await forwardTime(VOTE_PERIOD, this); await voting.revealVote(pollId1, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await forwardTime(REVEAL_WINDOW, this); - await forwardTime(STAKE_WINDOW, this); + await forwardTime(REVEAL_PERIOD, this); + await forwardTime(STAKE_PERIOD, this); let logs; ({ logs } = await voting.executePoll(pollId1)); @@ -1051,14 +1084,14 @@ contract("Voting Reputation", (accounts) => { await voting.respondToStake(pollId2, YAY, CALL, { from: USER0 }); await voting.respondToStake(pollId2, NAY, CALL, { from: USER1 }); - await voting.submitVote(pollId2, soliditySha3(SALT, YAY), { from: USER0 }); + await voting.submitVote(pollId2, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await forwardTime(VOTE_WINDOW, this); + await forwardTime(VOTE_PERIOD, this); await voting.revealVote(pollId2, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await forwardTime(REVEAL_WINDOW, this); - await forwardTime(STAKE_WINDOW, this); + await forwardTime(REVEAL_PERIOD, this); + await forwardTime(STAKE_PERIOD, this); ({ logs } = await voting.executePoll(pollId2)); expect(logs[0].args.success).to.be.false; @@ -1072,7 +1105,7 @@ contract("Voting Reputation", (accounts) => { await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await forwardTime(STAKE_WINDOW, this); + await forwardTime(STAKE_PERIOD, this); await voting.executePoll(pollId); const slot = soliditySha3(action.slice(0, action.length - 64)); @@ -1091,14 +1124,14 @@ contract("Voting Reputation", (accounts) => { await voting.respondToStake(pollId, YAY, CALL, { from: USER0 }); await voting.respondToStake(pollId, NAY, CALL, { from: USER1 }); - await voting.submitVote(pollId, soliditySha3(SALT, YAY), { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await forwardTime(VOTE_WINDOW, this); + await forwardTime(VOTE_PERIOD, this); await voting.revealVote(pollId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await forwardTime(REVEAL_WINDOW, this); - await forwardTime(STAKE_WINDOW, this); + await forwardTime(REVEAL_PERIOD, this); + await forwardTime(STAKE_PERIOD, this); await voting.executePoll(pollId); const slot = soliditySha3(action.slice(0, action.length - 64)); @@ -1126,7 +1159,7 @@ contract("Voting Reputation", (accounts) => { from: USER1, }); - await forwardTime(STAKE_WINDOW, this); + await forwardTime(STAKE_PERIOD, this); await voting.executePoll(pollId); @@ -1140,8 +1173,8 @@ contract("Voting Reputation", (accounts) => { const user1LockPost = await tokenLocking.getUserLock(token.address, USER1); // Note that no voter rewards were paid out - const expectedReward0 = REQUIRED_STAKE.add(REQUIRED_STAKE.divn(20)); // stake + (stake / 20) - const expectedReward1 = REQUIRED_STAKE.divn(20).muln(9); // (stake * 9 / 20) + const expectedReward0 = REQUIRED_STAKE.add(REQUIRED_STAKE.divn(20)); // 110% of stake + const expectedReward1 = REQUIRED_STAKE.divn(20).muln(9); // 90% of stake expect(new BN(user0LockPost.balance).sub(new BN(user0LockPre.balance))).to.eq.BN(expectedReward0); expect(new BN(user1LockPost.balance).sub(new BN(user1LockPre.balance))).to.eq.BN(expectedReward1); @@ -1166,16 +1199,13 @@ contract("Voting Reputation", (accounts) => { await voting.respondToStake(pollId, YAY, CALL, { from: USER0 }); await voting.respondToStake(pollId, NAY, CALL, { from: USER1 }); - await voting.submitVote(pollId, soliditySha3(SALT, YAY), { from: USER0 }); - await voting.submitVote(pollId, soliditySha3(SALT, NAY), { from: USER1 }); - - await forwardTime(VOTE_WINDOW, this); + await voting.submitVote(pollId, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, NAY), user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await voting.revealVote(pollId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await voting.revealVote(pollId, SALT, NAY, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await forwardTime(REVEAL_WINDOW, this); - await forwardTime(STAKE_WINDOW, this); + await forwardTime(STAKE_PERIOD, this); await voting.executePoll(pollId); @@ -1209,10 +1239,10 @@ contract("Voting Reputation", (accounts) => { const repCycle = await IReputationMiningCycle.at(addr); const numEntriesPrev = await repCycle.getReputationUpdateLogLength(); - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, 100, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, 100, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE.divn(2), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE.divn(2), user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await forwardTime(STAKE_WINDOW, this); + await forwardTime(STAKE_PERIOD, this); const user0LockPre = await tokenLocking.getUserLock(token.address, USER0); const user1LockPre = await tokenLocking.getUserLock(token.address, USER1); @@ -1226,8 +1256,8 @@ contract("Voting Reputation", (accounts) => { const user1LockPost = await tokenLocking.getUserLock(token.address, USER1); expect(numEntriesPrev).to.eq.BN(numEntriesPost); - expect(new BN(user0LockPost.balance).sub(new BN(user0LockPre.balance))).to.eq.BN(100); - expect(new BN(user1LockPost.balance).sub(new BN(user1LockPre.balance))).to.eq.BN(100); + expect(new BN(user0LockPost.balance).sub(new BN(user0LockPre.balance))).to.eq.BN(REQUIRED_STAKE.divn(2)); + expect(new BN(user1LockPost.balance).sub(new BN(user1LockPre.balance))).to.eq.BN(REQUIRED_STAKE.divn(2)); }); it("cannot claim rewards twice", async () => { @@ -1237,14 +1267,14 @@ contract("Voting Reputation", (accounts) => { await voting.respondToStake(pollId, YAY, CALL, { from: USER0 }); await voting.respondToStake(pollId, NAY, CALL, { from: USER1 }); - await voting.submitVote(pollId, soliditySha3(SALT, YAY), { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await forwardTime(VOTE_WINDOW, this); + await forwardTime(VOTE_PERIOD, this); await voting.revealVote(pollId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await forwardTime(REVEAL_WINDOW, this); - await forwardTime(STAKE_WINDOW, this); + await forwardTime(REVEAL_PERIOD, this); + await forwardTime(STAKE_PERIOD, this); await voting.executePoll(pollId); @@ -1290,15 +1320,11 @@ contract("Voting Reputation", (accounts) => { await voting.respondToStake(pollId, NAY, CALL, { from: USER1 }); // Note that this is a passing vote - await voting.submitVote(pollId, soliditySha3(SALT, NAY), { from: USER0 }); - await voting.submitVote(pollId, soliditySha3(SALT, YAY), { from: USER1 }); - - await forwardTime(VOTE_WINDOW, this); + await voting.submitVote(pollId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, YAY), user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await voting.revealVote(pollId, SALT, NAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await voting.revealVote(pollId, SALT, YAY, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - - await forwardTime(REVEAL_WINDOW, this); }); it("can internally escalate a domain poll after a vote", async () => { @@ -1310,7 +1336,7 @@ contract("Voting Reputation", (accounts) => { }); it("cannot internally escalate a domain poll if not in a 'closed' state", async () => { - await forwardTime(STAKE_WINDOW, this); + await forwardTime(STAKE_PERIOD, this); await voting.executePoll(pollId); @@ -1357,7 +1383,7 @@ contract("Voting Reputation", (accounts) => { const remainingStake = REQUIRED_STAKE.sub(WAD.divn(1000)); await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, remainingStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await forwardTime(STAKE_WINDOW, this); + await forwardTime(STAKE_PERIOD, this); const { logs } = await voting.executePoll(pollId); expect(logs[0].args.success).to.be.true; @@ -1373,7 +1399,7 @@ contract("Voting Reputation", (accounts) => { const remainingStake = REQUIRED_STAKE.sub(WAD.divn(1000)); await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, remainingStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await forwardTime(STAKE_WINDOW, this); + await forwardTime(STAKE_PERIOD, this); const { logs } = await voting.executePoll(pollId); expect(logs[0].args.success).to.be.false; @@ -1382,7 +1408,7 @@ contract("Voting Reputation", (accounts) => { it("can fall back on the previous vote if both sides fail to stake", async () => { await voting.escalatePoll(pollId, 1, 0, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); - await forwardTime(STAKE_WINDOW, this); + await forwardTime(STAKE_PERIOD, this); // Note that the previous vote succeeded const { logs } = await voting.executePoll(pollId); @@ -1408,16 +1434,13 @@ contract("Voting Reputation", (accounts) => { await voting.respondToStake(pollId, NAY, CALL, { from: USER1 }); // Make the vote fail this time (everyone votes against) - await voting.submitVote(pollId, soliditySha3(SALT, NAY), { from: USER0 }); - await voting.submitVote(pollId, soliditySha3(SALT, NAY), { from: USER1 }); - - await forwardTime(VOTE_WINDOW, this); + await voting.submitVote(pollId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, NAY), user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await voting.revealVote(pollId, SALT, NAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await voting.revealVote(pollId, SALT, NAY, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await forwardTime(REVEAL_WINDOW, this); - await forwardTime(STAKE_WINDOW, this); + await forwardTime(STAKE_PERIOD, this); const { logs } = await voting.executePoll(pollId); expect(logs[0].args.success).to.be.false; From 0631f902ad14dbb44e39e95d911f02b044f28fc7 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Fri, 17 Jul 2020 14:58:23 -0700 Subject: [PATCH 25/61] Remove stake responses --- contracts/extensions/VotingReputation.sol | 32 +---- test/extensions/voting-rep.js | 149 +--------------------- 2 files changed, 4 insertions(+), 177 deletions(-) diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index 6e94ae3f8a..529ac1a621 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -135,7 +135,6 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { // Data structures enum PollState { Staking, Submit, Reveal, Closed, Executable, Executed, Failed } - enum Response { None, Fold, Call } struct Poll { uint256[4] events; // Creation, Staking, Submission, Revelation @@ -148,8 +147,6 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { uint256 unpaidRewards; uint256[2] stakes; // [nay, yay] uint256[2] votes; // [nay, yay] - address[2] leads; // [nay, yay] - Response[2] responses; // [nay, yay] bool executed; address target; bytes action; @@ -249,11 +246,6 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { poll.stakes[_vote] = add(poll.stakes[_vote], amount); stakes[_pollId][msg.sender][_vote] = stakerTotalAmount; - // Update the lead if the stake is larger - if (stakes[_pollId][poll.leads[_vote]][_vote] < amount) { - poll.leads[_vote] = msg.sender; - } - // Activate poll to prevent competing polls from being activated if (poll.stakes[YAY] == requiredStake && _vote == YAY) { activePolls[hashAction(poll.action)] = _pollId; @@ -270,28 +262,13 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { // Claim tokens if fully staked if (poll.stakes[YAY] == requiredStake && poll.stakes[NAY] == requiredStake) { + poll.events[STAKE_END] = now; tokenLocking.claim(token, true); } emit PollStaked(_pollId, msg.sender, _vote, amount); } - function respondToStake(uint256 _pollId, uint256 _vote, Response _response) public { - Poll storage poll = polls[_pollId]; - uint256 requiredStake = getRequiredStake(_pollId); - - require(poll.stakes[YAY] == requiredStake && poll.stakes[NAY] == requiredStake, "voting-rep-not-fully-staked"); - require(getPollState(_pollId) == PollState.Staking, "voting-rep-not-staking"); - require(poll.leads[_vote] == msg.sender, "voting-rep-not-lead"); - require(poll.responses[_vote] == Response.None, "voting-rep-already-responded"); - - poll.responses[_vote] = _response; - - if (poll.responses[YAY] != Response.None && poll.responses[NAY] != Response.None) { - poll.events[STAKE_END] = now; - } - } - function submitVote( uint256 _pollId, bytes32 _voteSecret, @@ -373,7 +350,6 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { require(childSkillId == poll.skillId, "voting-rep-invalid-domain-proof"); delete poll.events; - delete poll.responses; poll.events[CREATE] = now; poll.domainId = _newDomainId; @@ -559,12 +535,6 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { return PollState.Failed; } - // Fully staked, check for any folds - } else if (poll.responses[YAY] == Response.Fold) { - return PollState.Failed; - } else if (poll.responses[NAY] == Response.Fold) { - return PollState.Executable; - // Do we need to keep waiting? } else if (now < poll.events[CREATE] + stakePeriod && poll.events[STAKE_END] == 0) { return PollState.Staking; diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index 6248f7ce17..3f4370f816 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -89,14 +89,11 @@ contract("Voting Reputation", (accounts) => { const STAKING = 0; const SUBMIT = 1; - const REVEAL = 2; + // const REVEAL = 2; // const CLOSED = 3; - const EXECUTABLE = 4; + // const EXECUTABLE = 4; // const EXECUTED = 5; - const FAILED = 6; - - const FOLD = 1; - const CALL = 2; + // const FAILED = 6; const ADDRESS_ZERO = ethers.constants.AddressZero; const REQUIRED_STAKE = WAD.muln(3).divn(1000); @@ -380,47 +377,9 @@ contract("Voting Reputation", (accounts) => { await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); pollState = await voting.getPollState(pollId); - expect(pollState).to.eq.BN(STAKING); - - await voting.respondToStake(pollId, YAY, CALL, { from: USER0 }); - pollState = await voting.getPollState(pollId); - expect(pollState).to.eq.BN(STAKING); - - await voting.respondToStake(pollId, NAY, CALL, { from: USER1 }); - pollState = await voting.getPollState(pollId); - expect(pollState).to.eq.BN(SUBMIT); - }); - - it("can go to a vote even if both sides do not call", async () => { - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - - await forwardTime(STAKE_PERIOD, this); - - const pollState = await voting.getPollState(pollId); expect(pollState).to.eq.BN(SUBMIT); }); - it("cannot execute if the YAY side stakes and folds", async () => { - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - - await voting.respondToStake(pollId, YAY, FOLD, { from: USER0 }); - - const pollState = await voting.getPollState(pollId); - expect(pollState).to.eq.BN(FAILED); - }); - - it("can execute if the NAY side stakes and folds", async () => { - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - - await voting.respondToStake(pollId, NAY, FOLD, { from: USER1 }); - - const pollState = await voting.getPollState(pollId); - expect(pollState).to.eq.BN(EXECUTABLE); - }); - it("cannot stake a nonexistent side", async () => { await checkErrorRevert( voting.stakePoll(pollId, 1, UINT256_MAX, 2, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), @@ -428,35 +387,6 @@ contract("Voting Reputation", (accounts) => { ); }); - it("cannot respond to a stake if not fully staked", async () => { - await checkErrorRevert(voting.respondToStake(pollId, YAY, CALL, { from: USER0 }), "voting-rep-not-fully-staked"); - }); - - it("cannot respond to a stake if not in the staking phase", async () => { - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - - await forwardTime(STAKE_PERIOD, this); - - await checkErrorRevert(voting.respondToStake(pollId, YAY, CALL, { from: USER0 }), "voting-rep-not-staking"); - }); - - it("cannot respond to a stake if not the lead", async () => { - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - - await checkErrorRevert(voting.respondToStake(pollId, YAY, CALL, { from: USER2 }), "voting-rep-not-lead"); - }); - - it("cannot respond to a stake twice", async () => { - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - - await voting.respondToStake(pollId, YAY, CALL, { from: USER0 }); - - await checkErrorRevert(voting.respondToStake(pollId, YAY, CALL, { from: USER0 }), "voting-rep-already-responded"); - }); - it("cannot stake for an action while there is an active poll for the same action", async () => { let activePoll; @@ -687,50 +617,6 @@ contract("Voting Reputation", (accounts) => { "voting-rep-staking-closed" ); }); - - it("can go to a vote even if the nay side fails to respond", async () => { - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - - // Yay side responds - await voting.respondToStake(pollId, YAY, CALL, { from: USER0 }); - - let pollState; - pollState = await voting.getPollState(pollId); - expect(pollState).to.eq.BN(STAKING); - - await forwardTime(STAKE_PERIOD, this); - - pollState = await voting.getPollState(pollId); - expect(pollState).to.eq.BN(SUBMIT); - - await forwardTime(VOTE_PERIOD, this); - - pollState = await voting.getPollState(pollId); - expect(pollState).to.eq.BN(REVEAL); - }); - - it("can go to a vote even if the yay side fails to respond", async () => { - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - - // Nay side responds - await voting.respondToStake(pollId, NAY, CALL, { from: USER1 }); - - let pollState; - pollState = await voting.getPollState(pollId); - expect(pollState).to.eq.BN(STAKING); - - await forwardTime(STAKE_PERIOD, this); - - pollState = await voting.getPollState(pollId); - expect(pollState).to.eq.BN(SUBMIT); - - await forwardTime(VOTE_PERIOD, this); - - pollState = await voting.getPollState(pollId); - expect(pollState).to.eq.BN(REVEAL); - }); }); describe("voting on polls", async () => { @@ -743,9 +629,6 @@ contract("Voting Reputation", (accounts) => { await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - - await voting.respondToStake(pollId, YAY, CALL, { from: USER0 }); - await voting.respondToStake(pollId, NAY, CALL, { from: USER1 }); }); it("can rate and reveal for a poll", async () => { @@ -826,8 +709,6 @@ contract("Voting Reputation", (accounts) => { const pollId2 = await voting.getPollCount(); await voting.stakePoll(pollId2, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value2, user0Mask2, user0Siblings2, { from: USER0 }); await voting.stakePoll(pollId2, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask2, user1Siblings2, { from: USER1 }); - await voting.respondToStake(pollId2, YAY, CALL, { from: USER0 }); - await voting.respondToStake(pollId2, NAY, CALL, { from: USER1 }); await voting.submitVote(pollId2, soliditySha3(SALT, NAY), user0Key, user0Value2, user0Mask2, user0Siblings2, { from: USER0 }); @@ -988,8 +869,6 @@ contract("Voting Reputation", (accounts) => { await checkErrorRevert(voting.executePoll(pollId), "voting-rep-poll-not-executable"); await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await voting.respondToStake(pollId, YAY, CALL, { from: USER0 }); - await voting.respondToStake(pollId, NAY, CALL, { from: USER1 }); pollState = await voting.getPollState(pollId); expect(pollState).to.eq.BN(SUBMIT); @@ -1010,8 +889,6 @@ contract("Voting Reputation", (accounts) => { it("can take an action if the poll passes", async () => { await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await voting.respondToStake(pollId, YAY, CALL, { from: USER0 }); - await voting.respondToStake(pollId, NAY, CALL, { from: USER1 }); await voting.submitVote(pollId, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); @@ -1032,8 +909,6 @@ contract("Voting Reputation", (accounts) => { it("cannot take an action if the poll fails", async () => { await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await voting.respondToStake(pollId, YAY, CALL, { from: USER0 }); - await voting.respondToStake(pollId, NAY, CALL, { from: USER1 }); await voting.submitVote(pollId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); @@ -1059,8 +934,6 @@ contract("Voting Reputation", (accounts) => { await voting.stakePoll(pollId1, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await voting.stakePoll(pollId1, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await voting.respondToStake(pollId1, YAY, CALL, { from: USER0 }); - await voting.respondToStake(pollId1, NAY, CALL, { from: USER1 }); await voting.submitVote(pollId1, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); @@ -1081,8 +954,6 @@ contract("Voting Reputation", (accounts) => { await voting.stakePoll(pollId2, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await voting.stakePoll(pollId2, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await voting.respondToStake(pollId2, YAY, CALL, { from: USER0 }); - await voting.respondToStake(pollId2, NAY, CALL, { from: USER1 }); await voting.submitVote(pollId2, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); @@ -1121,8 +992,6 @@ contract("Voting Reputation", (accounts) => { await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await voting.respondToStake(pollId, YAY, CALL, { from: USER0 }); - await voting.respondToStake(pollId, NAY, CALL, { from: USER1 }); await voting.submitVote(pollId, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); @@ -1196,9 +1065,6 @@ contract("Voting Reputation", (accounts) => { await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await voting.respondToStake(pollId, YAY, CALL, { from: USER0 }); - await voting.respondToStake(pollId, NAY, CALL, { from: USER1 }); - await voting.submitVote(pollId, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await voting.submitVote(pollId, soliditySha3(SALT, NAY), user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); @@ -1264,9 +1130,6 @@ contract("Voting Reputation", (accounts) => { await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await voting.respondToStake(pollId, YAY, CALL, { from: USER0 }); - await voting.respondToStake(pollId, NAY, CALL, { from: USER1 }); - await voting.submitVote(pollId, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await forwardTime(VOTE_PERIOD, this); @@ -1316,9 +1179,6 @@ contract("Voting Reputation", (accounts) => { await voting.stakePoll(pollId, 1, 0, YAY, WAD.divn(1000), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await voting.stakePoll(pollId, 1, 0, NAY, WAD.divn(1000), user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await voting.respondToStake(pollId, YAY, CALL, { from: USER0 }); - await voting.respondToStake(pollId, NAY, CALL, { from: USER1 }); - // Note that this is a passing vote await voting.submitVote(pollId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await voting.submitVote(pollId, soliditySha3(SALT, YAY), user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); @@ -1430,9 +1290,6 @@ contract("Voting Reputation", (accounts) => { await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, remainingStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, remainingStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await voting.respondToStake(pollId, YAY, CALL, { from: USER0 }); - await voting.respondToStake(pollId, NAY, CALL, { from: USER1 }); - // Make the vote fail this time (everyone votes against) await voting.submitVote(pollId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await voting.submitVote(pollId, soliditySha3(SALT, NAY), user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); From 2f2be739dd886e5bd5e058cf4b6f32ac80e82c69 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Fri, 17 Jul 2020 16:14:12 -0700 Subject: [PATCH 26/61] Remove activePoll, add unstakePoll logic --- contracts/extensions/VotingReputation.sol | 68 ++++++++------- test/extensions/voting-rep.js | 101 ++++++++++++---------- 2 files changed, 93 insertions(+), 76 deletions(-) diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index 529ac1a621..14faac2fca 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -34,6 +34,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { event ExtensionDeprecated(); event PollCreated(uint256 indexed pollId, uint256 indexed skillId); event PollStaked(uint256 indexed pollId, address indexed staker, uint256 indexed vote, uint256 amount); + event PollUnstaked(uint256 indexed pollId, address indexed staker, uint256 indexed vote, uint256 amount); event PollVoteSubmitted(uint256 indexed pollId, address indexed voter); event PollVoteRevealed(uint256 indexed pollId, address indexed voter, uint256 indexed vote); event PollExecuted(uint256 indexed pollId, bytes action, bool success); @@ -75,6 +76,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { uint256 stakePeriod; // Length of time for staking uint256 submitPeriod; // Length of time for submitting votes uint256 revealPeriod; // Length of time for revealing votes + uint256 escalationPeriod; // Length of time for escalating after a vote constructor(address _colony) public { colony = IColony(_colony); @@ -91,7 +93,8 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { uint256 _votePowerFraction, uint256 _stakePeriod, uint256 _submitPeriod, - uint256 _revealPeriod + uint256 _revealPeriod, + uint256 _escalationPeriod ) public { @@ -108,6 +111,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { require(_stakePeriod <= 365 days, "voting-rep-period-too-long"); require(_submitPeriod <= 365 days, "voting-rep-period-too-long"); require(_revealPeriod <= 365 days, "voting-rep-period-too-long"); + require(_escalationPeriod <= 365 days, "voting-rep-period-too-long"); state = ExtensionState.Active; @@ -121,6 +125,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { stakePeriod = _stakePeriod; submitPeriod = _submitPeriod; revealPeriod = _revealPeriod; + escalationPeriod = _escalationPeriod; emit ExtensionInitialised(); } @@ -160,7 +165,6 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { mapping (address => mapping (uint256 => bytes32)) voteSecrets; mapping (bytes32 => uint256) pastPolls; // action signature => voting power - mapping (bytes32 => uint256) activePolls; // action signature => pollId mapping (bytes32 => uint256) expenditurePollCounts; // expenditure signature => count // Public functions (interface) @@ -219,12 +223,6 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { require(_vote <= 1, "voting-rep-bad-vote"); require(getPollState(_pollId) == PollState.Staking, "voting-rep-staking-closed"); - require( - activePolls[hashAction(poll.action)] == 0 || - activePolls[hashAction(poll.action)] == _pollId, - "voting-rep-competing-poll-exists" - ); - uint256 requiredStake = getRequiredStake(_pollId); uint256 amount = min(_amount, sub(requiredStake, poll.stakes[_vote])); uint256 stakerTotalAmount = add(stakes[_pollId][msg.sender][_vote], amount); @@ -246,18 +244,12 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { poll.stakes[_vote] = add(poll.stakes[_vote], amount); stakes[_pollId][msg.sender][_vote] = stakerTotalAmount; - // Activate poll to prevent competing polls from being activated - if (poll.stakes[YAY] == requiredStake && _vote == YAY) { - activePolls[hashAction(poll.action)] = _pollId; - - // Increment counter & extend claim delay if staking for an expenditure state change - if (getSig(poll.action) == CHANGE_FUNCTION) { - bytes32 expenditureHash = hashExpenditureAction(poll.action); - expenditurePollCounts[expenditureHash] = add(expenditurePollCounts[expenditureHash], 1); - - bytes memory claimDelayAction = createClaimDelayAction(poll.action, UINT256_MAX); - executeCall(_pollId, claimDelayAction); - } + // Increment counter & extend claim delay if staking for an expenditure state change + if (poll.stakes[YAY] == requiredStake && _vote == YAY && getSig(poll.action) == CHANGE_FUNCTION) { + bytes32 expenditureHash = hashExpenditureAction(poll.action); + expenditurePollCounts[expenditureHash] = add(expenditurePollCounts[expenditureHash], 1); + bytes memory claimDelayAction = createClaimDelayAction(poll.action, UINT256_MAX); + executeCall(_pollId, claimDelayAction); } // Claim tokens if fully staked @@ -269,6 +261,27 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { emit PollStaked(_pollId, msg.sender, _vote, amount); } + function unstakePoll( + uint256 _pollId, + uint256 _permissionDomainId, // For extension's arbitration permission + uint256 _childSkillIndex, // For extension's arbitration permission + uint256 _vote, + uint256 _amount + ) + public + { + Poll storage poll = polls[_pollId]; + require(getPollState(_pollId) == PollState.Staking, "voting-rep-not-staking"); + + tokenLocking.transfer(token, _amount, msg.sender, true); + + poll.unpaidRewards = sub(poll.unpaidRewards, _amount); + poll.stakes[_vote] = sub(poll.stakes[_vote], _amount); + stakes[_pollId][msg.sender][_vote] = sub(stakes[_pollId][msg.sender][_vote], _amount); + + emit PollUnstaked(_pollId, msg.sender, _vote, _amount); + } + function submitVote( uint256 _pollId, bytes32 _voteSecret, @@ -368,7 +381,6 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { require(pollState == PollState.Executable, "voting-rep-poll-not-executable"); poll.executed = true; - delete activePolls[actionHash]; bool canExecute = ( poll.stakes[NAY] <= poll.stakes[YAY] && @@ -492,10 +504,6 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { poll = polls[_pollId]; } - function getActivePoll(bytes32 _actionHash) public view returns (uint256) { - return activePolls[_actionHash]; - } - function getExpenditurePollCount(bytes32 _expenditureHash) public view returns (uint256) { return expenditurePollCounts[_expenditureHash]; } @@ -539,7 +547,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { } else if (now < poll.events[CREATE] + stakePeriod && poll.events[STAKE_END] == 0) { return PollState.Staking; - // Fully staked, no folds, go to a vote + // Fully staked, go to a vote } else { if (now < min( @@ -562,12 +570,12 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { )) { return PollState.Reveal; } else if (now < min( - poll.events[CREATE] + stakePeriod + submitPeriod + revealPeriod + stakePeriod, + poll.events[CREATE] + stakePeriod + submitPeriod + revealPeriod + escalationPeriod, min( - selfOrMax(poll.events[STAKE_END]) + submitPeriod + revealPeriod + stakePeriod, + selfOrMax(poll.events[STAKE_END]) + submitPeriod + revealPeriod + escalationPeriod, min( - selfOrMax(poll.events[SUBMIT_END]) + revealPeriod + stakePeriod, - selfOrMax(poll.events[REVEAL_END]) + stakePeriod + selfOrMax(poll.events[SUBMIT_END]) + revealPeriod + escalationPeriod, + selfOrMax(poll.events[REVEAL_END]) + escalationPeriod ) ) )) { diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index 3f4370f816..71a2ec3c97 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -75,6 +75,7 @@ contract("Voting Reputation", (accounts) => { const STAKE_PERIOD = SECONDS_PER_DAY * 3; const VOTE_PERIOD = SECONDS_PER_DAY * 2; const REVEAL_PERIOD = SECONDS_PER_DAY * 2; + const ESCALATION_PERIOD = SECONDS_PER_DAY; const USER0 = accounts[0]; const USER1 = accounts[1]; @@ -135,7 +136,8 @@ contract("Voting Reputation", (accounts) => { VOTE_POWER_FRACTION, STAKE_PERIOD, VOTE_PERIOD, - REVEAL_PERIOD + REVEAL_PERIOD, + ESCALATION_PERIOD ); await colony.setArbitrationRole(1, UINT256_MAX, voting.address, 1, true); @@ -241,10 +243,13 @@ contract("Voting Reputation", (accounts) => { }); it("cannot initialise twice or if not root", async () => { - await checkErrorRevert(voting.initialise(WAD, WAD, WAD, WAD, WAD, STAKE_PERIOD, VOTE_PERIOD, REVEAL_PERIOD), "voting-rep-already-initialised"); + await checkErrorRevert( + voting.initialise(WAD, WAD, WAD, WAD, WAD, STAKE_PERIOD, VOTE_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD), + "voting-rep-already-initialised" + ); await checkErrorRevert( - voting.initialise(WAD, WAD, WAD, WAD, WAD, STAKE_PERIOD, VOTE_PERIOD, REVEAL_PERIOD, { from: USER2 }), + voting.initialise(WAD, WAD, WAD, WAD, WAD, STAKE_PERIOD, VOTE_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD, { from: USER2 }), "voting-rep-user-not-root" ); }); @@ -255,26 +260,52 @@ contract("Voting Reputation", (accounts) => { const votingAddress = await votingFactory.deployedExtensions(colony.address); voting = await VotingReputation.at(votingAddress); - await checkErrorRevert(voting.initialise(WAD.addn(1), WAD, WAD, WAD, WAD, STAKE_PERIOD, VOTE_PERIOD, REVEAL_PERIOD), "voting-rep-must-be-wad"); - await checkErrorRevert(voting.initialise(WAD, WAD.addn(1), WAD, WAD, WAD, STAKE_PERIOD, VOTE_PERIOD, REVEAL_PERIOD), "voting-rep-must-be-wad"); - await checkErrorRevert(voting.initialise(WAD, WAD, WAD.addn(1), WAD, WAD, STAKE_PERIOD, VOTE_PERIOD, REVEAL_PERIOD), "voting-rep-must-be-wad"); - await checkErrorRevert(voting.initialise(WAD, WAD, WAD, WAD.addn(1), WAD, STAKE_PERIOD, VOTE_PERIOD, REVEAL_PERIOD), "voting-rep-must-be-wad"); - await checkErrorRevert(voting.initialise(WAD, WAD, WAD, WAD, WAD.addn(1), STAKE_PERIOD, VOTE_PERIOD, REVEAL_PERIOD), "voting-rep-must-be-wad"); + await checkErrorRevert( + voting.initialise(WAD.addn(1), WAD, WAD, WAD, WAD, STAKE_PERIOD, VOTE_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD), + "voting-rep-must-be-wad" + ); + + await checkErrorRevert( + voting.initialise(WAD, WAD.addn(1), WAD, WAD, WAD, STAKE_PERIOD, VOTE_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD), + "voting-rep-must-be-wad" + ); + + await checkErrorRevert( + voting.initialise(WAD, WAD, WAD.addn(1), WAD, WAD, STAKE_PERIOD, VOTE_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD), + "voting-rep-must-be-wad" + ); await checkErrorRevert( - voting.initialise(WAD, WAD, WAD, WAD, WAD, SECONDS_PER_DAY * 366, VOTE_PERIOD, REVEAL_PERIOD), + voting.initialise(WAD, WAD, WAD, WAD.addn(1), WAD, STAKE_PERIOD, VOTE_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD), + "voting-rep-must-be-wad" + ); + + await checkErrorRevert( + voting.initialise(WAD, WAD, WAD, WAD, WAD.addn(1), STAKE_PERIOD, VOTE_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD), + "voting-rep-must-be-wad" + ); + + await checkErrorRevert( + voting.initialise(WAD, WAD, WAD, WAD, WAD, SECONDS_PER_DAY * 366, VOTE_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD), "voting-rep-period-too-long" ); + await checkErrorRevert( - voting.initialise(WAD, WAD, WAD, WAD, WAD, STAKE_PERIOD, SECONDS_PER_DAY * 366, REVEAL_PERIOD), + voting.initialise(WAD, WAD, WAD, WAD, WAD, STAKE_PERIOD, SECONDS_PER_DAY * 366, REVEAL_PERIOD, ESCALATION_PERIOD), "voting-rep-period-too-long" ); + await checkErrorRevert( - voting.initialise(WAD, WAD, WAD, WAD, WAD, STAKE_PERIOD, VOTE_PERIOD, SECONDS_PER_DAY * 366), + voting.initialise(WAD, WAD, WAD, WAD, WAD, STAKE_PERIOD, VOTE_PERIOD, SECONDS_PER_DAY * 366, ESCALATION_PERIOD), "voting-rep-period-too-long" ); - await voting.initialise(WAD, WAD, WAD, WAD, WAD, STAKE_PERIOD, VOTE_PERIOD, REVEAL_PERIOD); + await checkErrorRevert( + voting.initialise(WAD, WAD, WAD, WAD, WAD, STAKE_PERIOD, VOTE_PERIOD, REVEAL_PERIOD, SECONDS_PER_DAY * 366), + "voting-rep-period-too-long" + ); + + await voting.initialise(WAD, WAD, WAD, WAD, WAD, STAKE_PERIOD, VOTE_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD); }); }); @@ -356,6 +387,18 @@ contract("Voting Reputation", (accounts) => { expect(stake1).to.eq.BN(REQUIRED_STAKE.divn(2)); }); + it("can withdraw a stake", async () => { + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + + let poll = await voting.getPoll(pollId); + expect(poll.stakes[1]).to.eq.BN(REQUIRED_STAKE); + + await voting.unstakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, { from: USER0 }); + + poll = await voting.getPoll(pollId); + expect(poll.stakes[1]).to.be.zero; + }); + it("cannot stake less than the minStake", async () => { const minStake = REQUIRED_STAKE.divn(10); @@ -387,40 +430,6 @@ contract("Voting Reputation", (accounts) => { ); }); - it("cannot stake for an action while there is an active poll for the same action", async () => { - let activePoll; - - const { action } = await voting.getPoll(pollId); - activePoll = await voting.getActivePoll(soliditySha3(action)); - expect(activePoll).to.be.zero; - - await forwardTime(STAKE_PERIOD / 2, this); - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - - activePoll = await voting.getActivePoll(soliditySha3(action)); - expect(activePoll).to.eq.BN(pollId); - - await voting.createRootPoll(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); - const otherPollId = await voting.getPollCount(); - - await checkErrorRevert( - voting.stakePoll(otherPollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), - "voting-rep-competing-poll-exists" - ); - - // But, can stake once the first poll is executed. - await forwardTime(STAKE_PERIOD / 2, this); - await voting.executePoll(pollId); - - activePoll = await voting.getActivePoll(soliditySha3(action)); - expect(activePoll).to.be.zero; - - await voting.stakePoll(otherPollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - - activePoll = await voting.getActivePoll(soliditySha3(action)); - expect(activePoll).to.eq.BN(otherPollId); - }); - it("can update the expenditure globalClaimDelay if voting on expenditure state", async () => { await colony.makeExpenditure(1, UINT256_MAX, 1); const expenditureId = await colony.getExpenditureCount(); From aadcbb316d22ef1b2802522b7af7c369652296bf Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Mon, 20 Jul 2020 13:30:25 -0700 Subject: [PATCH 27/61] Add stake withdraw restriction --- contracts/extensions/VotingReputation.sol | 3 +++ test/extensions/voting-rep.js | 11 +++++++++++ 2 files changed, 14 insertions(+) diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index 14faac2fca..0a5920b5dc 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -52,6 +52,8 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { uint256 constant SUBMIT_END = 2; uint256 constant REVEAL_END = 3; + uint256 constant WITHDRAW_FRACTION = (WAD / 4) * 3; // 75%, cannot withdraw stake after this + bytes4 constant CHANGE_FUNCTION = bytes4( keccak256("setExpenditureState(uint256,uint256,uint256,uint256,bool[],bytes32[],bytes32)") ); @@ -272,6 +274,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { { Poll storage poll = polls[_pollId]; require(getPollState(_pollId) == PollState.Staking, "voting-rep-not-staking"); + require(now < add(poll.events[CREATE], wmul(stakePeriod, WITHDRAW_FRACTION)), "voting-rep-cannot-withdraw"); tokenLocking.transfer(token, _amount, msg.sender, true); diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index 71a2ec3c97..c1365fb4fb 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -399,6 +399,17 @@ contract("Voting Reputation", (accounts) => { expect(poll.stakes[1]).to.be.zero; }); + it("cannot withdraw a stake during the last 25% of the staking period", async () => { + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + + const poll = await voting.getPoll(pollId); + expect(poll.stakes[1]).to.eq.BN(REQUIRED_STAKE); + + await forwardTime(STAKE_PERIOD * 0.75 + 1, this); + + await checkErrorRevert(voting.unstakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, { from: USER0 }), "voting-rep-cannot-withdraw"); + }); + it("cannot stake less than the minStake", async () => { const minStake = REQUIRED_STAKE.divn(10); From 22d8cb0c94ef5471decdacf4c7592d6919d5eb50 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Tue, 21 Jul 2020 14:16:11 -0700 Subject: [PATCH 28/61] Limit locking to expenditure state changes --- contracts/extensions/VotingReputation.sol | 57 ++++++++++------------- test/extensions/voting-rep.js | 10 ++-- 2 files changed, 30 insertions(+), 37 deletions(-) diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index 0a5920b5dc..ad209c8f5d 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -166,8 +166,8 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { mapping (address => mapping (uint256 => bytes32)) voteSecrets; - mapping (bytes32 => uint256) pastPolls; // action signature => voting power - mapping (bytes32 => uint256) expenditurePollCounts; // expenditure signature => count + mapping (bytes32 => uint256) expenditurePastPolls; // expenditure slot signature => voting power + mapping (bytes32 => uint256) expenditurePollCounts; // expenditure struct signature => count // Public functions (interface) @@ -248,8 +248,8 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { // Increment counter & extend claim delay if staking for an expenditure state change if (poll.stakes[YAY] == requiredStake && _vote == YAY && getSig(poll.action) == CHANGE_FUNCTION) { - bytes32 expenditureHash = hashExpenditureAction(poll.action); - expenditurePollCounts[expenditureHash] = add(expenditurePollCounts[expenditureHash], 1); + bytes32 structHash = hashExpenditureStruct(poll.action); + expenditurePollCounts[structHash] = add(expenditurePollCounts[structHash], 1); bytes memory claimDelayAction = createClaimDelayAction(poll.action, UINT256_MAX); executeCall(_pollId, claimDelayAction); } @@ -376,7 +376,6 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { function executePoll(uint256 _pollId) public { Poll storage poll = polls[_pollId]; PollState pollState = getPollState(_pollId); - bytes32 actionHash = hashAction(poll.action); require(pollState != PollState.Failed, "voting-rep-poll-failed"); require(pollState != PollState.Closed, "voting-rep-poll-escalation-window-open"); @@ -391,11 +390,11 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { ); if (getSig(poll.action) == CHANGE_FUNCTION) { - bytes32 expenditureHash = hashExpenditureAction(poll.action); - expenditurePollCounts[expenditureHash] = sub(expenditurePollCounts[expenditureHash], 1); + bytes32 structHash = hashExpenditureStruct(poll.action); + expenditurePollCounts[structHash] = sub(expenditurePollCounts[structHash], 1); // Release the claimDelay if this is the last active poll - if (expenditurePollCounts[expenditureHash] == 0) { + if (expenditurePollCounts[structHash] == 0) { bytes memory claimDelayAction = createClaimDelayAction(poll.action, 0); executeCall(_pollId, claimDelayAction); } @@ -411,8 +410,9 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { votePower = poll.votes[YAY]; } - if (pastPolls[actionHash] < votePower) { - pastPolls[actionHash] = votePower; + bytes32 slotHash = hashExpenditureSlot(poll.action); + if (expenditurePastPolls[slotHash] < votePower) { + expenditurePastPolls[slotHash] = votePower; canExecute = canExecute && true; } else { canExecute = canExecute && false; @@ -507,16 +507,16 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { poll = polls[_pollId]; } - function getExpenditurePollCount(bytes32 _expenditureHash) public view returns (uint256) { - return expenditurePollCounts[_expenditureHash]; - } - function getStake(uint256 _pollId, address _staker, uint256 _vote) public view returns (uint256) { return stakes[_pollId][_staker][_vote]; } - function getPastPoll(bytes32 _slot) public view returns (uint256) { - return pastPolls[_slot]; + function getExpenditurePollCount(bytes32 _structHash) public view returns (uint256) { + return expenditurePollCounts[_structHash]; + } + + function getExpenditurePastPoll(bytes32 _slotHash) public view returns (uint256) { + return expenditurePastPolls[_slotHash]; } function getPollState(uint256 _pollId) public view returns (PollState) { @@ -706,25 +706,18 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { } } - function hashAction(bytes memory action) internal returns (bytes32 hash) { - if (getSig(action) == CHANGE_FUNCTION) { - assembly { - // Hash all but last (value) bytes32 - // Recall: mload(action) gives length of bytes array - // So skip past the first bytes32 (length), and the last bytes32 (value) - hash := keccak256(add(action, 0x20), sub(mload(action), 0x20)) - } - } else { - assembly { - // Hash entire action - // Recall: mload(action) gives length of bytes array - // So skip past the first bytes32 (length) - hash := keccak256(add(action, 0x20), mload(action)) - } + function hashExpenditureSlot(bytes memory action) internal returns (bytes32 hash) { + assert(getSig(action) == CHANGE_FUNCTION); + + assembly { + // Hash all but last (value) bytes32 + // Recall: mload(action) gives length of bytes array + // So skip past the first bytes32 (length), and the last bytes32 (value) + hash := keccak256(add(action, 0x20), sub(mload(action), 0x20)) } } - function hashExpenditureAction(bytes memory action) internal returns (bytes32 hash) { + function hashExpenditureStruct(bytes memory action) internal returns (bytes32 hash) { assert(getSig(action) == CHANGE_FUNCTION); uint256 expenditureId; diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index c1365fb4fb..506970c311 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -999,8 +999,8 @@ contract("Voting Reputation", (accounts) => { await forwardTime(STAKE_PERIOD, this); await voting.executePoll(pollId); - const slot = soliditySha3(action.slice(0, action.length - 64)); - const pastPoll = await voting.getPastPoll(slot); + const slotHash = soliditySha3(`0x${action.slice(2, action.length - 64)}`); + const pastPoll = await voting.getExpenditurePastPoll(slotHash); expect(pastPoll).to.eq.BN(WAD.muln(2).subn(2)); // ~66.6% of 3 WAD }); @@ -1020,11 +1020,11 @@ contract("Voting Reputation", (accounts) => { await voting.revealVote(pollId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await forwardTime(REVEAL_PERIOD, this); - await forwardTime(STAKE_PERIOD, this); + await forwardTime(ESCALATION_PERIOD, this); await voting.executePoll(pollId); - const slot = soliditySha3(action.slice(0, action.length - 64)); - const pastPoll = await voting.getPastPoll(slot); + const slotHash = soliditySha3(`0x${action.slice(2, action.length - 64)}`); + const pastPoll = await voting.getExpenditurePastPoll(slotHash); expect(pastPoll).to.eq.BN(WAD); // USER0 had 1 WAD of reputation }); }); From 8338d85bc526f6209a98fb64ed67c43fa8fb6342 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Tue, 21 Jul 2020 14:48:46 -0700 Subject: [PATCH 29/61] Disallow staking after commit cutoff unless other side is staked --- contracts/extensions/VotingReputation.sol | 16 ++++++++++++++-- test/extensions/voting-rep.js | 20 ++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index ad209c8f5d..92d9796d7f 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -52,7 +52,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { uint256 constant SUBMIT_END = 2; uint256 constant REVEAL_END = 3; - uint256 constant WITHDRAW_FRACTION = (WAD / 4) * 3; // 75%, cannot withdraw stake after this + uint256 constant STAKE_COMMIT_FRACTION = (WAD / 4) * 3; // 75%, cannot stake / withdraw after this bytes4 constant CHANGE_FUNCTION = bytes4( keccak256("setExpenditureState(uint256,uint256,uint256,uint256,bool[],bytes32[],bytes32)") @@ -68,6 +68,8 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { ITokenLocking tokenLocking; address token; + // All `Fraction` variables are WAD-denominated + uint256 stakeFraction; // Percent of domain reputation needed for staking uint256 minStakeFraction; // Minimum stake as percent of required stake (100% means single-staker) @@ -75,6 +77,8 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { uint256 voterRewardFraction; // Percent of stake paid out to voters as rewards (immediately taken from the stake) uint256 votePowerFraction; // Percent of domain rep used as vote power if no-contest + // All `Period` variables are second-denominated + uint256 stakePeriod; // Length of time for staking uint256 submitPeriod; // Length of time for submitting votes uint256 revealPeriod; // Length of time for revealing votes @@ -226,6 +230,14 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { require(getPollState(_pollId) == PollState.Staking, "voting-rep-staking-closed"); uint256 requiredStake = getRequiredStake(_pollId); + + // Either we are before the commit deadline or other side is fully staked + require( + now < add(poll.events[CREATE], wmul(stakePeriod, STAKE_COMMIT_FRACTION)) || + poll.stakes[flip(_vote)] == requiredStake, + "voting-rep-cannot-stake" + ); + uint256 amount = min(_amount, sub(requiredStake, poll.stakes[_vote])); uint256 stakerTotalAmount = add(stakes[_pollId][msg.sender][_vote], amount); @@ -274,7 +286,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { { Poll storage poll = polls[_pollId]; require(getPollState(_pollId) == PollState.Staking, "voting-rep-not-staking"); - require(now < add(poll.events[CREATE], wmul(stakePeriod, WITHDRAW_FRACTION)), "voting-rep-cannot-withdraw"); + require(now < add(poll.events[CREATE], wmul(stakePeriod, STAKE_COMMIT_FRACTION)), "voting-rep-cannot-withdraw"); tokenLocking.transfer(token, _amount, msg.sender, true); diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index 506970c311..3b5a017de2 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -410,6 +410,26 @@ contract("Voting Reputation", (accounts) => { await checkErrorRevert(voting.unstakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, { from: USER0 }), "voting-rep-cannot-withdraw"); }); + it("cannot stake during the last 25% of the staking period if other side is not fully staked", async () => { + await forwardTime(STAKE_PERIOD * 0.75 + 1, this); + + await checkErrorRevert( + voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }), + "voting-rep-cannot-stake" + ); + }); + + it("can stake during the last 25% of the staking period if other side is fully staked", async () => { + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + + const poll = await voting.getPoll(pollId); + expect(poll.stakes[1]).to.eq.BN(REQUIRED_STAKE); + + await forwardTime(STAKE_PERIOD * 0.75 + 1, this); + + await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + }); + it("cannot stake less than the minStake", async () => { const minStake = REQUIRED_STAKE.divn(10); From b8398e32d6999b27bb1c54e5644dbc3d9a7df1fc Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Fri, 24 Jul 2020 07:53:10 -0700 Subject: [PATCH 30/61] Improve timestamp handling --- contracts/extensions/VotingReputation.sol | 142 ++++++++-------------- test/extensions/voting-rep.js | 103 +++++----------- 2 files changed, 83 insertions(+), 162 deletions(-) diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index 92d9796d7f..f44a76616d 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -32,7 +32,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { // Events event ExtensionInitialised(); event ExtensionDeprecated(); - event PollCreated(uint256 indexed pollId, uint256 indexed skillId); + event PollCreated(uint256 indexed pollId, address creator, uint256 indexed domainId); event PollStaked(uint256 indexed pollId, address indexed staker, uint256 indexed vote, uint256 amount); event PollUnstaked(uint256 indexed pollId, address indexed staker, uint256 indexed vote, uint256 amount); event PollVoteSubmitted(uint256 indexed pollId, address indexed voter); @@ -47,13 +47,11 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { uint256 constant NAY = 0; uint256 constant YAY = 1; - uint256 constant CREATE = 0; - uint256 constant STAKE_END = 1; + uint256 constant STAKE1_END = 0; + uint256 constant STAKE2_END = 1; uint256 constant SUBMIT_END = 2; uint256 constant REVEAL_END = 3; - uint256 constant STAKE_COMMIT_FRACTION = (WAD / 4) * 3; // 75%, cannot stake / withdraw after this - bytes4 constant CHANGE_FUNCTION = bytes4( keccak256("setExpenditureState(uint256,uint256,uint256,uint256,bool[],bytes32[],bytes32)") ); @@ -148,7 +146,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { enum PollState { Staking, Submit, Reveal, Closed, Executable, Executed, Failed } struct Poll { - uint256[4] events; // Creation, Staking, Submission, Revelation + uint64[4] events; // Staking 1, Staking 2, Submission, Revelation bytes32 rootHash; uint256 domainId; uint256 skillId; @@ -167,8 +165,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { uint256 pollCount; mapping (uint256 => Poll) polls; mapping (uint256 => mapping (address => mapping (uint256 => uint256))) stakes; - - mapping (address => mapping (uint256 => bytes32)) voteSecrets; + mapping (uint256 => mapping (address => bytes32)) voteSecrets; mapping (bytes32 => uint256) expenditurePastPolls; // expenditure slot signature => voting power mapping (bytes32 => uint256) expenditurePollCounts; // expenditure struct signature => count @@ -230,19 +227,11 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { require(getPollState(_pollId) == PollState.Staking, "voting-rep-staking-closed"); uint256 requiredStake = getRequiredStake(_pollId); - - // Either we are before the commit deadline or other side is fully staked - require( - now < add(poll.events[CREATE], wmul(stakePeriod, STAKE_COMMIT_FRACTION)) || - poll.stakes[flip(_vote)] == requiredStake, - "voting-rep-cannot-stake" - ); - uint256 amount = min(_amount, sub(requiredStake, poll.stakes[_vote])); uint256 stakerTotalAmount = add(stakes[_pollId][msg.sender][_vote], amount); require( - stakerTotalAmount <= checkReputation(_pollId, msg.sender, _key, _value, _branchMask, _siblings), + stakerTotalAmount <= getReputationFromProof(_pollId, msg.sender, _key, _value, _branchMask, _siblings), "voting-rep-insufficient-rep" ); require( @@ -266,37 +255,28 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { executeCall(_pollId, claimDelayAction); } - // Claim tokens if fully staked + // Move to second staking window once one side is fully staked + if ( + (_vote == YAY && poll.stakes[YAY] == requiredStake && poll.stakes[NAY] < requiredStake) || + (_vote == NAY && poll.stakes[NAY] == requiredStake && poll.stakes[YAY] < requiredStake) + ) { + poll.events[STAKE1_END] = uint64(now); + poll.events[STAKE2_END] = uint64(now + stakePeriod); + poll.events[SUBMIT_END] = uint64(now + stakePeriod + submitPeriod); + poll.events[REVEAL_END] = uint64(now + stakePeriod + submitPeriod + revealPeriod); + } + + // Claim tokens once both sides are fully staked if (poll.stakes[YAY] == requiredStake && poll.stakes[NAY] == requiredStake) { - poll.events[STAKE_END] = now; + poll.events[STAKE2_END] = uint64(now); + poll.events[SUBMIT_END] = uint64(now + submitPeriod); + poll.events[REVEAL_END] = uint64(now + submitPeriod + revealPeriod); tokenLocking.claim(token, true); } emit PollStaked(_pollId, msg.sender, _vote, amount); } - function unstakePoll( - uint256 _pollId, - uint256 _permissionDomainId, // For extension's arbitration permission - uint256 _childSkillIndex, // For extension's arbitration permission - uint256 _vote, - uint256 _amount - ) - public - { - Poll storage poll = polls[_pollId]; - require(getPollState(_pollId) == PollState.Staking, "voting-rep-not-staking"); - require(now < add(poll.events[CREATE], wmul(stakePeriod, STAKE_COMMIT_FRACTION)), "voting-rep-cannot-withdraw"); - - tokenLocking.transfer(token, _amount, msg.sender, true); - - poll.unpaidRewards = sub(poll.unpaidRewards, _amount); - poll.stakes[_vote] = sub(poll.stakes[_vote], _amount); - stakes[_pollId][msg.sender][_vote] = sub(stakes[_pollId][msg.sender][_vote], _amount); - - emit PollUnstaked(_pollId, msg.sender, _vote, _amount); - } - function submitVote( uint256 _pollId, bytes32 _voteSecret, @@ -310,13 +290,14 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { Poll storage poll = polls[_pollId]; require(getPollState(_pollId) == PollState.Submit, "voting-rep-poll-not-open"); - uint256 userRep = checkReputation(_pollId, msg.sender, _key, _value, _branchMask, _siblings); + uint256 userRep = getReputationFromProof(_pollId, msg.sender, _key, _value, _branchMask, _siblings); - voteSecrets[msg.sender][_pollId] = _voteSecret; + voteSecrets[_pollId][msg.sender] = _voteSecret; poll.repSubmitted = add(poll.repSubmitted, userRep); if (poll.repSubmitted >= wmul(poll.skillRep, maxVoteFraction)) { - poll.events[SUBMIT_END] = now; + poll.events[SUBMIT_END] = uint64(now); + poll.events[REVEAL_END] = uint64(now + revealPeriod); } emit PollVoteSubmitted(_pollId, msg.sender); @@ -336,17 +317,17 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { Poll storage poll = polls[_pollId]; require(getPollState(_pollId) == PollState.Reveal, "voting-rep-poll-not-reveal"); - uint256 userRep = checkReputation(_pollId, msg.sender, _key, _value, _branchMask, _siblings); + uint256 userRep = getReputationFromProof(_pollId, msg.sender, _key, _value, _branchMask, _siblings); - bytes32 voteSecret = voteSecrets[msg.sender][_pollId]; + bytes32 voteSecret = voteSecrets[_pollId][msg.sender]; require(voteSecret == getVoteSecret(_salt, _vote), "voting-rep-secret-no-match"); - delete voteSecrets[msg.sender][_pollId]; + delete voteSecrets[_pollId][msg.sender]; poll.votes[_vote] = add(poll.votes[_vote], userRep); poll.repRevealed = add(poll.repRevealed, userRep); if (poll.repRevealed >= wmul(poll.skillRep, maxVoteFraction)) { - poll.events[REVEAL_END] = now; + poll.events[REVEAL_END] = uint64(now); } uint256 pctReputation = wdiv(userRep, poll.skillRep); @@ -379,20 +360,19 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { delete poll.events; - poll.events[CREATE] = now; + poll.events[STAKE1_END] = uint64(now + stakePeriod); + poll.events[STAKE2_END] = uint64(now + stakePeriod); + poll.events[SUBMIT_END] = uint64(now + stakePeriod + submitPeriod); + poll.events[REVEAL_END] = uint64(now + stakePeriod + submitPeriod + revealPeriod); + poll.domainId = _newDomainId; poll.skillId = newDomainSkillId; - poll.skillRep = checkReputation(_pollId, address(0x0), _key, _value, _branchMask, _siblings); + poll.skillRep = getReputationFromProof(_pollId, address(0x0), _key, _value, _branchMask, _siblings); } function executePoll(uint256 _pollId) public { Poll storage poll = polls[_pollId]; - PollState pollState = getPollState(_pollId); - - require(pollState != PollState.Failed, "voting-rep-poll-failed"); - require(pollState != PollState.Closed, "voting-rep-poll-escalation-window-open"); - require(pollState != PollState.Executed, "voting-rep-poll-already-executed"); - require(pollState == PollState.Executable, "voting-rep-poll-not-executable"); + require(getPollState(_pollId) == PollState.Executable, "voting-rep-poll-not-executable"); poll.executed = true; @@ -537,6 +517,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { // If executed, we're done if (poll.executed) { + return PollState.Executed; // Not fully staked @@ -544,8 +525,9 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { poll.stakes[YAY] < requiredStake || poll.stakes[NAY] < requiredStake ) { + // Are we still staking? - if (now < poll.events[CREATE] + stakePeriod && poll.events[STAKE_END] == 0) { + if (now < poll.events[STAKE2_END]) { return PollState.Staking; // If not, did the YAY side stake? } else if (poll.stakes[YAY] == requiredStake) { @@ -559,41 +541,18 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { } // Do we need to keep waiting? - } else if (now < poll.events[CREATE] + stakePeriod && poll.events[STAKE_END] == 0) { + } else if (now < poll.events[STAKE2_END]) { + return PollState.Staking; // Fully staked, go to a vote } else { - if (now < min( - poll.events[CREATE] + stakePeriod + submitPeriod, - min( - selfOrMax(poll.events[STAKE_END]) + submitPeriod, - selfOrMax(poll.events[SUBMIT_END]) - ) - )) { + if (now < poll.events[SUBMIT_END]) { return PollState.Submit; - } else if (now < min( - poll.events[CREATE] + stakePeriod + submitPeriod + revealPeriod, - min( - selfOrMax(poll.events[STAKE_END]) + submitPeriod + revealPeriod, - min( - selfOrMax(poll.events[SUBMIT_END]) + revealPeriod, - selfOrMax(poll.events[REVEAL_END]) - ) - ) - )) { + } else if (now < poll.events[REVEAL_END]) { return PollState.Reveal; - } else if (now < min( - poll.events[CREATE] + stakePeriod + submitPeriod + revealPeriod + escalationPeriod, - min( - selfOrMax(poll.events[STAKE_END]) + submitPeriod + revealPeriod + escalationPeriod, - min( - selfOrMax(poll.events[SUBMIT_END]) + revealPeriod + escalationPeriod, - selfOrMax(poll.events[REVEAL_END]) + escalationPeriod - ) - ) - )) { + } else if (now < poll.events[REVEAL_END] + escalationPeriod) { return PollState.Closed; } else { return PollState.Executable; @@ -619,15 +578,20 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { require(state == ExtensionState.Active, "voting-rep-not-active"); pollCount += 1; - polls[pollCount].events[CREATE] = now; + + polls[pollCount].events[STAKE1_END] = uint64(now + stakePeriod); + polls[pollCount].events[STAKE2_END] = uint64(now + stakePeriod); + polls[pollCount].events[SUBMIT_END] = uint64(now + stakePeriod + submitPeriod); + polls[pollCount].events[REVEAL_END] = uint64(now + stakePeriod + submitPeriod + revealPeriod); + polls[pollCount].rootHash = colonyNetwork.getReputationRootHash(); polls[pollCount].domainId = _domainId; polls[pollCount].skillId = _skillId; - polls[pollCount].skillRep = checkReputation(pollCount, address(0x0), _key, _value, _branchMask, _siblings); + polls[pollCount].skillRep = getReputationFromProof(pollCount, address(0x0), _key, _value, _branchMask, _siblings); polls[pollCount].target = _target; polls[pollCount].action = _action; - emit PollCreated(pollCount, _skillId); + emit PollCreated(pollCount, msg.sender, _domainId); } function getVoteSecret(bytes32 _salt, uint256 _vote) internal pure returns (bytes32) { @@ -646,7 +610,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { return (_timestamp == 0) ? UINT128_MAX : _timestamp; } - function checkReputation( + function getReputationFromProof( uint256 _pollId, address _who, bytes memory _key, diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index 3b5a017de2..3e0b2e7286 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -73,7 +73,7 @@ contract("Voting Reputation", (accounts) => { const VOTE_POWER_FRACTION = WAD.muln(2).divn(3); // 66 % const STAKE_PERIOD = SECONDS_PER_DAY * 3; - const VOTE_PERIOD = SECONDS_PER_DAY * 2; + const SUBMIT_PERIOD = SECONDS_PER_DAY * 2; const REVEAL_PERIOD = SECONDS_PER_DAY * 2; const ESCALATION_PERIOD = SECONDS_PER_DAY; @@ -135,7 +135,7 @@ contract("Voting Reputation", (accounts) => { VOTER_REWARD_FRACTION, VOTE_POWER_FRACTION, STAKE_PERIOD, - VOTE_PERIOD, + SUBMIT_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD ); @@ -244,12 +244,12 @@ contract("Voting Reputation", (accounts) => { it("cannot initialise twice or if not root", async () => { await checkErrorRevert( - voting.initialise(WAD, WAD, WAD, WAD, WAD, STAKE_PERIOD, VOTE_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD), + voting.initialise(WAD, WAD, WAD, WAD, WAD, STAKE_PERIOD, SUBMIT_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD), "voting-rep-already-initialised" ); await checkErrorRevert( - voting.initialise(WAD, WAD, WAD, WAD, WAD, STAKE_PERIOD, VOTE_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD, { from: USER2 }), + voting.initialise(WAD, WAD, WAD, WAD, WAD, STAKE_PERIOD, SUBMIT_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD, { from: USER2 }), "voting-rep-user-not-root" ); }); @@ -261,32 +261,32 @@ contract("Voting Reputation", (accounts) => { voting = await VotingReputation.at(votingAddress); await checkErrorRevert( - voting.initialise(WAD.addn(1), WAD, WAD, WAD, WAD, STAKE_PERIOD, VOTE_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD), + voting.initialise(WAD.addn(1), WAD, WAD, WAD, WAD, STAKE_PERIOD, SUBMIT_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD), "voting-rep-must-be-wad" ); await checkErrorRevert( - voting.initialise(WAD, WAD.addn(1), WAD, WAD, WAD, STAKE_PERIOD, VOTE_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD), + voting.initialise(WAD, WAD.addn(1), WAD, WAD, WAD, STAKE_PERIOD, SUBMIT_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD), "voting-rep-must-be-wad" ); await checkErrorRevert( - voting.initialise(WAD, WAD, WAD.addn(1), WAD, WAD, STAKE_PERIOD, VOTE_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD), + voting.initialise(WAD, WAD, WAD.addn(1), WAD, WAD, STAKE_PERIOD, SUBMIT_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD), "voting-rep-must-be-wad" ); await checkErrorRevert( - voting.initialise(WAD, WAD, WAD, WAD.addn(1), WAD, STAKE_PERIOD, VOTE_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD), + voting.initialise(WAD, WAD, WAD, WAD.addn(1), WAD, STAKE_PERIOD, SUBMIT_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD), "voting-rep-must-be-wad" ); await checkErrorRevert( - voting.initialise(WAD, WAD, WAD, WAD, WAD.addn(1), STAKE_PERIOD, VOTE_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD), + voting.initialise(WAD, WAD, WAD, WAD, WAD.addn(1), STAKE_PERIOD, SUBMIT_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD), "voting-rep-must-be-wad" ); await checkErrorRevert( - voting.initialise(WAD, WAD, WAD, WAD, WAD, SECONDS_PER_DAY * 366, VOTE_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD), + voting.initialise(WAD, WAD, WAD, WAD, WAD, SECONDS_PER_DAY * 366, SUBMIT_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD), "voting-rep-period-too-long" ); @@ -296,16 +296,16 @@ contract("Voting Reputation", (accounts) => { ); await checkErrorRevert( - voting.initialise(WAD, WAD, WAD, WAD, WAD, STAKE_PERIOD, VOTE_PERIOD, SECONDS_PER_DAY * 366, ESCALATION_PERIOD), + voting.initialise(WAD, WAD, WAD, WAD, WAD, STAKE_PERIOD, SUBMIT_PERIOD, SECONDS_PER_DAY * 366, ESCALATION_PERIOD), "voting-rep-period-too-long" ); await checkErrorRevert( - voting.initialise(WAD, WAD, WAD, WAD, WAD, STAKE_PERIOD, VOTE_PERIOD, REVEAL_PERIOD, SECONDS_PER_DAY * 366), + voting.initialise(WAD, WAD, WAD, WAD, WAD, STAKE_PERIOD, SUBMIT_PERIOD, REVEAL_PERIOD, SECONDS_PER_DAY * 366), "voting-rep-period-too-long" ); - await voting.initialise(WAD, WAD, WAD, WAD, WAD, STAKE_PERIOD, VOTE_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD); + await voting.initialise(WAD, WAD, WAD, WAD, WAD, STAKE_PERIOD, SUBMIT_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD); }); }); @@ -387,49 +387,6 @@ contract("Voting Reputation", (accounts) => { expect(stake1).to.eq.BN(REQUIRED_STAKE.divn(2)); }); - it("can withdraw a stake", async () => { - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - - let poll = await voting.getPoll(pollId); - expect(poll.stakes[1]).to.eq.BN(REQUIRED_STAKE); - - await voting.unstakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, { from: USER0 }); - - poll = await voting.getPoll(pollId); - expect(poll.stakes[1]).to.be.zero; - }); - - it("cannot withdraw a stake during the last 25% of the staking period", async () => { - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - - const poll = await voting.getPoll(pollId); - expect(poll.stakes[1]).to.eq.BN(REQUIRED_STAKE); - - await forwardTime(STAKE_PERIOD * 0.75 + 1, this); - - await checkErrorRevert(voting.unstakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, { from: USER0 }), "voting-rep-cannot-withdraw"); - }); - - it("cannot stake during the last 25% of the staking period if other side is not fully staked", async () => { - await forwardTime(STAKE_PERIOD * 0.75 + 1, this); - - await checkErrorRevert( - voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }), - "voting-rep-cannot-stake" - ); - }); - - it("can stake during the last 25% of the staking period if other side is fully staked", async () => { - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - - const poll = await voting.getPoll(pollId); - expect(poll.stakes[1]).to.eq.BN(REQUIRED_STAKE); - - await forwardTime(STAKE_PERIOD * 0.75 + 1, this); - - await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - }); - it("cannot stake less than the minStake", async () => { const minStake = REQUIRED_STAKE.divn(10); @@ -674,7 +631,7 @@ contract("Voting Reputation", (accounts) => { it("can rate and reveal for a poll", async () => { await voting.submitVote(pollId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await forwardTime(VOTE_PERIOD, this); + await forwardTime(SUBMIT_PERIOD, this); await voting.revealVote(pollId, SALT, NAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); }); @@ -696,7 +653,7 @@ contract("Voting Reputation", (accounts) => { await voting.submitVote(pollId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await voting.submitVote(pollId, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await forwardTime(VOTE_PERIOD, this); + await forwardTime(SUBMIT_PERIOD, this); // Revealing first vote fails await checkErrorRevert( @@ -711,7 +668,7 @@ contract("Voting Reputation", (accounts) => { it("cannot reveal a vote twice, and so cannot vote twice", async () => { await voting.submitVote(pollId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await forwardTime(VOTE_PERIOD, this); + await forwardTime(SUBMIT_PERIOD, this); await voting.revealVote(pollId, SALT, NAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); @@ -752,14 +709,14 @@ contract("Voting Reputation", (accounts) => { await voting.submitVote(pollId2, soliditySha3(SALT, NAY), user0Key, user0Value2, user0Mask2, user0Siblings2, { from: USER0 }); - await forwardTime(VOTE_PERIOD, this); + await forwardTime(SUBMIT_PERIOD, this); await voting.revealVote(pollId, SALT, NAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await voting.revealVote(pollId2, SALT, NAY, user0Key, user0Value2, user0Mask2, user0Siblings2, { from: USER0 }); }); it("cannot submit a vote if voting is closed", async () => { - await forwardTime(VOTE_PERIOD, this); + await forwardTime(SUBMIT_PERIOD, this); await checkErrorRevert( voting.submitVote(pollId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), @@ -775,7 +732,7 @@ contract("Voting Reputation", (accounts) => { it("cannot reveal a vote after voting closes", async () => { await voting.submitVote(pollId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await forwardTime(VOTE_PERIOD, this); + await forwardTime(SUBMIT_PERIOD, this); await forwardTime(REVEAL_PERIOD, this); await checkErrorRevert(voting.revealVote(pollId, SALT, NAY, FAKE, FAKE, 0, [], { from: USER0 }), "voting-rep-poll-not-reveal"); @@ -784,7 +741,7 @@ contract("Voting Reputation", (accounts) => { it("cannot reveal a vote with a bad secret", async () => { await voting.submitVote(pollId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await forwardTime(VOTE_PERIOD, this); + await forwardTime(SUBMIT_PERIOD, this); await checkErrorRevert( voting.revealVote(pollId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), @@ -795,7 +752,7 @@ contract("Voting Reputation", (accounts) => { it("cannot reveal a vote with a bad proof", async () => { await voting.submitVote(pollId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await forwardTime(VOTE_PERIOD, this); + await forwardTime(SUBMIT_PERIOD, this); // Invalid proof (wrong root hash) await checkErrorRevert(voting.revealVote(pollId, SALT, NAY, FAKE, FAKE, 0, [], { from: USER0 }), "voting-rep-invalid-root-hash"); @@ -837,7 +794,7 @@ contract("Voting Reputation", (accounts) => { await forwardTime(STAKE_PERIOD, this); - await checkErrorRevert(voting.executePoll(pollId), "voting-rep-poll-failed"); + await checkErrorRevert(voting.executePoll(pollId), "voting-rep-poll-not-executable"); }); it("can take an action if there is insufficient opposition", async () => { @@ -923,7 +880,7 @@ contract("Voting Reputation", (accounts) => { const { logs } = await voting.executePoll(pollId); expect(logs[0].args.success).to.be.true; - await checkErrorRevert(voting.executePoll(pollId), "voting-rep-poll-already-executed"); + await checkErrorRevert(voting.executePoll(pollId), "voting-rep-poll-not-executable"); }); it("can take an action if the poll passes", async () => { @@ -932,13 +889,13 @@ contract("Voting Reputation", (accounts) => { await voting.submitVote(pollId, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await forwardTime(VOTE_PERIOD, this); + await forwardTime(SUBMIT_PERIOD, this); await voting.revealVote(pollId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await forwardTime(REVEAL_PERIOD, this); - await checkErrorRevert(voting.executePoll(pollId), "voting-rep-poll-escalation-window-open"); + await checkErrorRevert(voting.executePoll(pollId), "voting-rep-poll-not-executable"); await forwardTime(STAKE_PERIOD, this); @@ -952,7 +909,7 @@ contract("Voting Reputation", (accounts) => { await voting.submitVote(pollId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await forwardTime(VOTE_PERIOD, this); + await forwardTime(SUBMIT_PERIOD, this); await voting.revealVote(pollId, SALT, NAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); @@ -977,7 +934,7 @@ contract("Voting Reputation", (accounts) => { await voting.submitVote(pollId1, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await forwardTime(VOTE_PERIOD, this); + await forwardTime(SUBMIT_PERIOD, this); await voting.revealVote(pollId1, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); @@ -997,7 +954,7 @@ contract("Voting Reputation", (accounts) => { await voting.submitVote(pollId2, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await forwardTime(VOTE_PERIOD, this); + await forwardTime(SUBMIT_PERIOD, this); await voting.revealVote(pollId2, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); @@ -1035,7 +992,7 @@ contract("Voting Reputation", (accounts) => { await voting.submitVote(pollId, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await forwardTime(VOTE_PERIOD, this); + await forwardTime(SUBMIT_PERIOD, this); await voting.revealVote(pollId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); @@ -1172,7 +1129,7 @@ contract("Voting Reputation", (accounts) => { await voting.submitVote(pollId, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await forwardTime(VOTE_PERIOD, this); + await forwardTime(SUBMIT_PERIOD, this); await voting.revealVote(pollId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); From f4c68a7a72d42c47d1d40042ab736028c03991c5 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Fri, 24 Jul 2020 11:01:23 -0700 Subject: [PATCH 31/61] Respond to review comments IV --- contracts/extensions/VotingReputation.sol | 226 ++++++++++++++++------ test/extensions/voting-rep.js | 195 +++++++++++++++---- 2 files changed, 320 insertions(+), 101 deletions(-) diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index f44a76616d..47ed2aa124 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -34,11 +34,12 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { event ExtensionDeprecated(); event PollCreated(uint256 indexed pollId, address creator, uint256 indexed domainId); event PollStaked(uint256 indexed pollId, address indexed staker, uint256 indexed vote, uint256 amount); - event PollUnstaked(uint256 indexed pollId, address indexed staker, uint256 indexed vote, uint256 amount); event PollVoteSubmitted(uint256 indexed pollId, address indexed voter); event PollVoteRevealed(uint256 indexed pollId, address indexed voter, uint256 indexed vote); event PollExecuted(uint256 indexed pollId, bytes action, bool success); + event PollEscalated(uint256 indexed pollId, address escalator, uint256 indexed domainId); event PollRewardClaimed(uint256 indexed pollId, address indexed staker, uint256 indexed vote, uint256 amount); + event PollEventSet(uint256 indexed pollId, uint256 eventIndex); // Constants uint256 constant UINT256_MAX = 2**256 - 1; @@ -66,14 +67,13 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { ITokenLocking tokenLocking; address token; - // All `Fraction` variables are WAD-denominated + // All `Fraction` variables are denominated in WADs (1 WAD = 100%) - uint256 stakeFraction; // Percent of domain reputation needed for staking - uint256 minStakeFraction; // Minimum stake as percent of required stake (100% means single-staker) + uint256 totalStakeFraction; // Percent of domain reputation needed for staking + uint256 userMinStakeFraction; // Minimum stake as percent of required stake (100% means single-staker) - uint256 maxVoteFraction; // The percent of total domain rep we need before closing the vote + uint256 maxVoteFraction; // Percent of total domain rep we need before closing the vote uint256 voterRewardFraction; // Percent of stake paid out to voters as rewards (immediately taken from the stake) - uint256 votePowerFraction; // Percent of domain rep used as vote power if no-contest // All `Period` variables are second-denominated @@ -89,12 +89,20 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { token = colony.getToken(); } + /// @notice Initialise the extension + /// @param _totalStakeFraction The percent of the domain's reputation we need to stake + /// @param _userMinStakeFraction The minimum per-user stake as percent of total stake + /// @param _maxVoteFraction The percent of the domain's reputation which must submit for quick-end + /// @param _voterRewardFraction The percent of the total stake paid out to voters as rewards + /// @param _stakePeriod The length of the staking period in seconds + /// @param _submitPeriod The length of the submit period in seconds + /// @param _revealPeriod The length of the reveal period in seconds + /// @param _escalationPeriod The length of the escalation period in seconds function initialise( - uint256 _stakeFraction, - uint256 _minStakeFraction, + uint256 _totalStakeFraction, + uint256 _userMinStakeFraction, uint256 _maxVoteFraction, uint256 _voterRewardFraction, - uint256 _votePowerFraction, uint256 _stakePeriod, uint256 _submitPeriod, uint256 _revealPeriod, @@ -105,12 +113,11 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { require(colony.hasUserRole(msg.sender, 1, ColonyDataTypes.ColonyRole.Root), "voting-rep-user-not-root"); require(state == ExtensionState.Deployed, "voting-rep-already-initialised"); - require(_stakeFraction <= WAD, "voting-rep-must-be-wad"); - require(_minStakeFraction <= WAD, "voting-rep-must-be-wad"); + require(_totalStakeFraction <= WAD, "voting-rep-greater-than-wad"); + require(_userMinStakeFraction <= WAD, "voting-rep-greater-than-wad"); - require(_maxVoteFraction <= WAD, "voting-rep-must-be-wad"); - require(_voterRewardFraction <= WAD, "voting-rep-must-be-wad"); - require(_votePowerFraction <= WAD, "voting-rep-must-be-wad"); + require(_maxVoteFraction <= WAD, "voting-rep-greater-than-wad"); + require(_voterRewardFraction <= WAD / 2, "voting-rep-greater-than-half-wad"); require(_stakePeriod <= 365 days, "voting-rep-period-too-long"); require(_submitPeriod <= 365 days, "voting-rep-period-too-long"); @@ -119,12 +126,11 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { state = ExtensionState.Active; - stakeFraction = _stakeFraction; - minStakeFraction = _minStakeFraction; + totalStakeFraction = _totalStakeFraction; + userMinStakeFraction = _userMinStakeFraction; maxVoteFraction = _maxVoteFraction; voterRewardFraction = _voterRewardFraction; - votePowerFraction = _votePowerFraction; stakePeriod = _stakePeriod; submitPeriod = _submitPeriod; @@ -134,6 +140,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { emit ExtensionInitialised(); } + /// @notice Deprecate the extension, prevening new polls from being created function deprecate() public { require(colony.hasUserRole(msg.sender, 1, ColonyDataTypes.ColonyRole.Root), "voting-rep-user-not-root"); @@ -146,14 +153,14 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { enum PollState { Staking, Submit, Reveal, Closed, Executable, Executed, Failed } struct Poll { - uint64[4] events; // Staking 1, Staking 2, Submission, Revelation + uint64[4] events; // For recording poll lifecycle timestamps (STAKE1, STAKE2, SUBMIT, REVEAL) bytes32 rootHash; uint256 domainId; uint256 skillId; uint256 skillRep; uint256 repSubmitted; uint256 repRevealed; - uint256 unpaidRewards; + uint256 paidVoterComp; uint256[2] stakes; // [nay, yay] uint256[2] votes; // [nay, yay] bool executed; @@ -172,6 +179,13 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { // Public functions (interface) + /// @notice Create a poll in the root domain + /// @param _target The contract to which we send the action (0x0 for the colony) + /// @param _action A bytes array encoding a function call + /// @param _key Reputation tree key for the root domain + /// @param _value Reputation tree value for the root domain + /// @param _branchMask The branchmask of the proof + /// @param _siblings The siblings of the proof function createRootPoll( address _target, bytes memory _action, @@ -186,6 +200,15 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { createPoll(_target, _action, 1, rootSkillId, _key, _value, _branchMask, _siblings); } + /// @notice Create a poll in any domain + /// @param _domainId The domain where we vote on the poll + /// @param _childSkillIndex The childSkillIndex pointing to the domain of the action + /// @param _target The contract to which we send the action (0x0 for the colony) + /// @param _action A bytes array encoding a function call + /// @param _key Reputation tree key for the domain + /// @param _value Reputation tree value for the domain + /// @param _branchMask The branchmask of the proof + /// @param _siblings The siblings of the proof function createDomainPoll( uint256 _domainId, uint256 _childSkillIndex, @@ -209,10 +232,20 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { createPoll(_target, _action, _domainId, domainSkillId, _key, _value, _branchMask, _siblings); } + /// @notice Stake on a poll + /// @param _pollId The id of the poll + /// @param _permissionDomainId The domain where the extension has the arbitration permission + /// @param _childSkillIndex For the domain in which the poll is occurring + /// @param _vote The side being supported (0 = NAY, 1 = YAY) + /// @param _amount The amount of tokens being staked + /// @param _key Reputation tree key for the staker/domain + /// @param _value Reputation tree value for the staker/domain + /// @param _branchMask The branchmask of the proof + /// @param _siblings The siblings of the proof function stakePoll( uint256 _pollId, - uint256 _permissionDomainId, // For extension's arbitration permission - uint256 _childSkillIndex, // For extension's arbitration permission + uint256 _permissionDomainId, + uint256 _childSkillIndex, uint256 _vote, uint256 _amount, bytes memory _key, @@ -235,7 +268,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { "voting-rep-insufficient-rep" ); require( - stakerTotalAmount >= wmul(requiredStake, minStakeFraction), + stakerTotalAmount >= wmul(requiredStake, userMinStakeFraction), "voting-rep-insufficient-stake" ); @@ -243,7 +276,6 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { colony.transferStake(_permissionDomainId, _childSkillIndex, address(this), msg.sender, poll.domainId, amount, address(this)); // Update the stake - poll.unpaidRewards = add(poll.unpaidRewards, amount); poll.stakes[_vote] = add(poll.stakes[_vote], amount); stakes[_pollId][msg.sender][_vote] = stakerTotalAmount; @@ -264,6 +296,8 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { poll.events[STAKE2_END] = uint64(now + stakePeriod); poll.events[SUBMIT_END] = uint64(now + stakePeriod + submitPeriod); poll.events[REVEAL_END] = uint64(now + stakePeriod + submitPeriod + revealPeriod); + + emit PollEventSet(_pollId, STAKE1_END); } // Claim tokens once both sides are fully staked @@ -272,11 +306,20 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { poll.events[SUBMIT_END] = uint64(now + submitPeriod); poll.events[REVEAL_END] = uint64(now + submitPeriod + revealPeriod); tokenLocking.claim(token, true); + + emit PollEventSet(_pollId, STAKE2_END); } emit PollStaked(_pollId, msg.sender, _vote, amount); } + /// @notice Submit a vote secret for a poll + /// @param _pollId The id of the poll + /// @param _voteSecret The hashed vote secret + /// @param _key Reputation tree key for the staker/domain + /// @param _value Reputation tree value for the staker/domain + /// @param _branchMask The branchmask of the proof + /// @param _siblings The siblings of the proof function submitVote( uint256 _pollId, bytes32 _voteSecret, @@ -289,20 +332,35 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { { Poll storage poll = polls[_pollId]; require(getPollState(_pollId) == PollState.Submit, "voting-rep-poll-not-open"); + require(_voteSecret != bytes32(0), "voting-rep-invalid-secret"); uint256 userRep = getReputationFromProof(_pollId, msg.sender, _key, _value, _branchMask, _siblings); + // Count reputation if first submission + if (voteSecrets[_pollId][msg.sender] == bytes32(0)) { + poll.repSubmitted = add(poll.repSubmitted, userRep); + } + voteSecrets[_pollId][msg.sender] = _voteSecret; - poll.repSubmitted = add(poll.repSubmitted, userRep); if (poll.repSubmitted >= wmul(poll.skillRep, maxVoteFraction)) { poll.events[SUBMIT_END] = uint64(now); poll.events[REVEAL_END] = uint64(now + revealPeriod); + + emit PollEventSet(_pollId, SUBMIT_END); } emit PollVoteSubmitted(_pollId, msg.sender); } + /// @notice Reveal a vote secret for a poll + /// @param _pollId The id of the poll + /// @param _salt The salt used to hash the vote + /// @param _vote The side being supported (0 = NAY, 1 = YAY) + /// @param _key Reputation tree key for the staker/domain + /// @param _value Reputation tree value for the staker/domain + /// @param _branchMask The branchmask of the proof + /// @param _siblings The siblings of the proof function revealVote( uint256 _pollId, bytes32 _salt, @@ -326,20 +384,30 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { poll.votes[_vote] = add(poll.votes[_vote], userRep); poll.repRevealed = add(poll.repRevealed, userRep); - if (poll.repRevealed >= wmul(poll.skillRep, maxVoteFraction)) { + if (poll.repRevealed == poll.repSubmitted) { poll.events[REVEAL_END] = uint64(now); + + emit PollEventSet(_pollId, REVEAL_END); } uint256 pctReputation = wdiv(userRep, poll.skillRep); uint256 totalStake = add(poll.stakes[YAY], poll.stakes[NAY]); uint256 voterReward = wmul(wmul(pctReputation, totalStake), voterRewardFraction); - poll.unpaidRewards = sub(poll.unpaidRewards, voterReward); + poll.paidVoterComp = add(poll.paidVoterComp, voterReward); tokenLocking.transfer(token, voterReward, msg.sender, true); emit PollVoteRevealed(_pollId, msg.sender, _vote); } + /// @notice Escalate a poll to a higher domain + /// @param _pollId The id of the poll + /// @param _newDomainId The desired domain of escalation + /// @param _childSkillIndex For the current domain, relative to the escalated domain + /// @param _key Reputation tree key for the new domain + /// @param _value Reputation tree value for the new domain + /// @param _branchMask The branchmask of the proof + /// @param _siblings The siblings of the proof function escalatePoll( uint256 _pollId, uint256 _newDomainId, @@ -368,6 +436,8 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { poll.domainId = _newDomainId; poll.skillId = newDomainSkillId; poll.skillRep = getReputationFromProof(_pollId, address(0x0), _key, _value, _branchMask, _siblings); + + emit PollEscalated(_pollId, msg.sender, _newDomainId); } function executePoll(uint256 _pollId) public { @@ -391,18 +461,10 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { executeCall(_pollId, claimDelayAction); } - // Conditions: - // - Yay side staked and nay side did not, and doman has sufficient vote power - // - Both sides staked and yay side won, with sufficient vote power - - uint256 votePower; - if (poll.stakes[NAY] < getRequiredStake(_pollId)) { - votePower = wmul(poll.skillRep, votePowerFraction); - } else { - votePower = poll.votes[YAY]; - } - + uint256 requiredStake = getRequiredStake(_pollId); + uint256 votePower = (poll.stakes[NAY] < requiredStake) ? poll.stakes[YAY] : poll.votes[YAY]; bytes32 slotHash = hashExpenditureSlot(poll.action); + if (expenditurePastPolls[slotHash] < votePower) { expenditurePastPolls[slotHash] = votePower; canExecute = canExecute && true; @@ -419,10 +481,16 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { emit PollExecuted(_pollId, poll.action, success); } + /// @notice Claim the staker's reward + /// @param _pollId The id of the poll + /// @param _permissionDomainId The domain where the extension has the arbitration permission + /// @param _childSkillIndex For the domain in which the poll is occurring + /// @param _user The user whose reward is being claimed + /// @param _vote The side being supported (0 = NAY, 1 = YAY) function claimReward( uint256 _pollId, - uint256 _permissionDomainId, // For extension's arbitration permission - uint256 _childSkillIndex, // For extension's arbitration permission + uint256 _permissionDomainId, + uint256 _childSkillIndex, address _user, uint256 _vote ) @@ -435,11 +503,8 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { "voting-rep-not-failed-or-executed" ); - // Calculate how much of the stake is left after voter compensation (>= 90%) uint256 stake = stakes[_pollId][_user][_vote]; - uint256 totalStake = add(poll.stakes[NAY], poll.stakes[YAY]); - uint256 rewardFraction = wdiv(poll.unpaidRewards, totalStake); - uint256 rewardStake = wmul(stake, rewardFraction); + uint256 requiredStake = getRequiredStake(_pollId); delete stakes[_pollId][_user][_vote]; @@ -447,31 +512,52 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { uint256 repPenalty; // Went to a vote, use vote to determine reward or penalty - if ( - poll.stakes[NAY] == getRequiredStake(_pollId) && - poll.stakes[YAY] == getRequiredStake(_pollId) - ) { - uint256 stakerVotes = poll.votes[_vote]; + if (poll.stakes[NAY] == requiredStake && poll.stakes[YAY] == requiredStake) { + + uint256 loserStake = sub( + (poll.votes[NAY] < poll.votes[YAY]) ? poll.stakes[NAY] : poll.stakes[YAY], + poll.paidVoterComp + ); + + uint256 stakerSideVotes = poll.votes[_vote]; uint256 totalVotes = add(poll.votes[NAY], poll.votes[YAY]); - uint256 winPercent = wdiv(stakerVotes, totalVotes); - uint256 winShare = wmul(winPercent, 2 * WAD); - stakerReward = wmul(rewardStake, winShare); - repPenalty = (winShare < WAD) ? sub(stake, wmul(winShare, stake)) : 0; + uint256 winFraction = wdiv(stakerSideVotes, totalVotes); + uint256 winShare = wmul(winFraction, 2 * WAD); // On a scale of 0 - 2 WAD + + uint256 stakeFraction = wdiv(stake, poll.stakes[_vote]); + + if (winShare > WAD || (winShare == WAD && _vote == NAY)) { + stakerReward = add(stake, wmul(stakeFraction, wmul(loserStake, winShare - WAD))); + } else { + stakerReward = wmul(stakeFraction, wmul(loserStake, winShare)); + repPenalty = sub(stake, stakerReward); + } // Your side fully staked, receive 10% (proportional) of loser's stake - } else if (poll.stakes[_vote] == getRequiredStake(_pollId)) { - uint256 stakePercent = wdiv(stake, poll.stakes[_vote]); - uint256 totalPenalty = wmul(poll.stakes[flip(_vote)], WAD / 10); - stakerReward = add(rewardStake, wmul(stakePercent, totalPenalty)); + } else if (poll.stakes[_vote] == requiredStake) { + + uint256 loserStake = sub(poll.stakes[flip(_vote)], poll.paidVoterComp); + uint256 stakeFraction = wdiv(stake, poll.stakes[_vote]); + uint256 totalPenalty = wmul(loserStake, WAD / 10); + + stakerReward = add(stake, wmul(stakeFraction, totalPenalty)); // Opponent's side fully staked, pay 10% penalty - } else if (poll.stakes[flip(_vote)] == getRequiredStake(_pollId)) { - stakerReward = wmul(rewardStake, (WAD / 10) * 9); - repPenalty = stake / 10; + } else if (poll.stakes[flip(_vote)] == requiredStake) { + + uint256 loserStake = sub(poll.stakes[_vote], poll.paidVoterComp); + uint256 stakeFraction = wdiv(stake, poll.stakes[_vote]); + uint256 totalPenalty = wmul(loserStake, WAD / 10); + + stakerReward = sub(stake, wmul(stakeFraction, totalPenalty)); + repPenalty = sub(stake, stakerReward); // Neither side fully staked, no reward or penalty } else { - stakerReward = rewardStake; + + uint256 totalStake = add(poll.stakes[NAY], poll.stakes[YAY]); + uint256 rewardShare = wdiv(sub(totalStake, poll.paidVoterComp), totalStake); + stakerReward = wmul(stake, rewardShare); } tokenLocking.transfer(token, stakerReward, _user, true); @@ -491,26 +577,44 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { // Public view functions + /// @notice Get the total poll count + /// @return The total poll count function getPollCount() public view returns (uint256) { return pollCount; } + /// @notice Get the data for a single poll + /// @param _pollId The id of the poll + /// @return poll The poll struct function getPoll(uint256 _pollId) public view returns (Poll memory poll) { poll = polls[_pollId]; } + /// @notice Get a user's stake on a poll + /// @param _pollId The id of the poll + /// @param _staker The staker address + /// @param _vote The side being supported (0 = NAY, 1 = YAY) + /// @return The user's stake function getStake(uint256 _pollId, address _staker, uint256 _vote) public view returns (uint256) { return stakes[_pollId][_staker][_vote]; } + /// @notice Get the number of ongoing polls for a single expenditure / slot + /// @param _structHash The hash of the expenditureId or expenditureId*expenditureSlot + /// @return The number of ongoing polls function getExpenditurePollCount(bytes32 _structHash) public view returns (uint256) { return expenditurePollCounts[_structHash]; } + /// @notice Get the largest past vote on a single expenditure variable + /// @param _slotHash The hash of the particular expenditure slot + /// @return The largest past vote on this variable function getExpenditurePastPoll(bytes32 _slotHash) public view returns (uint256) { return expenditurePastPolls[_slotHash]; } + /// @notice Get the current state of the poll + /// @return The current poll state function getPollState(uint256 _pollId) public view returns (PollState) { Poll storage poll = polls[_pollId]; uint256 requiredStake = getRequiredStake(_pollId); @@ -599,7 +703,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { } function getRequiredStake(uint256 _pollId) internal view returns (uint256) { - return wmul(polls[_pollId].skillRep, stakeFraction); + return wmul(polls[_pollId].skillRep, totalStakeFraction); } function flip(uint256 _vote) internal pure returns (uint256) { diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index 3e0b2e7286..997f2bbfb5 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -65,12 +65,11 @@ contract("Voting Reputation", (accounts) => { let user1Mask; let user1Siblings; - const STAKE_FRACTION = WAD.divn(1000); // 0.1 % - const MIN_STAKE_FRACTION = WAD.divn(10); // 10 % + const TOTAL_STAKE_FRACTION = WAD.divn(1000); // 0.1 % + const USER_MIN_STAKE_FRACTION = WAD.divn(10); // 10 % const MAX_VOTE_FRACTION = WAD.divn(10).muln(8); // 80 % const VOTER_REWARD_FRACTION = WAD.divn(10); // 10 % - const VOTE_POWER_FRACTION = WAD.muln(2).divn(3); // 66 % const STAKE_PERIOD = SECONDS_PER_DAY * 3; const SUBMIT_PERIOD = SECONDS_PER_DAY * 2; @@ -129,11 +128,10 @@ contract("Voting Reputation", (accounts) => { voting = await VotingReputation.at(votingAddress); await voting.initialise( - STAKE_FRACTION, - MIN_STAKE_FRACTION, + TOTAL_STAKE_FRACTION, + USER_MIN_STAKE_FRACTION, MAX_VOTE_FRACTION, VOTER_REWARD_FRACTION, - VOTE_POWER_FRACTION, STAKE_PERIOD, SUBMIT_PERIOD, REVEAL_PERIOD, @@ -244,12 +242,14 @@ contract("Voting Reputation", (accounts) => { it("cannot initialise twice or if not root", async () => { await checkErrorRevert( - voting.initialise(WAD, WAD, WAD, WAD, WAD, STAKE_PERIOD, SUBMIT_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD), + voting.initialise(WAD, WAD, WAD, WAD.divn(2), STAKE_PERIOD, SUBMIT_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD), "voting-rep-already-initialised" ); await checkErrorRevert( - voting.initialise(WAD, WAD, WAD, WAD, WAD, STAKE_PERIOD, SUBMIT_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD, { from: USER2 }), + voting.initialise(WAD, WAD, WAD, WAD.divn(2), STAKE_PERIOD, SUBMIT_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD, { + from: USER2, + }), "voting-rep-user-not-root" ); }); @@ -260,52 +260,49 @@ contract("Voting Reputation", (accounts) => { const votingAddress = await votingFactory.deployedExtensions(colony.address); voting = await VotingReputation.at(votingAddress); - await checkErrorRevert( - voting.initialise(WAD.addn(1), WAD, WAD, WAD, WAD, STAKE_PERIOD, SUBMIT_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD), - "voting-rep-must-be-wad" - ); + const half = WAD.divn(2); await checkErrorRevert( - voting.initialise(WAD, WAD.addn(1), WAD, WAD, WAD, STAKE_PERIOD, SUBMIT_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD), - "voting-rep-must-be-wad" + voting.initialise(WAD.addn(1), WAD, WAD, half, STAKE_PERIOD, SUBMIT_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD), + "voting-rep-greater-than-wad" ); await checkErrorRevert( - voting.initialise(WAD, WAD, WAD.addn(1), WAD, WAD, STAKE_PERIOD, SUBMIT_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD), - "voting-rep-must-be-wad" + voting.initialise(WAD, WAD.addn(1), WAD, half, STAKE_PERIOD, SUBMIT_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD), + "voting-rep-greater-than-wad" ); await checkErrorRevert( - voting.initialise(WAD, WAD, WAD, WAD.addn(1), WAD, STAKE_PERIOD, SUBMIT_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD), - "voting-rep-must-be-wad" + voting.initialise(WAD, WAD, WAD.addn(1), half, STAKE_PERIOD, SUBMIT_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD), + "voting-rep-greater-than-wad" ); await checkErrorRevert( - voting.initialise(WAD, WAD, WAD, WAD, WAD.addn(1), STAKE_PERIOD, SUBMIT_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD), - "voting-rep-must-be-wad" + voting.initialise(WAD, WAD, WAD, half.addn(1), STAKE_PERIOD, SUBMIT_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD), + "voting-rep-greater-than-half-wad" ); await checkErrorRevert( - voting.initialise(WAD, WAD, WAD, WAD, WAD, SECONDS_PER_DAY * 366, SUBMIT_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD), + voting.initialise(WAD, WAD, WAD, half, SECONDS_PER_DAY * 366, SUBMIT_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD), "voting-rep-period-too-long" ); await checkErrorRevert( - voting.initialise(WAD, WAD, WAD, WAD, WAD, STAKE_PERIOD, SECONDS_PER_DAY * 366, REVEAL_PERIOD, ESCALATION_PERIOD), + voting.initialise(WAD, WAD, WAD, half, STAKE_PERIOD, SECONDS_PER_DAY * 366, REVEAL_PERIOD, ESCALATION_PERIOD), "voting-rep-period-too-long" ); await checkErrorRevert( - voting.initialise(WAD, WAD, WAD, WAD, WAD, STAKE_PERIOD, SUBMIT_PERIOD, SECONDS_PER_DAY * 366, ESCALATION_PERIOD), + voting.initialise(WAD, WAD, WAD, half, STAKE_PERIOD, SUBMIT_PERIOD, SECONDS_PER_DAY * 366, ESCALATION_PERIOD), "voting-rep-period-too-long" ); await checkErrorRevert( - voting.initialise(WAD, WAD, WAD, WAD, WAD, STAKE_PERIOD, SUBMIT_PERIOD, REVEAL_PERIOD, SECONDS_PER_DAY * 366), + voting.initialise(WAD, WAD, WAD, half, STAKE_PERIOD, SUBMIT_PERIOD, REVEAL_PERIOD, SECONDS_PER_DAY * 366), "voting-rep-period-too-long" ); - await voting.initialise(WAD, WAD, WAD, WAD, WAD, STAKE_PERIOD, SUBMIT_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD); + await voting.initialise(WAD, WAD, WAD, half, STAKE_PERIOD, SUBMIT_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD); }); }); @@ -411,6 +408,19 @@ contract("Voting Reputation", (accounts) => { expect(pollState).to.eq.BN(SUBMIT); }); + it("can stake even with a locked token", async () => { + await token.mint(colony.address, WAD); + await colony.setRewardInverse(100); + await colony.claimColonyFunds(token.address); + await colony.startNextRewardPayout(token.address, domain1Key, domain1Value, domain1Mask, domain1Siblings); + + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + + const lock = await tokenLocking.getUserLock(token.address, voting.address); + expect(lock.balance).to.eq.BN(REQUIRED_STAKE.muln(2)); + }); + it("cannot stake a nonexistent side", async () => { await checkErrorRevert( voting.stakePoll(pollId, 1, UINT256_MAX, 2, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), @@ -665,15 +675,29 @@ contract("Voting Reputation", (accounts) => { await voting.revealVote(pollId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); }); - it("cannot reveal a vote twice, and so cannot vote twice", async () => { + it("can update votes, but the total reputation does not change", async () => { + let poll = await voting.getPoll(pollId); + expect(poll.repSubmitted).to.be.zero; + await voting.submitVote(pollId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await forwardTime(SUBMIT_PERIOD, this); + poll = await voting.getPoll(pollId); + expect(poll.repSubmitted).to.eq.BN(WAD); - await voting.revealVote(pollId, SALT, NAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + + poll = await voting.getPoll(pollId); + expect(poll.repSubmitted).to.eq.BN(WAD); + }); + + it("cannot reveal a vote twice, and so cannot vote twice", async () => { + await voting.submitVote(pollId, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, NAY), user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + + await voting.revealVote(pollId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await checkErrorRevert( - voting.revealVote(pollId, SALT, NAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), + voting.revealVote(pollId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), "voting-rep-secret-no-match" ); }); @@ -715,6 +739,13 @@ contract("Voting Reputation", (accounts) => { await voting.revealVote(pollId2, SALT, NAY, user0Key, user0Value2, user0Mask2, user0Siblings2, { from: USER0 }); }); + it("cannot submit a null vote", async () => { + await checkErrorRevert( + voting.submitVote(pollId, "0x0", user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), + "voting-rep-invalid-secret" + ); + }); + it("cannot submit a vote if voting is closed", async () => { await forwardTime(SUBMIT_PERIOD, this); @@ -893,9 +924,7 @@ contract("Voting Reputation", (accounts) => { await voting.revealVote(pollId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await forwardTime(REVEAL_PERIOD, this); - - await checkErrorRevert(voting.executePoll(pollId), "voting-rep-poll-not-executable"); + // Don't need to wait for the reveal period, since 100% of the secret is revealed await forwardTime(STAKE_PERIOD, this); @@ -978,7 +1007,7 @@ contract("Voting Reputation", (accounts) => { await voting.executePoll(pollId); const slotHash = soliditySha3(`0x${action.slice(2, action.length - 64)}`); const pastPoll = await voting.getExpenditurePastPoll(slotHash); - expect(pastPoll).to.eq.BN(WAD.muln(2).subn(2)); // ~66.6% of 3 WAD + expect(pastPoll).to.eq.BN(REQUIRED_STAKE); }); it("can set vote power correctly after a vote", async () => { @@ -1068,7 +1097,7 @@ contract("Voting Reputation", (accounts) => { await voting.revealVote(pollId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await voting.revealVote(pollId, SALT, NAY, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await forwardTime(STAKE_PERIOD, this); + await forwardTime(ESCALATION_PERIOD, this); await voting.executePoll(pollId); @@ -1081,9 +1110,9 @@ contract("Voting Reputation", (accounts) => { const user0LockPost = await tokenLocking.getUserLock(token.address, USER0); const user1LockPost = await tokenLocking.getUserLock(token.address, USER1); - const stakerRewards = REQUIRED_STAKE.divn(10).muln(9); - const expectedReward0 = stakerRewards.divn(3).muln(2); // (stake * .9) * (winPct = 1/3 * 2) - const expectedReward1 = stakerRewards.divn(3).muln(4); // (stake * .9) * (winPct = 2/3 * 2) + const loserStake = REQUIRED_STAKE.divn(10).muln(8); // Take out voter comp + const expectedReward0 = loserStake.divn(3).muln(2); // (stake * .8) * (winPct = 1/3 * 2) + const expectedReward1 = REQUIRED_STAKE.add(loserStake.divn(3)); // stake + ((stake * .8) * (1 - (winPct = 2/3 * 2)) expect(new BN(user0LockPost.balance).sub(new BN(user0LockPre.balance))).to.eq.BN(expectedReward0); expect(new BN(user1LockPost.balance).sub(new BN(user1LockPre.balance))).to.eq.BN(expectedReward1); @@ -1094,7 +1123,93 @@ contract("Voting Reputation", (accounts) => { const repUpdate = await repCycle.getReputationUpdateLogEntry(numEntriesPost.subn(1)); expect(repUpdate.user).to.equal(USER0); - expect(repUpdate.amount).to.eq.BN(REQUIRED_STAKE.divn(3).neg()); + expect(repUpdate.amount).to.eq.BN(REQUIRED_STAKE.sub(expectedReward0).neg()); + }); + + it("can let stakers claim rewards, based on the vote outcome, with multiple losing stakers", async () => { + const user2Key = makeReputationKey(colony.address, domain1.skillId, USER2); + const user2Value = makeReputationValue(REQUIRED_STAKE.subn(1), 8); + const [user2Mask, user2Siblings] = await reputationTree.getProof(user2Key); + + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE.divn(3).muln(2), user1Key, user1Value, user1Mask, user1Siblings, { + from: USER1, + }); + await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE.divn(3), user2Key, user2Value, user2Mask, user2Siblings, { from: USER2 }); + + await voting.submitVote(pollId, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, NAY), user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + + await voting.revealVote(pollId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.revealVote(pollId, SALT, NAY, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + + await forwardTime(ESCALATION_PERIOD, this); + + await voting.executePoll(pollId); + + const user0LockPre = await tokenLocking.getUserLock(token.address, USER0); + const user1LockPre = await tokenLocking.getUserLock(token.address, USER1); + const user2LockPre = await tokenLocking.getUserLock(token.address, USER2); + + await voting.claimReward(pollId, 1, UINT256_MAX, USER0, YAY); + await voting.claimReward(pollId, 1, UINT256_MAX, USER1, NAY); + await voting.claimReward(pollId, 1, UINT256_MAX, USER2, NAY); + + const user0LockPost = await tokenLocking.getUserLock(token.address, USER0); + const user1LockPost = await tokenLocking.getUserLock(token.address, USER1); + const user2LockPost = await tokenLocking.getUserLock(token.address, USER2); + + const loserStake = REQUIRED_STAKE.divn(10).muln(8); // Take out voter comp + const expectedReward0 = loserStake.divn(3).muln(2); // (stake * .8) * (winPct = 1/3 * 2) + const expectedReward1 = REQUIRED_STAKE.add(loserStake.divn(3)).divn(3).muln(2); // stake + ((stake * .8) * (1 - (winPct = 2/3 * 2)) + const expectedReward2 = REQUIRED_STAKE.add(loserStake.divn(3)).divn(3); // stake + ((stake * .8) * (1 - (winPct = 2/3 * 2)) + + expect(new BN(user0LockPost.balance).sub(new BN(user0LockPre.balance))).to.eq.BN(expectedReward0); + expect(new BN(user1LockPost.balance).sub(new BN(user1LockPre.balance))).to.eq.BN(expectedReward1.addn(1)); // Rounding + expect(new BN(user2LockPost.balance).sub(new BN(user2LockPre.balance))).to.eq.BN(expectedReward2.addn(1)); // Rounding + }); + + it("can let stakers claim rewards, based on the vote outcome, with multiple winning stakers", async () => { + const user2Key = makeReputationKey(colony.address, domain1.skillId, USER2); + const user2Value = makeReputationValue(REQUIRED_STAKE.subn(1), 8); + const [user2Mask, user2Siblings] = await reputationTree.getProof(user2Key); + + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE.divn(3).muln(2), user0Key, user0Value, user0Mask, user0Siblings, { + from: USER0, + }); + await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE.divn(3), user2Key, user2Value, user2Mask, user2Siblings, { from: USER2 }); + + await voting.submitVote(pollId, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, NAY), user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + + await voting.revealVote(pollId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.revealVote(pollId, SALT, NAY, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + + await forwardTime(ESCALATION_PERIOD, this); + + await voting.executePoll(pollId); + + const user0LockPre = await tokenLocking.getUserLock(token.address, USER0); + const user1LockPre = await tokenLocking.getUserLock(token.address, USER1); + const user2LockPre = await tokenLocking.getUserLock(token.address, USER2); + + await voting.claimReward(pollId, 1, UINT256_MAX, USER0, YAY); + await voting.claimReward(pollId, 1, UINT256_MAX, USER1, NAY); + await voting.claimReward(pollId, 1, UINT256_MAX, USER2, YAY); + + const user0LockPost = await tokenLocking.getUserLock(token.address, USER0); + const user1LockPost = await tokenLocking.getUserLock(token.address, USER1); + const user2LockPost = await tokenLocking.getUserLock(token.address, USER2); + + const loserStake = REQUIRED_STAKE.divn(10).muln(8); // Take out voter comp + const expectedReward0 = loserStake.divn(3).muln(2).divn(3).muln(2); // (stake * .8) * (winPct = 1/3 * 2) + const expectedReward1 = REQUIRED_STAKE.add(loserStake.divn(3)); // stake + ((stake * .8) * (1 - (winPct = 2/3 * 2)) + const expectedReward2 = loserStake.divn(3).muln(2).divn(3); // (stake * .8) * (winPct = 1/3 * 2) + + expect(new BN(user0LockPost.balance).sub(new BN(user0LockPre.balance))).to.eq.BN(expectedReward0.addn(1)); // Rounding + expect(new BN(user1LockPost.balance).sub(new BN(user1LockPre.balance))).to.eq.BN(expectedReward1); + expect(new BN(user2LockPost.balance).sub(new BN(user2LockPre.balance))).to.eq.BN(expectedReward2); }); it("can let stakers claim their original stake if neither side fully staked", async () => { @@ -1134,7 +1249,7 @@ contract("Voting Reputation", (accounts) => { await voting.revealVote(pollId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await forwardTime(REVEAL_PERIOD, this); - await forwardTime(STAKE_PERIOD, this); + await forwardTime(ESCALATION_PERIOD, this); await voting.executePoll(pollId); @@ -1193,7 +1308,7 @@ contract("Voting Reputation", (accounts) => { }); it("cannot internally escalate a domain poll if not in a 'closed' state", async () => { - await forwardTime(STAKE_PERIOD, this); + await forwardTime(ESCALATION_PERIOD, this); await voting.executePoll(pollId); From 9a0f739c2d2b5cdcf94ad118019c5dd1b274db9e Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Wed, 29 Jul 2020 21:25:45 -0700 Subject: [PATCH 32/61] Rename executed to finalized --- contracts/extensions/VotingReputation.sol | 35 +++++----- test/extensions/voting-rep.js | 80 +++++++++++------------ 2 files changed, 58 insertions(+), 57 deletions(-) diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index 47ed2aa124..b73970385e 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -36,7 +36,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { event PollStaked(uint256 indexed pollId, address indexed staker, uint256 indexed vote, uint256 amount); event PollVoteSubmitted(uint256 indexed pollId, address indexed voter); event PollVoteRevealed(uint256 indexed pollId, address indexed voter, uint256 indexed vote); - event PollExecuted(uint256 indexed pollId, bytes action, bool success); + event PollFinalized(uint256 indexed pollId, bytes action, bool executed); event PollEscalated(uint256 indexed pollId, address escalator, uint256 indexed domainId); event PollRewardClaimed(uint256 indexed pollId, address indexed staker, uint256 indexed vote, uint256 amount); event PollEventSet(uint256 indexed pollId, uint256 eventIndex); @@ -150,7 +150,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { } // Data structures - enum PollState { Staking, Submit, Reveal, Closed, Executable, Executed, Failed } + enum PollState { Staking, Submit, Reveal, Closed, Finalizable, Finalized, Failed } struct Poll { uint64[4] events; // For recording poll lifecycle timestamps (STAKE1, STAKE2, SUBMIT, REVEAL) @@ -163,7 +163,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { uint256 paidVoterComp; uint256[2] stakes; // [nay, yay] uint256[2] votes; // [nay, yay] - bool executed; + bool finalized; address target; bytes action; } @@ -440,11 +440,11 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { emit PollEscalated(_pollId, msg.sender, _newDomainId); } - function executePoll(uint256 _pollId) public { + function finalizePoll(uint256 _pollId) public { Poll storage poll = polls[_pollId]; - require(getPollState(_pollId) == PollState.Executable, "voting-rep-poll-not-executable"); + require(getPollState(_pollId) == PollState.Finalizable, "voting-rep-poll-not-executable"); - poll.executed = true; + poll.finalized = true; bool canExecute = ( poll.stakes[NAY] <= poll.stakes[YAY] && @@ -473,12 +473,13 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { } } - bool success; + bool executed; + if (canExecute) { - success = executeCall(_pollId, poll.action); + executed = executeCall(_pollId, poll.action); } - emit PollExecuted(_pollId, poll.action, success); + emit PollFinalized(_pollId, poll.action, executed); } /// @notice Claim the staker's reward @@ -498,9 +499,9 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { { Poll storage poll = polls[_pollId]; require( - getPollState(_pollId) == PollState.Executed || + getPollState(_pollId) == PollState.Finalized || getPollState(_pollId) == PollState.Failed, - "voting-rep-not-failed-or-executed" + "voting-rep-not-failed-or-finalized" ); uint256 stake = stakes[_pollId][_user][_vote]; @@ -619,10 +620,10 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { Poll storage poll = polls[_pollId]; uint256 requiredStake = getRequiredStake(_pollId); - // If executed, we're done - if (poll.executed) { + // If finalized, we're done + if (poll.finalized) { - return PollState.Executed; + return PollState.Finalized; // Not fully staked } else if ( @@ -635,10 +636,10 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { return PollState.Staking; // If not, did the YAY side stake? } else if (poll.stakes[YAY] == requiredStake) { - return PollState.Executable; + return PollState.Finalizable; // If not, was there a prior vote we can fall back on? } else if (poll.votes[NAY] > 0 || poll.votes[YAY] > 0) { - return PollState.Executable; + return PollState.Finalizable; // Otherwise, the poll failed } else { return PollState.Failed; @@ -659,7 +660,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { } else if (now < poll.events[REVEAL_END] + escalationPeriod) { return PollState.Closed; } else { - return PollState.Executable; + return PollState.Finalizable; } } diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index 997f2bbfb5..0f35655679 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -576,7 +576,7 @@ contract("Voting Reputation", (accounts) => { expect(expenditureSlot.claimDelay).to.eq.BN(UINT256_MAX); await forwardTime(STAKE_PERIOD, this); - await voting.executePoll(pollId1); + await voting.finalizePoll(pollId1); expenditurePollCount = await voting.getExpenditurePollCount(expenditureHash); expect(expenditurePollCount).to.eq.BN(1); @@ -584,7 +584,7 @@ contract("Voting Reputation", (accounts) => { expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); expect(expenditureSlot.claimDelay).to.eq.BN(UINT256_MAX); - await voting.executePoll(pollId2); + await voting.finalizePoll(pollId2); expenditurePollCount = await voting.getExpenditurePollCount(expenditureHash); expect(expenditurePollCount).to.be.zero; @@ -825,7 +825,7 @@ contract("Voting Reputation", (accounts) => { await forwardTime(STAKE_PERIOD, this); - await checkErrorRevert(voting.executePoll(pollId), "voting-rep-poll-not-executable"); + await checkErrorRevert(voting.finalizePoll(pollId), "voting-rep-poll-not-executable"); }); it("can take an action if there is insufficient opposition", async () => { @@ -836,8 +836,8 @@ contract("Voting Reputation", (accounts) => { await forwardTime(STAKE_PERIOD, this); - const { logs } = await voting.executePoll(pollId); - expect(logs[0].args.success).to.be.true; + const { logs } = await voting.finalizePoll(pollId); + expect(logs[0].args.executed).to.be.true; }); it("can take an action with a return value", async () => { @@ -850,8 +850,8 @@ contract("Voting Reputation", (accounts) => { await forwardTime(STAKE_PERIOD, this); - const { logs } = await voting.executePoll(pollId); - expect(logs[0].args.success).to.be.true; + const { logs } = await voting.finalizePoll(pollId); + expect(logs[0].args.executed).to.be.true; }); it("can take an action with an arbitrary target", async () => { @@ -869,7 +869,7 @@ contract("Voting Reputation", (accounts) => { const balanceBefore = await otherColony.getFundingPotBalance(1, token.address); expect(balanceBefore).to.be.zero; - await voting.executePoll(pollId); + await voting.finalizePoll(pollId); const balanceAfter = await otherColony.getFundingPotBalance(1, token.address); expect(balanceAfter).to.eq.BN(WAD); @@ -884,8 +884,8 @@ contract("Voting Reputation", (accounts) => { await forwardTime(STAKE_PERIOD, this); - const { logs } = await voting.executePoll(pollId); - expect(logs[0].args.success).to.be.false; + const { logs } = await voting.finalizePoll(pollId); + expect(logs[0].args.executed).to.be.false; }); it("cannot take an action during staking or voting", async () => { @@ -894,13 +894,13 @@ contract("Voting Reputation", (accounts) => { pollState = await voting.getPollState(pollId); expect(pollState).to.eq.BN(STAKING); - await checkErrorRevert(voting.executePoll(pollId), "voting-rep-poll-not-executable"); + await checkErrorRevert(voting.finalizePoll(pollId), "voting-rep-poll-not-executable"); await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); pollState = await voting.getPollState(pollId); expect(pollState).to.eq.BN(SUBMIT); - await checkErrorRevert(voting.executePoll(pollId), "voting-rep-poll-not-executable"); + await checkErrorRevert(voting.finalizePoll(pollId), "voting-rep-poll-not-executable"); }); it("cannot take an action twice", async () => { @@ -908,10 +908,10 @@ contract("Voting Reputation", (accounts) => { await forwardTime(STAKE_PERIOD, this); - const { logs } = await voting.executePoll(pollId); - expect(logs[0].args.success).to.be.true; + const { logs } = await voting.finalizePoll(pollId); + expect(logs[0].args.executed).to.be.true; - await checkErrorRevert(voting.executePoll(pollId), "voting-rep-poll-not-executable"); + await checkErrorRevert(voting.finalizePoll(pollId), "voting-rep-poll-not-executable"); }); it("can take an action if the poll passes", async () => { @@ -928,8 +928,8 @@ contract("Voting Reputation", (accounts) => { await forwardTime(STAKE_PERIOD, this); - const { logs } = await voting.executePoll(pollId); - expect(logs[0].args.success).to.be.true; + const { logs } = await voting.finalizePoll(pollId); + expect(logs[0].args.executed).to.be.true; }); it("cannot take an action if the poll fails", async () => { @@ -945,8 +945,8 @@ contract("Voting Reputation", (accounts) => { await forwardTime(REVEAL_PERIOD, this); await forwardTime(STAKE_PERIOD, this); - const { logs } = await voting.executePoll(pollId); - expect(logs[0].args.success).to.be.false; + const { logs } = await voting.finalizePoll(pollId); + expect(logs[0].args.executed).to.be.false; }); it("cannot take an action if there is insufficient voting power (state change actions)", async () => { @@ -971,8 +971,8 @@ contract("Voting Reputation", (accounts) => { await forwardTime(STAKE_PERIOD, this); let logs; - ({ logs } = await voting.executePoll(pollId1)); - expect(logs[0].args.success).to.be.true; + ({ logs } = await voting.finalizePoll(pollId1)); + expect(logs[0].args.executed).to.be.true; // Create another poll for the same variable await voting.createDomainPoll(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); @@ -990,8 +990,8 @@ contract("Voting Reputation", (accounts) => { await forwardTime(REVEAL_PERIOD, this); await forwardTime(STAKE_PERIOD, this); - ({ logs } = await voting.executePoll(pollId2)); - expect(logs[0].args.success).to.be.false; + ({ logs } = await voting.finalizePoll(pollId2)); + expect(logs[0].args.executed).to.be.false; }); it("can set vote power correctly if there is insufficient opposition", async () => { @@ -1004,7 +1004,7 @@ contract("Voting Reputation", (accounts) => { await forwardTime(STAKE_PERIOD, this); - await voting.executePoll(pollId); + await voting.finalizePoll(pollId); const slotHash = soliditySha3(`0x${action.slice(2, action.length - 64)}`); const pastPoll = await voting.getExpenditurePastPoll(slotHash); expect(pastPoll).to.eq.BN(REQUIRED_STAKE); @@ -1028,7 +1028,7 @@ contract("Voting Reputation", (accounts) => { await forwardTime(REVEAL_PERIOD, this); await forwardTime(ESCALATION_PERIOD, this); - await voting.executePoll(pollId); + await voting.finalizePoll(pollId); const slotHash = soliditySha3(`0x${action.slice(2, action.length - 64)}`); const pastPoll = await voting.getExpenditurePastPoll(slotHash); expect(pastPoll).to.eq.BN(WAD); // USER0 had 1 WAD of reputation @@ -1056,7 +1056,7 @@ contract("Voting Reputation", (accounts) => { await forwardTime(STAKE_PERIOD, this); - await voting.executePoll(pollId); + await voting.finalizePoll(pollId); const user0LockPre = await tokenLocking.getUserLock(token.address, USER0); const user1LockPre = await tokenLocking.getUserLock(token.address, USER1); @@ -1099,7 +1099,7 @@ contract("Voting Reputation", (accounts) => { await forwardTime(ESCALATION_PERIOD, this); - await voting.executePoll(pollId); + await voting.finalizePoll(pollId); const user0LockPre = await tokenLocking.getUserLock(token.address, USER0); const user1LockPre = await tokenLocking.getUserLock(token.address, USER1); @@ -1145,7 +1145,7 @@ contract("Voting Reputation", (accounts) => { await forwardTime(ESCALATION_PERIOD, this); - await voting.executePoll(pollId); + await voting.finalizePoll(pollId); const user0LockPre = await tokenLocking.getUserLock(token.address, USER0); const user1LockPre = await tokenLocking.getUserLock(token.address, USER1); @@ -1188,7 +1188,7 @@ contract("Voting Reputation", (accounts) => { await forwardTime(ESCALATION_PERIOD, this); - await voting.executePoll(pollId); + await voting.finalizePoll(pollId); const user0LockPre = await tokenLocking.getUserLock(token.address, USER0); const user1LockPre = await tokenLocking.getUserLock(token.address, USER1); @@ -1251,7 +1251,7 @@ contract("Voting Reputation", (accounts) => { await forwardTime(REVEAL_PERIOD, this); await forwardTime(ESCALATION_PERIOD, this); - await voting.executePoll(pollId); + await voting.finalizePoll(pollId); await voting.claimReward(pollId, 1, UINT256_MAX, USER0, YAY); const userLock0 = await tokenLocking.getUserLock(token.address, USER0); @@ -1261,7 +1261,7 @@ contract("Voting Reputation", (accounts) => { }); it("cannot claim rewards before a poll is executed", async () => { - await checkErrorRevert(voting.claimReward(pollId, 1, UINT256_MAX, USER0, YAY), "voting-rep-not-failed-or-executed"); + await checkErrorRevert(voting.claimReward(pollId, 1, UINT256_MAX, USER0, YAY), "voting-rep-not-failed-or-finalized"); }); }); @@ -1310,7 +1310,7 @@ contract("Voting Reputation", (accounts) => { it("cannot internally escalate a domain poll if not in a 'closed' state", async () => { await forwardTime(ESCALATION_PERIOD, this); - await voting.executePoll(pollId); + await voting.finalizePoll(pollId); await checkErrorRevert( voting.escalatePoll(pollId, 1, 0, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER2 }), @@ -1357,8 +1357,8 @@ contract("Voting Reputation", (accounts) => { await forwardTime(STAKE_PERIOD, this); - const { logs } = await voting.executePoll(pollId); - expect(logs[0].args.success).to.be.true; + const { logs } = await voting.finalizePoll(pollId); + expect(logs[0].args.executed).to.be.true; }); it("cannot execute after internally escalating a domain poll, if there is insufficient support", async () => { @@ -1373,8 +1373,8 @@ contract("Voting Reputation", (accounts) => { await forwardTime(STAKE_PERIOD, this); - const { logs } = await voting.executePoll(pollId); - expect(logs[0].args.success).to.be.false; + const { logs } = await voting.finalizePoll(pollId); + expect(logs[0].args.executed).to.be.false; }); it("can fall back on the previous vote if both sides fail to stake", async () => { @@ -1383,8 +1383,8 @@ contract("Voting Reputation", (accounts) => { await forwardTime(STAKE_PERIOD, this); // Note that the previous vote succeeded - const { logs } = await voting.executePoll(pollId); - expect(logs[0].args.success).to.be.true; + const { logs } = await voting.finalizePoll(pollId); + expect(logs[0].args.executed).to.be.true; }); it("can use the result of a new vote after internally escalating a domain poll", async () => { @@ -1411,8 +1411,8 @@ contract("Voting Reputation", (accounts) => { await forwardTime(STAKE_PERIOD, this); - const { logs } = await voting.executePoll(pollId); - expect(logs[0].args.success).to.be.false; + const { logs } = await voting.finalizePoll(pollId); + expect(logs[0].args.executed).to.be.false; }); }); }); From 687d89c8334efc0300fd731bb349921ab7e2160c Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 30 Jul 2020 14:03:48 +0100 Subject: [PATCH 33/61] Updates to comments, and one rename of a variable --- contracts/extensions/VotingReputation.sol | 28 ++++++++++++++--------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index b73970385e..aaa3977414 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -67,13 +67,19 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { ITokenLocking tokenLocking; address token; - // All `Fraction` variables are denominated in WADs (1 WAD = 100%) + // All `Fraction` variables are stored as WADs i.e. fixed-point numbers with 18 digits after the radix. So + // 1 WAD = 10**18, which is interpreted as 1. - uint256 totalStakeFraction; // Percent of domain reputation needed for staking - uint256 userMinStakeFraction; // Minimum stake as percent of required stake (100% means single-staker) + uint256 totalStakeFraction; // Fraction of the domain's reputation needed to stake on each side in order to go to a poll. NB if set to over + // 0.5, then votes can never occur. + uint256 userMinStakeFraction; // Minimum stake as fraction of required stake. 1 means a single user will be required to + // provide the whole stake on each side, which may not be possible depending on totalStakeFraction and the distribution of + // reputation in a domain. - uint256 maxVoteFraction; // Percent of total domain rep we need before closing the vote - uint256 voterRewardFraction; // Percent of stake paid out to voters as rewards (immediately taken from the stake) + uint256 maxVoteFraction; // Fraction of total domain reputation that needs to commit votes before closing to further votes. + // Setting this to anything other than 1 will mean it is likely not all those eligible to vote will be able to do so. + uint256 voterRewardFraction; // Fraction of staked tokens paid out to voters as rewards. This will be paid from the staked + // tokens of the losing side. This can be set to a maximum of 0.5. // All `Period` variables are second-denominated @@ -90,10 +96,10 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { } /// @notice Initialise the extension - /// @param _totalStakeFraction The percent of the domain's reputation we need to stake - /// @param _userMinStakeFraction The minimum per-user stake as percent of total stake - /// @param _maxVoteFraction The percent of the domain's reputation which must submit for quick-end - /// @param _voterRewardFraction The percent of the total stake paid out to voters as rewards + /// @param _totalStakeFraction The fraction of the domain's reputation we need to stake + /// @param _userMinStakeFraction The minimum per-user stake as fraction of total stake + /// @param _maxVoteFraction The fraction of the domain's reputation which must submit for quick-end + /// @param _voterRewardFraction The fraction of the total stake paid out to voters as rewards /// @param _stakePeriod The length of the staking period in seconds /// @param _submitPeriod The length of the submit period in seconds /// @param _revealPeriod The length of the reveal period in seconds @@ -390,9 +396,9 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { emit PollEventSet(_pollId, REVEAL_END); } - uint256 pctReputation = wdiv(userRep, poll.skillRep); + uint256 fractionUserReputation = wdiv(userRep, poll.skillRep); uint256 totalStake = add(poll.stakes[YAY], poll.stakes[NAY]); - uint256 voterReward = wmul(wmul(pctReputation, totalStake), voterRewardFraction); + uint256 voterReward = wmul(wmul(fractionUserReputation, totalStake), voterRewardFraction); poll.paidVoterComp = add(poll.paidVoterComp, voterReward); tokenLocking.transfer(token, voterReward, msg.sender, true); From 292b0394ef8e2a54a10503a7fc9b02e8ca989bb2 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Thu, 30 Jul 2020 11:15:07 -0700 Subject: [PATCH 34/61] Respond to review comments V --- contracts/extensions/VotingReputation.sol | 81 +++++++------- test/extensions/voting-rep.js | 122 ++++++++++------------ 2 files changed, 93 insertions(+), 110 deletions(-) diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index aaa3977414..418682bf7c 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -48,10 +48,9 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { uint256 constant NAY = 0; uint256 constant YAY = 1; - uint256 constant STAKE1_END = 0; - uint256 constant STAKE2_END = 1; - uint256 constant SUBMIT_END = 2; - uint256 constant REVEAL_END = 3; + uint256 constant STAKE_END = 0; + uint256 constant SUBMIT_END = 1; + uint256 constant REVEAL_END = 2; bytes4 constant CHANGE_FUNCTION = bytes4( keccak256("setExpenditureState(uint256,uint256,uint256,uint256,bool[],bytes32[],bytes32)") @@ -67,19 +66,19 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { ITokenLocking tokenLocking; address token; - // All `Fraction` variables are stored as WADs i.e. fixed-point numbers with 18 digits after the radix. So + // All `Fraction` variables are stored as WADs i.e. fixed-point numbers with 18 digits after the radix. So // 1 WAD = 10**18, which is interpreted as 1. - uint256 totalStakeFraction; // Fraction of the domain's reputation needed to stake on each side in order to go to a poll. NB if set to over - // 0.5, then votes can never occur. - uint256 userMinStakeFraction; // Minimum stake as fraction of required stake. 1 means a single user will be required to + uint256 totalStakeFraction; // Fraction of the domain's reputation needed to stake on each side in order to go to a poll. + // This can be set to a maximum of 0.5. + uint256 voterRewardFraction; // Fraction of staked tokens paid out to voters as rewards. This will be paid from the staked + // tokens of the losing side. This can be set to a maximum of 0.5. + + uint256 userMinStakeFraction; // Minimum stake as fraction of required stake. 1 means a single user will be required to // provide the whole stake on each side, which may not be possible depending on totalStakeFraction and the distribution of // reputation in a domain. - uint256 maxVoteFraction; // Fraction of total domain reputation that needs to commit votes before closing to further votes. // Setting this to anything other than 1 will mean it is likely not all those eligible to vote will be able to do so. - uint256 voterRewardFraction; // Fraction of staked tokens paid out to voters as rewards. This will be paid from the staked - // tokens of the losing side. This can be set to a maximum of 0.5. // All `Period` variables are second-denominated @@ -106,9 +105,9 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { /// @param _escalationPeriod The length of the escalation period in seconds function initialise( uint256 _totalStakeFraction, + uint256 _voterRewardFraction, uint256 _userMinStakeFraction, uint256 _maxVoteFraction, - uint256 _voterRewardFraction, uint256 _stakePeriod, uint256 _submitPeriod, uint256 _revealPeriod, @@ -119,11 +118,11 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { require(colony.hasUserRole(msg.sender, 1, ColonyDataTypes.ColonyRole.Root), "voting-rep-user-not-root"); require(state == ExtensionState.Deployed, "voting-rep-already-initialised"); - require(_totalStakeFraction <= WAD, "voting-rep-greater-than-wad"); - require(_userMinStakeFraction <= WAD, "voting-rep-greater-than-wad"); + require(_totalStakeFraction <= WAD / 2, "voting-rep-greater-than-half-wad"); + require(_voterRewardFraction <= WAD / 2, "voting-rep-greater-than-half-wad"); + require(_userMinStakeFraction <= WAD, "voting-rep-greater-than-wad"); require(_maxVoteFraction <= WAD, "voting-rep-greater-than-wad"); - require(_voterRewardFraction <= WAD / 2, "voting-rep-greater-than-half-wad"); require(_stakePeriod <= 365 days, "voting-rep-period-too-long"); require(_submitPeriod <= 365 days, "voting-rep-period-too-long"); @@ -133,10 +132,10 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { state = ExtensionState.Active; totalStakeFraction = _totalStakeFraction; - userMinStakeFraction = _userMinStakeFraction; + voterRewardFraction = _voterRewardFraction; + userMinStakeFraction = _userMinStakeFraction; maxVoteFraction = _maxVoteFraction; - voterRewardFraction = _voterRewardFraction; stakePeriod = _stakePeriod; submitPeriod = _submitPeriod; @@ -159,7 +158,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { enum PollState { Staking, Submit, Reveal, Closed, Finalizable, Finalized, Failed } struct Poll { - uint64[4] events; // For recording poll lifecycle timestamps (STAKE1, STAKE2, SUBMIT, REVEAL) + uint64[3] events; // For recording poll lifecycle timestamps (STAKE, SUBMIT, REVEAL) bytes32 rootHash; uint256 domainId; uint256 skillId; @@ -263,6 +262,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { { Poll storage poll = polls[_pollId]; require(_vote <= 1, "voting-rep-bad-vote"); + require(_amount > 0, "voting-rep-bad-amount"); require(getPollState(_pollId) == PollState.Staking, "voting-rep-staking-closed"); uint256 requiredStake = getRequiredStake(_pollId); @@ -290,7 +290,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { bytes32 structHash = hashExpenditureStruct(poll.action); expenditurePollCounts[structHash] = add(expenditurePollCounts[structHash], 1); bytes memory claimDelayAction = createClaimDelayAction(poll.action, UINT256_MAX); - executeCall(_pollId, claimDelayAction); + require(executeCall(_pollId, claimDelayAction), "voting-rep-expenditure-lock-failed"); } // Move to second staking window once one side is fully staked @@ -298,22 +298,21 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { (_vote == YAY && poll.stakes[YAY] == requiredStake && poll.stakes[NAY] < requiredStake) || (_vote == NAY && poll.stakes[NAY] == requiredStake && poll.stakes[YAY] < requiredStake) ) { - poll.events[STAKE1_END] = uint64(now); - poll.events[STAKE2_END] = uint64(now + stakePeriod); + poll.events[STAKE_END] = uint64(now + stakePeriod); poll.events[SUBMIT_END] = uint64(now + stakePeriod + submitPeriod); poll.events[REVEAL_END] = uint64(now + stakePeriod + submitPeriod + revealPeriod); - emit PollEventSet(_pollId, STAKE1_END); + emit PollEventSet(_pollId, STAKE_END); } // Claim tokens once both sides are fully staked if (poll.stakes[YAY] == requiredStake && poll.stakes[NAY] == requiredStake) { - poll.events[STAKE2_END] = uint64(now); + poll.events[STAKE_END] = uint64(now); poll.events[SUBMIT_END] = uint64(now + submitPeriod); poll.events[REVEAL_END] = uint64(now + submitPeriod + revealPeriod); tokenLocking.claim(token, true); - emit PollEventSet(_pollId, STAKE2_END); + emit PollEventSet(_pollId, STAKE_END); } emit PollStaked(_pollId, msg.sender, _vote, amount); @@ -349,14 +348,14 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { voteSecrets[_pollId][msg.sender] = _voteSecret; + emit PollVoteSubmitted(_pollId, msg.sender); + if (poll.repSubmitted >= wmul(poll.skillRep, maxVoteFraction)) { poll.events[SUBMIT_END] = uint64(now); poll.events[REVEAL_END] = uint64(now + revealPeriod); emit PollEventSet(_pollId, SUBMIT_END); } - - emit PollVoteSubmitted(_pollId, msg.sender); } /// @notice Reveal a vote secret for a poll @@ -390,11 +389,6 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { poll.votes[_vote] = add(poll.votes[_vote], userRep); poll.repRevealed = add(poll.repRevealed, userRep); - if (poll.repRevealed == poll.repSubmitted) { - poll.events[REVEAL_END] = uint64(now); - - emit PollEventSet(_pollId, REVEAL_END); - } uint256 fractionUserReputation = wdiv(userRep, poll.skillRep); uint256 totalStake = add(poll.stakes[YAY], poll.stakes[NAY]); @@ -404,6 +398,12 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { tokenLocking.transfer(token, voterReward, msg.sender, true); emit PollVoteRevealed(_pollId, msg.sender, _vote); + + if (poll.repRevealed == poll.repSubmitted) { + poll.events[REVEAL_END] = uint64(now); + + emit PollEventSet(_pollId, REVEAL_END); + } } /// @notice Escalate a poll to a higher domain @@ -434,8 +434,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { delete poll.events; - poll.events[STAKE1_END] = uint64(now + stakePeriod); - poll.events[STAKE2_END] = uint64(now + stakePeriod); + poll.events[STAKE_END] = uint64(now + stakePeriod); poll.events[SUBMIT_END] = uint64(now + stakePeriod + submitPeriod); poll.events[REVEAL_END] = uint64(now + stakePeriod + submitPeriod + revealPeriod); @@ -464,7 +463,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { // Release the claimDelay if this is the last active poll if (expenditurePollCounts[structHash] == 0) { bytes memory claimDelayAction = createClaimDelayAction(poll.action, 0); - executeCall(_pollId, claimDelayAction); + require(executeCall(_pollId, claimDelayAction), "voting-rep-expenditure-unlock-failed"); } uint256 requiredStake = getRequiredStake(_pollId); @@ -511,7 +510,6 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { ); uint256 stake = stakes[_pollId][_user][_vote]; - uint256 requiredStake = getRequiredStake(_pollId); delete stakes[_pollId][_user][_vote]; @@ -519,7 +517,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { uint256 repPenalty; // Went to a vote, use vote to determine reward or penalty - if (poll.stakes[NAY] == requiredStake && poll.stakes[YAY] == requiredStake) { + if (add(poll.votes[NAY], poll.votes[YAY]) > 0) { uint256 loserStake = sub( (poll.votes[NAY] < poll.votes[YAY]) ? poll.stakes[NAY] : poll.stakes[YAY], @@ -541,7 +539,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { } // Your side fully staked, receive 10% (proportional) of loser's stake - } else if (poll.stakes[_vote] == requiredStake) { + } else if (poll.stakes[_vote] == getRequiredStake(_pollId)) { uint256 loserStake = sub(poll.stakes[flip(_vote)], poll.paidVoterComp); uint256 stakeFraction = wdiv(stake, poll.stakes[_vote]); @@ -550,7 +548,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { stakerReward = add(stake, wmul(stakeFraction, totalPenalty)); // Opponent's side fully staked, pay 10% penalty - } else if (poll.stakes[flip(_vote)] == requiredStake) { + } else if (poll.stakes[flip(_vote)] == getRequiredStake(_pollId)) { uint256 loserStake = sub(poll.stakes[_vote], poll.paidVoterComp); uint256 stakeFraction = wdiv(stake, poll.stakes[_vote]); @@ -638,7 +636,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { ) { // Are we still staking? - if (now < poll.events[STAKE2_END]) { + if (now < poll.events[STAKE_END]) { return PollState.Staking; // If not, did the YAY side stake? } else if (poll.stakes[YAY] == requiredStake) { @@ -652,7 +650,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { } // Do we need to keep waiting? - } else if (now < poll.events[STAKE2_END]) { + } else if (now < poll.events[STAKE_END]) { return PollState.Staking; @@ -690,8 +688,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { pollCount += 1; - polls[pollCount].events[STAKE1_END] = uint64(now + stakePeriod); - polls[pollCount].events[STAKE2_END] = uint64(now + stakePeriod); + polls[pollCount].events[STAKE_END] = uint64(now + stakePeriod); polls[pollCount].events[SUBMIT_END] = uint64(now + stakePeriod + submitPeriod); polls[pollCount].events[REVEAL_END] = uint64(now + stakePeriod + submitPeriod + revealPeriod); diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index 0f35655679..1bcdb55578 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -98,6 +98,8 @@ contract("Voting Reputation", (accounts) => { const ADDRESS_ZERO = ethers.constants.AddressZero; const REQUIRED_STAKE = WAD.muln(3).divn(1000); const WAD32 = bn2bytes32(WAD); + const HALF = WAD.divn(2); + const YEAR = SECONDS_PER_DAY * 365; before(async () => { colonyNetwork = await setupColonyNetwork(); @@ -129,9 +131,9 @@ contract("Voting Reputation", (accounts) => { await voting.initialise( TOTAL_STAKE_FRACTION, + VOTER_REWARD_FRACTION, USER_MIN_STAKE_FRACTION, MAX_VOTE_FRACTION, - VOTER_REWARD_FRACTION, STAKE_PERIOD, SUBMIT_PERIOD, REVEAL_PERIOD, @@ -241,17 +243,8 @@ contract("Voting Reputation", (accounts) => { }); it("cannot initialise twice or if not root", async () => { - await checkErrorRevert( - voting.initialise(WAD, WAD, WAD, WAD.divn(2), STAKE_PERIOD, SUBMIT_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD), - "voting-rep-already-initialised" - ); - - await checkErrorRevert( - voting.initialise(WAD, WAD, WAD, WAD.divn(2), STAKE_PERIOD, SUBMIT_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD, { - from: USER2, - }), - "voting-rep-user-not-root" - ); + await checkErrorRevert(voting.initialise(HALF, HALF, WAD, WAD, YEAR, YEAR, YEAR, YEAR), "voting-rep-already-initialised"); + await checkErrorRevert(voting.initialise(HALF, HALF, WAD, WAD, YEAR, YEAR, YEAR, YEAR, { from: USER2 }), "voting-rep-user-not-root"); }); it("cannot initialise with invalid values", async () => { @@ -260,49 +253,16 @@ contract("Voting Reputation", (accounts) => { const votingAddress = await votingFactory.deployedExtensions(colony.address); voting = await VotingReputation.at(votingAddress); - const half = WAD.divn(2); - - await checkErrorRevert( - voting.initialise(WAD.addn(1), WAD, WAD, half, STAKE_PERIOD, SUBMIT_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD), - "voting-rep-greater-than-wad" - ); - - await checkErrorRevert( - voting.initialise(WAD, WAD.addn(1), WAD, half, STAKE_PERIOD, SUBMIT_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD), - "voting-rep-greater-than-wad" - ); - - await checkErrorRevert( - voting.initialise(WAD, WAD, WAD.addn(1), half, STAKE_PERIOD, SUBMIT_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD), - "voting-rep-greater-than-wad" - ); - - await checkErrorRevert( - voting.initialise(WAD, WAD, WAD, half.addn(1), STAKE_PERIOD, SUBMIT_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD), - "voting-rep-greater-than-half-wad" - ); - - await checkErrorRevert( - voting.initialise(WAD, WAD, WAD, half, SECONDS_PER_DAY * 366, SUBMIT_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD), - "voting-rep-period-too-long" - ); - - await checkErrorRevert( - voting.initialise(WAD, WAD, WAD, half, STAKE_PERIOD, SECONDS_PER_DAY * 366, REVEAL_PERIOD, ESCALATION_PERIOD), - "voting-rep-period-too-long" - ); - - await checkErrorRevert( - voting.initialise(WAD, WAD, WAD, half, STAKE_PERIOD, SUBMIT_PERIOD, SECONDS_PER_DAY * 366, ESCALATION_PERIOD), - "voting-rep-period-too-long" - ); + await checkErrorRevert(voting.initialise(HALF.addn(1), HALF, WAD, WAD, YEAR, YEAR, YEAR, YEAR), "voting-rep-greater-than-half-wad"); + await checkErrorRevert(voting.initialise(HALF, HALF.addn(1), WAD, WAD, YEAR, YEAR, YEAR, YEAR), "voting-rep-greater-than-half-wad"); + await checkErrorRevert(voting.initialise(HALF, HALF, WAD.addn(1), WAD, YEAR, YEAR, YEAR, YEAR), "voting-rep-greater-than-wad"); + await checkErrorRevert(voting.initialise(HALF, HALF, WAD, WAD.addn(1), YEAR, YEAR, YEAR, YEAR), "voting-rep-greater-than-wad"); + await checkErrorRevert(voting.initialise(HALF, HALF, WAD, WAD, YEAR + 1, YEAR, YEAR, YEAR), "voting-rep-period-too-long"); + await checkErrorRevert(voting.initialise(HALF, HALF, WAD, WAD, YEAR, YEAR + 1, YEAR, YEAR), "voting-rep-period-too-long"); + await checkErrorRevert(voting.initialise(HALF, HALF, WAD, WAD, YEAR, YEAR, YEAR + 1, YEAR), "voting-rep-period-too-long"); + await checkErrorRevert(voting.initialise(HALF, HALF, WAD, WAD, YEAR, YEAR, YEAR, YEAR + 1), "voting-rep-period-too-long"); - await checkErrorRevert( - voting.initialise(WAD, WAD, WAD, half, STAKE_PERIOD, SUBMIT_PERIOD, REVEAL_PERIOD, SECONDS_PER_DAY * 366), - "voting-rep-period-too-long" - ); - - await voting.initialise(WAD, WAD, WAD, half, STAKE_PERIOD, SUBMIT_PERIOD, REVEAL_PERIOD, ESCALATION_PERIOD); + await voting.initialise(HALF, HALF, WAD, WAD, YEAR, YEAR, YEAR, YEAR); }); }); @@ -384,17 +344,6 @@ contract("Voting Reputation", (accounts) => { expect(stake1).to.eq.BN(REQUIRED_STAKE.divn(2)); }); - it("cannot stake less than the minStake", async () => { - const minStake = REQUIRED_STAKE.divn(10); - - await checkErrorRevert( - voting.stakePoll(pollId, 1, UINT256_MAX, YAY, minStake.subn(1), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), - "voting-rep-insufficient-stake" - ); - - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, minStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - }); - it("can update the poll states correctly", async () => { let pollState = await voting.getPollState(pollId); expect(pollState).to.eq.BN(STAKING); @@ -421,6 +370,13 @@ contract("Voting Reputation", (accounts) => { expect(lock.balance).to.eq.BN(REQUIRED_STAKE.muln(2)); }); + it("cannot stake 0", async () => { + await checkErrorRevert( + voting.stakePoll(pollId, 1, UINT256_MAX, YAY, 0, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), + "voting-rep-bad-amount" + ); + }); + it("cannot stake a nonexistent side", async () => { await checkErrorRevert( voting.stakePoll(pollId, 1, UINT256_MAX, 2, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), @@ -428,6 +384,17 @@ contract("Voting Reputation", (accounts) => { ); }); + it("cannot stake less than the minStake", async () => { + const minStake = REQUIRED_STAKE.divn(10); + + await checkErrorRevert( + voting.stakePoll(pollId, 1, UINT256_MAX, YAY, minStake.subn(1), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), + "voting-rep-insufficient-stake" + ); + + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, minStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + }); + it("can update the expenditure globalClaimDelay if voting on expenditure state", async () => { await colony.makeExpenditure(1, UINT256_MAX, 1); const expenditureId = await colony.getExpenditureCount(); @@ -525,6 +492,19 @@ contract("Voting Reputation", (accounts) => { expect(expenditureSlot.claimDelay).to.eq.BN(UINT256_MAX); }); + it("cannot update the expenditure slot claimDelay if given an invalid action", async () => { + // Create a poorly-formed action (no keys) + const action = await encodeTxData(colony, "setExpenditureState", [1, UINT256_MAX, 1, 0, [], [], ethers.constants.HashZero]); + + await voting.createDomainPoll(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + pollId = await voting.getPollCount(); + + await checkErrorRevert( + voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), + "voting-rep-expenditure-lock-failed" + ); + }); + it("can accurately track the number of polls for a single expenditure", async () => { await colony.makeExpenditure(1, UINT256_MAX, 1); const expenditureId = await colony.getExpenditureCount(); @@ -995,7 +975,10 @@ contract("Voting Reputation", (accounts) => { }); it("can set vote power correctly if there is insufficient opposition", async () => { - const action = await encodeTxData(colony, "setExpenditureState", [1, UINT256_MAX, 1, 0, [], [], ethers.constants.HashZero]); + await colony.makeExpenditure(1, UINT256_MAX, 1); + const expenditureId = await colony.getExpenditureCount(); + + const action = await encodeTxData(colony, "setExpenditureState", [1, UINT256_MAX, expenditureId, 25, [true], ["0x0"], WAD32]); await voting.createDomainPoll(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); pollId = await voting.getPollCount(); @@ -1011,7 +994,10 @@ contract("Voting Reputation", (accounts) => { }); it("can set vote power correctly after a vote", async () => { - const action = await encodeTxData(colony, "setExpenditureState", [1, UINT256_MAX, 1, 0, [], [], ethers.constants.HashZero]); + await colony.makeExpenditure(1, UINT256_MAX, 1); + const expenditureId = await colony.getExpenditureCount(); + + const action = await encodeTxData(colony, "setExpenditureState", [1, UINT256_MAX, expenditureId, 25, [true], ["0x0"], WAD32]); await voting.createDomainPoll(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); pollId = await voting.getPollCount(); @@ -1260,7 +1246,7 @@ contract("Voting Reputation", (accounts) => { expect(userLock0.balance).to.eq.BN(userLock1.balance); }); - it("cannot claim rewards before a poll is executed", async () => { + it("cannot claim rewards before a poll is finalized", async () => { await checkErrorRevert(voting.claimReward(pollId, 1, UINT256_MAX, USER0, YAY), "voting-rep-not-failed-or-finalized"); }); }); From 8a8b738a84ef44501a6481bd551c5a0e16177c51 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Fri, 31 Jul 2020 10:29:01 -0700 Subject: [PATCH 35/61] Respond to reviewer feedback VI --- contracts/extensions/VotingReputation.sol | 41 ++++++++++++++++++----- test/extensions/voting-rep.js | 10 ++++-- 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index 418682bf7c..2a9db0ca59 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -262,19 +262,21 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { { Poll storage poll = polls[_pollId]; require(_vote <= 1, "voting-rep-bad-vote"); - require(_amount > 0, "voting-rep-bad-amount"); require(getPollState(_pollId) == PollState.Staking, "voting-rep-staking-closed"); uint256 requiredStake = getRequiredStake(_pollId); uint256 amount = min(_amount, sub(requiredStake, poll.stakes[_vote])); uint256 stakerTotalAmount = add(stakes[_pollId][msg.sender][_vote], amount); + require(amount > 0, "voting-rep-bad-amount"); + require( stakerTotalAmount <= getReputationFromProof(_pollId, msg.sender, _key, _value, _branchMask, _siblings), "voting-rep-insufficient-rep" ); require( - stakerTotalAmount >= wmul(requiredStake, userMinStakeFraction), + stakerTotalAmount >= wmul(requiredStake, userMinStakeFraction) || + add(poll.stakes[_vote], amount) == requiredStake, // To prevent a residual stake from being un-stakable "voting-rep-insufficient-stake" ); @@ -286,7 +288,12 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { stakes[_pollId][msg.sender][_vote] = stakerTotalAmount; // Increment counter & extend claim delay if staking for an expenditure state change - if (poll.stakes[YAY] == requiredStake && _vote == YAY && getSig(poll.action) == CHANGE_FUNCTION) { + if ( + _vote == YAY && + poll.stakes[YAY] == requiredStake && + getSig(poll.action) == CHANGE_FUNCTION && + add(poll.votes[NAY], poll.votes[YAY]) == 0 + ) { bytes32 structHash = hashExpenditureStruct(poll.action); expenditurePollCounts[structHash] = add(expenditurePollCounts[structHash], 1); bytes memory claimDelayAction = createClaimDelayAction(poll.action, UINT256_MAX); @@ -432,8 +439,6 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { uint256 childSkillId = colonyNetwork.getChildSkillId(newDomainSkillId, _childSkillIndex); require(childSkillId == poll.skillId, "voting-rep-invalid-domain-proof"); - delete poll.events; - poll.events[STAKE_END] = uint64(now + stakePeriod); poll.events[SUBMIT_END] = uint64(now + stakePeriod + submitPeriod); poll.events[REVEAL_END] = uint64(now + stakePeriod + submitPeriod + revealPeriod); @@ -442,7 +447,24 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { poll.skillId = newDomainSkillId; poll.skillRep = getReputationFromProof(_pollId, address(0x0), _key, _value, _branchMask, _siblings); + if (poll.votes[NAY] < poll.votes[YAY]) { + poll.stakes[NAY] = sub(poll.stakes[NAY], poll.paidVoterComp); + } else { + poll.stakes[YAY] = sub(poll.stakes[YAY], poll.paidVoterComp); + } + + delete poll.paidVoterComp; + emit PollEscalated(_pollId, msg.sender, _newDomainId); + + // Check to see if the stake is unchanged, if so skip the staking period + if (poll.stakes[NAY] == getRequiredStake(_pollId)) { + poll.events[STAKE_END] = uint64(now); + poll.events[SUBMIT_END] = uint64(now + submitPeriod); + poll.events[REVEAL_END] = uint64(now + submitPeriod + revealPeriod); + + emit PollEventSet(_pollId, STAKE_END); + } } function finalizePoll(uint256 _pollId) public { @@ -714,10 +736,6 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { return 1 - _vote; } - function selfOrMax(uint256 _timestamp) internal pure returns (uint256) { - return (_timestamp == 0) ? UINT128_MAX : _timestamp; - } - function getReputationFromProof( uint256 _pollId, address _who, @@ -827,6 +845,11 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { { // See https://solidity.readthedocs.io/en/develop/abi-spec.html#use-of-dynamic-types // for documentation on how the action `bytes` is encoded + // In brief, the first byte32 is the length of the array. Then we have + // 4 bytes of function signature, following by an arbitrary number of + // additional byte32 arguments. 32 in hex is 0x20, so every increment + // of 0x20 represents advancing one byte, 4 is the function signature. + // So: 0x[length][sig][args...] bytes32 functionSignature; uint256 permissionDomainId; diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index 1bcdb55578..50a9709f9b 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -384,7 +384,7 @@ contract("Voting Reputation", (accounts) => { ); }); - it("cannot stake less than the minStake", async () => { + it("cannot stake less than the minStake, unless there is less than minStake to go", async () => { const minStake = REQUIRED_STAKE.divn(10); await checkErrorRevert( @@ -393,6 +393,12 @@ contract("Voting Reputation", (accounts) => { ); await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, minStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + + // Unless there's less than the minStake to go! + + const stake = REQUIRED_STAKE.sub(minStake.muln(2)).addn(1); + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, stake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, minStake.subn(1), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); }); it("can update the expenditure globalClaimDelay if voting on expenditure state", async () => { @@ -1386,7 +1392,7 @@ contract("Voting Reputation", (accounts) => { const remainingStake = REQUIRED_STAKE.sub(WAD.divn(1000)); await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, remainingStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, remainingStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); // Make the vote fail this time (everyone votes against) await voting.submitVote(pollId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); From 1cbbf5fc2613bdaa3f5c6b3686a0595af6546f1e Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Tue, 4 Aug 2020 15:10:26 -0700 Subject: [PATCH 36/61] Update reward accounting for escalations --- contracts/extensions/VotingReputation.sol | 71 +++++------- test/extensions/voting-rep.js | 132 +++++++++++++--------- 2 files changed, 107 insertions(+), 96 deletions(-) diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index 2a9db0ca59..5c7daaa917 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -166,6 +166,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { uint256 repSubmitted; uint256 repRevealed; uint256 paidVoterComp; + uint256[2] pastVoterComp; // [nay, yay] uint256[2] stakes; // [nay, yay] uint256[2] votes; // [nay, yay] bool finalized; @@ -308,6 +309,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { poll.events[STAKE_END] = uint64(now + stakePeriod); poll.events[SUBMIT_END] = uint64(now + stakePeriod + submitPeriod); poll.events[REVEAL_END] = uint64(now + stakePeriod + submitPeriod + revealPeriod); + delete poll.votes; // New stake supersedes prior votes emit PollEventSet(_pollId, STAKE_END); } @@ -447,24 +449,12 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { poll.skillId = newDomainSkillId; poll.skillRep = getReputationFromProof(_pollId, address(0x0), _key, _value, _branchMask, _siblings); - if (poll.votes[NAY] < poll.votes[YAY]) { - poll.stakes[NAY] = sub(poll.stakes[NAY], poll.paidVoterComp); - } else { - poll.stakes[YAY] = sub(poll.stakes[YAY], poll.paidVoterComp); - } - + uint256 loser = (poll.votes[NAY] < poll.votes[YAY]) ? NAY : YAY; + poll.stakes[loser] = sub(poll.stakes[loser], poll.paidVoterComp); + poll.pastVoterComp[loser] = add(poll.pastVoterComp[loser], poll.paidVoterComp); delete poll.paidVoterComp; emit PollEscalated(_pollId, msg.sender, _newDomainId); - - // Check to see if the stake is unchanged, if so skip the staking period - if (poll.stakes[NAY] == getRequiredStake(_pollId)) { - poll.events[STAKE_END] = uint64(now); - poll.events[SUBMIT_END] = uint64(now + submitPeriod); - poll.events[REVEAL_END] = uint64(now + submitPeriod + revealPeriod); - - emit PollEventSet(_pollId, STAKE_END); - } } function finalizePoll(uint256 _pollId) public { @@ -488,9 +478,9 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { require(executeCall(_pollId, claimDelayAction), "voting-rep-expenditure-unlock-failed"); } - uint256 requiredStake = getRequiredStake(_pollId); - uint256 votePower = (poll.stakes[NAY] < requiredStake) ? poll.stakes[YAY] : poll.votes[YAY]; bytes32 slotHash = hashExpenditureSlot(poll.action); + uint256 votePower = (add(poll.votes[NAY], poll.votes[YAY]) > 0) ? + poll.votes[YAY] : poll.stakes[YAY]; if (expenditurePastPolls[slotHash] < votePower) { expenditurePastPolls[slotHash] = votePower; @@ -531,60 +521,58 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { "voting-rep-not-failed-or-finalized" ); - uint256 stake = stakes[_pollId][_user][_vote]; + uint256 stakeFraction = wdiv( + stakes[_pollId][_user][_vote], + add(poll.stakes[_vote], poll.pastVoterComp[_vote]) + ); delete stakes[_pollId][_user][_vote]; + uint256 realStake = wmul(stakeFraction, poll.stakes[_vote]); + uint256 requiredStake = getRequiredStake(_pollId); + uint256 stakerReward; uint256 repPenalty; // Went to a vote, use vote to determine reward or penalty - if (add(poll.votes[NAY], poll.votes[YAY]) > 0) { + if (add(poll.votes[NAY], poll.votes[YAY]) > 0) { + assert(poll.stakes[NAY] == requiredStake && poll.stakes[YAY] == requiredStake); - uint256 loserStake = sub( - (poll.votes[NAY] < poll.votes[YAY]) ? poll.stakes[NAY] : poll.stakes[YAY], - poll.paidVoterComp - ); - - uint256 stakerSideVotes = poll.votes[_vote]; + uint256 loserStake = sub(requiredStake, poll.paidVoterComp); uint256 totalVotes = add(poll.votes[NAY], poll.votes[YAY]); - uint256 winFraction = wdiv(stakerSideVotes, totalVotes); - uint256 winShare = wmul(winFraction, 2 * WAD); // On a scale of 0 - 2 WAD - - uint256 stakeFraction = wdiv(stake, poll.stakes[_vote]); + uint256 winFraction = wdiv(poll.votes[_vote], totalVotes); + uint256 winShare = wmul(winFraction, 2 * WAD); // On a scale of 0-2 WAD if (winShare > WAD || (winShare == WAD && _vote == NAY)) { - stakerReward = add(stake, wmul(stakeFraction, wmul(loserStake, winShare - WAD))); + stakerReward = wmul(stakeFraction, add(requiredStake, wmul(loserStake, winShare - WAD))); } else { stakerReward = wmul(stakeFraction, wmul(loserStake, winShare)); - repPenalty = sub(stake, stakerReward); + repPenalty = sub(realStake, stakerReward); } // Your side fully staked, receive 10% (proportional) of loser's stake - } else if (poll.stakes[_vote] == getRequiredStake(_pollId)) { + } else if (poll.stakes[_vote] == requiredStake) { uint256 loserStake = sub(poll.stakes[flip(_vote)], poll.paidVoterComp); - uint256 stakeFraction = wdiv(stake, poll.stakes[_vote]); uint256 totalPenalty = wmul(loserStake, WAD / 10); - stakerReward = add(stake, wmul(stakeFraction, totalPenalty)); + stakerReward = wmul(stakeFraction, add(requiredStake, totalPenalty)); // Opponent's side fully staked, pay 10% penalty - } else if (poll.stakes[flip(_vote)] == getRequiredStake(_pollId)) { + } else if (poll.stakes[flip(_vote)] == requiredStake) { uint256 loserStake = sub(poll.stakes[_vote], poll.paidVoterComp); - uint256 stakeFraction = wdiv(stake, poll.stakes[_vote]); uint256 totalPenalty = wmul(loserStake, WAD / 10); - stakerReward = sub(stake, wmul(stakeFraction, totalPenalty)); - repPenalty = sub(stake, stakerReward); + stakerReward = wmul(stakeFraction, sub(loserStake, totalPenalty)); + repPenalty = sub(realStake, stakerReward); // Neither side fully staked, no reward or penalty } else { uint256 totalStake = add(poll.stakes[NAY], poll.stakes[YAY]); uint256 rewardShare = wdiv(sub(totalStake, poll.paidVoterComp), totalStake); - stakerReward = wmul(stake, rewardShare); + stakerReward = wmul(realStake, rewardShare); } tokenLocking.transfer(token, stakerReward, _user, true); @@ -671,11 +659,6 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { return PollState.Failed; } - // Do we need to keep waiting? - } else if (now < poll.events[STAKE_END]) { - - return PollState.Staking; - // Fully staked, go to a vote } else { diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index 50a9709f9b..1301b7d45f 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -93,7 +93,7 @@ contract("Voting Reputation", (accounts) => { // const CLOSED = 3; // const EXECUTABLE = 4; // const EXECUTED = 5; - // const FAILED = 6; + const FAILED = 6; const ADDRESS_ZERO = ethers.constants.AddressZero; const REQUIRED_STAKE = WAD.muln(3).divn(1000); @@ -1041,10 +1041,9 @@ contract("Voting Reputation", (accounts) => { const repCycle = await IReputationMiningCycle.at(addr); const numEntriesPrev = await repCycle.getReputationUpdateLogLength(); + const nayStake = REQUIRED_STAKE.divn(2); await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE.divn(2), user1Key, user1Value, user1Mask, user1Siblings, { - from: USER1, - }); + await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, nayStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await forwardTime(STAKE_PERIOD, this); @@ -1265,13 +1264,13 @@ contract("Voting Reputation", (accounts) => { const domain2Value = makeReputationValue(WAD, 6); const [domain2Mask, domain2Siblings] = await reputationTree.getProof(domain2Key); - user0Key = makeReputationKey(colony.address, domain2.skillId, USER0); - user0Value = makeReputationValue(WAD.divn(3), 9); - [user0Mask, user0Siblings] = await reputationTree.getProof(user0Key); + const user0Key2 = makeReputationKey(colony.address, domain2.skillId, USER0); + const user0Value2 = makeReputationValue(WAD.divn(3), 9); + const [user0Mask2, user0Siblings2] = await reputationTree.getProof(user0Key2); - user1Key = makeReputationKey(colony.address, domain2.skillId, USER1); - user1Value = makeReputationValue(WAD.divn(3).muln(2), 10); - [user1Mask, user1Siblings] = await reputationTree.getProof(user1Key); + const user1Key2 = makeReputationKey(colony.address, domain2.skillId, USER1); + const user1Value2 = makeReputationValue(WAD.divn(3).muln(2), 10); + const [user1Mask2, user1Siblings2] = await reputationTree.getProof(user1Key2); const action = await encodeTxData(colony, "makeTask", [1, 0, FAKE, 2, 0, 0]); await voting.createDomainPoll(2, UINT256_MAX, ADDRESS_ZERO, action, domain2Key, domain2Value, domain2Mask, domain2Siblings); @@ -1280,15 +1279,15 @@ contract("Voting Reputation", (accounts) => { await colony.approveStake(voting.address, 2, WAD, { from: USER0 }); await colony.approveStake(voting.address, 2, WAD, { from: USER1 }); - await voting.stakePoll(pollId, 1, 0, YAY, WAD.divn(1000), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, 0, NAY, WAD.divn(1000), user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakePoll(pollId, 1, 0, NAY, WAD.divn(1000), user0Key2, user0Value2, user0Mask2, user0Siblings2, { from: USER0 }); + await voting.stakePoll(pollId, 1, 0, YAY, WAD.divn(1000), user1Key2, user1Value2, user1Mask2, user1Siblings2, { from: USER1 }); // Note that this is a passing vote - await voting.submitVote(pollId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.submitVote(pollId, soliditySha3(SALT, YAY), user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.submitVote(pollId, soliditySha3(SALT, NAY), user0Key2, user0Value2, user0Mask2, user0Siblings2, { from: USER0 }); + await voting.submitVote(pollId, soliditySha3(SALT, YAY), user1Key2, user1Value2, user1Mask2, user1Siblings2, { from: USER1 }); - await voting.revealVote(pollId, SALT, NAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.revealVote(pollId, SALT, YAY, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.revealVote(pollId, SALT, NAY, user0Key2, user0Value2, user0Mask2, user0Siblings2, { from: USER0 }); + await voting.revealVote(pollId, SALT, YAY, user1Key2, user1Value2, user1Mask2, user1Siblings2, { from: USER1 }); }); it("can internally escalate a domain poll after a vote", async () => { @@ -1324,28 +1323,20 @@ contract("Voting Reputation", (accounts) => { it("can stake after internally escalating a domain poll", async () => { await voting.escalatePoll(pollId, 1, 0, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); - user0Key = makeReputationKey(colony.address, domain1.skillId, USER0); - user0Value = makeReputationValue(WAD, 2); - [user0Mask, user0Siblings] = await reputationTree.getProof(user0Key); - - user1Key = makeReputationKey(colony.address, domain1.skillId, USER1); - user1Value = makeReputationValue(WAD.muln(2), 5); - [user1Mask, user1Siblings] = await reputationTree.getProof(user1Key); + const yayStake = REQUIRED_STAKE.sub(WAD.divn(1000)); + const nayStake = yayStake.add(REQUIRED_STAKE.divn(10)); + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, yayStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, nayStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - const remainingStake = REQUIRED_STAKE.sub(WAD.divn(1000)); - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, remainingStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, remainingStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + const pollState = await voting.getPollState(pollId); + expect(pollState).to.eq.BN(SUBMIT); }); it("can execute after internally escalating a domain poll, if there is insufficient opposition", async () => { await voting.escalatePoll(pollId, 1, 0, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); - user0Key = makeReputationKey(colony.address, domain1.skillId, USER0); - user0Value = makeReputationValue(WAD, 2); - [user0Mask, user0Siblings] = await reputationTree.getProof(user0Key); - - const remainingStake = REQUIRED_STAKE.sub(WAD.divn(1000)); - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, remainingStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + const yayStake = REQUIRED_STAKE.sub(WAD.divn(1000)); + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, yayStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await forwardTime(STAKE_PERIOD, this); @@ -1356,12 +1347,8 @@ contract("Voting Reputation", (accounts) => { it("cannot execute after internally escalating a domain poll, if there is insufficient support", async () => { await voting.escalatePoll(pollId, 1, 0, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); - user1Key = makeReputationKey(colony.address, domain1.skillId, USER1); - user1Value = makeReputationValue(WAD.muln(2), 5); - [user1Mask, user1Siblings] = await reputationTree.getProof(user1Key); - - const remainingStake = REQUIRED_STAKE.sub(WAD.divn(1000)); - await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, remainingStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + const yayStake = REQUIRED_STAKE.sub(WAD.divn(1000)); + await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, yayStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await forwardTime(STAKE_PERIOD, this); @@ -1379,32 +1366,73 @@ contract("Voting Reputation", (accounts) => { expect(logs[0].args.executed).to.be.true; }); - it("can use the result of a new vote after internally escalating a domain poll", async () => { + it("can use the result of a new stake after internally escalating a domain poll", async () => { await voting.escalatePoll(pollId, 1, 0, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); - user0Key = makeReputationKey(colony.address, domain1.skillId, USER0); - user0Value = makeReputationValue(WAD, 2); - [user0Mask, user0Siblings] = await reputationTree.getProof(user0Key); + const yayStake = REQUIRED_STAKE.sub(WAD.divn(1000)); + const nayStake = yayStake.add(REQUIRED_STAKE.divn(10)); + await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, nayStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - user1Key = makeReputationKey(colony.address, domain1.skillId, USER1); - user1Value = makeReputationValue(WAD.muln(2), 5); - [user1Mask, user1Siblings] = await reputationTree.getProof(user1Key); + await forwardTime(STAKE_PERIOD, this); - const remainingStake = REQUIRED_STAKE.sub(WAD.divn(1000)); - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, remainingStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + const pollState = await voting.getPollState(pollId); + expect(pollState).to.eq.BN(FAILED); - // Make the vote fail this time (everyone votes against) - await voting.submitVote(pollId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + // Now check that the rewards come out properly + const user0LockPre = await tokenLocking.getUserLock(token.address, USER0); + const user1LockPre = await tokenLocking.getUserLock(token.address, USER1); + + await voting.claimReward(pollId, 1, UINT256_MAX, USER0, YAY); + await voting.claimReward(pollId, 1, UINT256_MAX, USER1, NAY); + + const user0LockPost = await tokenLocking.getUserLock(token.address, USER0); + const user1LockPost = await tokenLocking.getUserLock(token.address, USER1); + + const expectedReward1 = (REQUIRED_STAKE.add(WAD.divn(1000 * 10))).divn(32).muln(22); // eslint-disable-line prettier/prettier + + expect(new BN(user0LockPost.balance).sub(new BN(user0LockPre.balance))).to.be.zero; + expect(new BN(user1LockPost.balance).sub(new BN(user1LockPre.balance))).to.eq.BN(expectedReward1); + }); + + it("can use the result of a new vote after internally escalating a domain poll", async () => { + await voting.escalatePoll(pollId, 1, 0, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); + + const yayStake = REQUIRED_STAKE.sub(WAD.divn(1000)); + const nayStake = yayStake.add(REQUIRED_STAKE.divn(10)); + await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, yayStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, nayStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + + // Vote fails + await voting.submitVote(pollId, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await voting.submitVote(pollId, soliditySha3(SALT, NAY), user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await voting.revealVote(pollId, SALT, NAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.revealVote(pollId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await voting.revealVote(pollId, SALT, NAY, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await forwardTime(STAKE_PERIOD, this); + await forwardTime(ESCALATION_PERIOD, this); const { logs } = await voting.finalizePoll(pollId); expect(logs[0].args.executed).to.be.false; + + // Now check that the rewards come out properly + // 1st voter reward paid by YAY (user0), 2nd paid by NAY (user1) + const user0LockPre = await tokenLocking.getUserLock(token.address, USER0); + const user1LockPre = await tokenLocking.getUserLock(token.address, USER1); + + await voting.claimReward(pollId, 1, UINT256_MAX, USER0, YAY); + await voting.claimReward(pollId, 1, UINT256_MAX, USER1, NAY); + + const user0LockPost = await tokenLocking.getUserLock(token.address, USER0); + const user1LockPost = await tokenLocking.getUserLock(token.address, USER1); + + const loserStake = REQUIRED_STAKE.divn(10).muln(8); + // (stake * .8) * (winPct = 1/3 * 2) * 2/3 (since 1/3 of stake is from other user!) + const expectedReward0 = loserStake.divn(3).muln(2).divn(3).muln(2); + // stake + ((stake * .8) * (1 - (winPct = 2/3 * 2)) * 22/32) (since 10/32 of stake is from other user!) + const expectedReward1 = REQUIRED_STAKE.add(loserStake.divn(3)).divn(32).muln(22); + + expect(new BN(user0LockPost.balance).sub(new BN(user0LockPre.balance))).to.eq.BN(expectedReward0.addn(1)); // Rounding + expect(new BN(user1LockPost.balance).sub(new BN(user1LockPre.balance))).to.eq.BN(expectedReward1); }); }); }); From 0680d8863e95455f265c64cc028849a6f125eb11 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Tue, 4 Aug 2020 16:45:16 -0700 Subject: [PATCH 37/61] Correctly hash expenditure slots --- contracts/extensions/VotingReputation.sol | 7 ++++--- test/extensions/voting-rep.js | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index 5c7daaa917..fdf59d6cf8 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -796,9 +796,10 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { assembly { // Hash all but last (value) bytes32 - // Recall: mload(action) gives length of bytes array - // So skip past the first bytes32 (length), and the last bytes32 (value) - hash := keccak256(add(action, 0x20), sub(mload(action), 0x20)) + // Recall: mload(action) gives length of bytes array + // So skip past the three bytes32 (length + domain proof), + // and the last bytes32 (value). + hash := keccak256(add(action, 0x64), sub(mload(action), 0x64)) } } diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index 1301b7d45f..a3ff589240 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -994,7 +994,7 @@ contract("Voting Reputation", (accounts) => { await forwardTime(STAKE_PERIOD, this); await voting.finalizePoll(pollId); - const slotHash = soliditySha3(`0x${action.slice(2, action.length - 64)}`); + const slotHash = soliditySha3(`0x${action.slice(2 + 8 + 128, action.length - 64)}`); const pastPoll = await voting.getExpenditurePastPoll(slotHash); expect(pastPoll).to.eq.BN(REQUIRED_STAKE); }); @@ -1021,7 +1021,7 @@ contract("Voting Reputation", (accounts) => { await forwardTime(ESCALATION_PERIOD, this); await voting.finalizePoll(pollId); - const slotHash = soliditySha3(`0x${action.slice(2, action.length - 64)}`); + const slotHash = soliditySha3(`0x${action.slice(2 + 8 + 128, action.length - 64)}`); const pastPoll = await voting.getExpenditurePastPoll(slotHash); expect(pastPoll).to.eq.BN(WAD); // USER0 had 1 WAD of reputation }); From 209b7532af89ad4fcdab66f357e655a596d77adc Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Wed, 5 Aug 2020 09:08:32 -0700 Subject: [PATCH 38/61] Rename poll to motion --- contracts/extensions/VotingReputation.sol | 433 ++++++------- test/extensions/voting-rep.js | 707 +++++++++++----------- 2 files changed, 577 insertions(+), 563 deletions(-) diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index fdf59d6cf8..1461f2862e 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -32,14 +32,14 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { // Events event ExtensionInitialised(); event ExtensionDeprecated(); - event PollCreated(uint256 indexed pollId, address creator, uint256 indexed domainId); - event PollStaked(uint256 indexed pollId, address indexed staker, uint256 indexed vote, uint256 amount); - event PollVoteSubmitted(uint256 indexed pollId, address indexed voter); - event PollVoteRevealed(uint256 indexed pollId, address indexed voter, uint256 indexed vote); - event PollFinalized(uint256 indexed pollId, bytes action, bool executed); - event PollEscalated(uint256 indexed pollId, address escalator, uint256 indexed domainId); - event PollRewardClaimed(uint256 indexed pollId, address indexed staker, uint256 indexed vote, uint256 amount); - event PollEventSet(uint256 indexed pollId, uint256 eventIndex); + event MotionCreated(uint256 indexed motionId, address creator, uint256 indexed domainId); + event MotionStaked(uint256 indexed motionId, address indexed staker, uint256 indexed vote, uint256 amount); + event MotionVoteSubmitted(uint256 indexed motionId, address indexed voter); + event MotionVoteRevealed(uint256 indexed motionId, address indexed voter, uint256 indexed vote); + event MotionFinalized(uint256 indexed motionId, bytes action, bool executed); + event MotionEscalated(uint256 indexed motionId, address escalator, uint256 indexed domainId); + event MotionRewardClaimed(uint256 indexed motionId, address indexed staker, uint256 indexed vote, uint256 amount); + event MotionEventSet(uint256 indexed motionId, uint256 eventIndex); // Constants uint256 constant UINT256_MAX = 2**256 - 1; @@ -69,7 +69,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { // All `Fraction` variables are stored as WADs i.e. fixed-point numbers with 18 digits after the radix. So // 1 WAD = 10**18, which is interpreted as 1. - uint256 totalStakeFraction; // Fraction of the domain's reputation needed to stake on each side in order to go to a poll. + uint256 totalStakeFraction; // Fraction of the domain's reputation needed to stake on each side in order to go to a motion. // This can be set to a maximum of 0.5. uint256 voterRewardFraction; // Fraction of staked tokens paid out to voters as rewards. This will be paid from the staked // tokens of the losing side. This can be set to a maximum of 0.5. @@ -145,7 +145,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { emit ExtensionInitialised(); } - /// @notice Deprecate the extension, prevening new polls from being created + /// @notice Deprecate the extension, prevening new motions from being created function deprecate() public { require(colony.hasUserRole(msg.sender, 1, ColonyDataTypes.ColonyRole.Root), "voting-rep-user-not-root"); @@ -155,10 +155,10 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { } // Data structures - enum PollState { Staking, Submit, Reveal, Closed, Finalizable, Finalized, Failed } + enum MotionState { Staking, Submit, Reveal, Closed, Finalizable, Finalized, Failed } - struct Poll { - uint64[3] events; // For recording poll lifecycle timestamps (STAKE, SUBMIT, REVEAL) + struct Motion { + uint64[3] events; // For recording motion lifecycle timestamps (STAKE, SUBMIT, REVEAL) bytes32 rootHash; uint256 domainId; uint256 skillId; @@ -175,24 +175,24 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { } // Storage - uint256 pollCount; - mapping (uint256 => Poll) polls; + uint256 motionCount; + mapping (uint256 => Motion) motions; mapping (uint256 => mapping (address => mapping (uint256 => uint256))) stakes; mapping (uint256 => mapping (address => bytes32)) voteSecrets; - mapping (bytes32 => uint256) expenditurePastPolls; // expenditure slot signature => voting power - mapping (bytes32 => uint256) expenditurePollCounts; // expenditure struct signature => count + mapping (bytes32 => uint256) expenditurePastMotions; // expenditure slot signature => voting power + mapping (bytes32 => uint256) expenditureMotionCounts; // expenditure struct signature => count // Public functions (interface) - /// @notice Create a poll in the root domain + /// @notice Create a motion in the root domain /// @param _target The contract to which we send the action (0x0 for the colony) /// @param _action A bytes array encoding a function call /// @param _key Reputation tree key for the root domain /// @param _value Reputation tree value for the root domain /// @param _branchMask The branchmask of the proof /// @param _siblings The siblings of the proof - function createRootPoll( + function createRootMotion( address _target, bytes memory _action, bytes memory _key, @@ -203,11 +203,11 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { public { uint256 rootSkillId = colony.getDomain(1).skillId; - createPoll(_target, _action, 1, rootSkillId, _key, _value, _branchMask, _siblings); + createMotion(_target, _action, 1, rootSkillId, _key, _value, _branchMask, _siblings); } - /// @notice Create a poll in any domain - /// @param _domainId The domain where we vote on the poll + /// @notice Create a motion in any domain + /// @param _domainId The domain where we vote on the motion /// @param _childSkillIndex The childSkillIndex pointing to the domain of the action /// @param _target The contract to which we send the action (0x0 for the colony) /// @param _action A bytes array encoding a function call @@ -215,7 +215,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { /// @param _value Reputation tree value for the domain /// @param _branchMask The branchmask of the proof /// @param _siblings The siblings of the proof - function createDomainPoll( + function createDomainMotion( uint256 _domainId, uint256 _childSkillIndex, address _target, @@ -235,21 +235,21 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { require(childSkillId == actionDomainSkillId, "voting-rep-invalid-domain-id"); } - createPoll(_target, _action, _domainId, domainSkillId, _key, _value, _branchMask, _siblings); + createMotion(_target, _action, _domainId, domainSkillId, _key, _value, _branchMask, _siblings); } - /// @notice Stake on a poll - /// @param _pollId The id of the poll + /// @notice Stake on a motion + /// @param _motionId The id of the motion /// @param _permissionDomainId The domain where the extension has the arbitration permission - /// @param _childSkillIndex For the domain in which the poll is occurring + /// @param _childSkillIndex For the domain in which the motion is occurring /// @param _vote The side being supported (0 = NAY, 1 = YAY) /// @param _amount The amount of tokens being staked /// @param _key Reputation tree key for the staker/domain /// @param _value Reputation tree value for the staker/domain /// @param _branchMask The branchmask of the proof /// @param _siblings The siblings of the proof - function stakePoll( - uint256 _pollId, + function stakeMotion( + uint256 _motionId, uint256 _permissionDomainId, uint256 _childSkillIndex, uint256 _vote, @@ -261,81 +261,81 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { ) public { - Poll storage poll = polls[_pollId]; + Motion storage motion = motions[_motionId]; require(_vote <= 1, "voting-rep-bad-vote"); - require(getPollState(_pollId) == PollState.Staking, "voting-rep-staking-closed"); + require(getMotionState(_motionId) == MotionState.Staking, "voting-rep-staking-closed"); - uint256 requiredStake = getRequiredStake(_pollId); - uint256 amount = min(_amount, sub(requiredStake, poll.stakes[_vote])); - uint256 stakerTotalAmount = add(stakes[_pollId][msg.sender][_vote], amount); + uint256 requiredStake = getRequiredStake(_motionId); + uint256 amount = min(_amount, sub(requiredStake, motion.stakes[_vote])); + uint256 stakerTotalAmount = add(stakes[_motionId][msg.sender][_vote], amount); require(amount > 0, "voting-rep-bad-amount"); require( - stakerTotalAmount <= getReputationFromProof(_pollId, msg.sender, _key, _value, _branchMask, _siblings), + stakerTotalAmount <= getReputationFromProof(_motionId, msg.sender, _key, _value, _branchMask, _siblings), "voting-rep-insufficient-rep" ); require( stakerTotalAmount >= wmul(requiredStake, userMinStakeFraction) || - add(poll.stakes[_vote], amount) == requiredStake, // To prevent a residual stake from being un-stakable + add(motion.stakes[_vote], amount) == requiredStake, // To prevent a residual stake from being un-stakable "voting-rep-insufficient-stake" ); - colony.obligateStake(msg.sender, poll.domainId, amount); - colony.transferStake(_permissionDomainId, _childSkillIndex, address(this), msg.sender, poll.domainId, amount, address(this)); + colony.obligateStake(msg.sender, motion.domainId, amount); + colony.transferStake(_permissionDomainId, _childSkillIndex, address(this), msg.sender, motion.domainId, amount, address(this)); // Update the stake - poll.stakes[_vote] = add(poll.stakes[_vote], amount); - stakes[_pollId][msg.sender][_vote] = stakerTotalAmount; + motion.stakes[_vote] = add(motion.stakes[_vote], amount); + stakes[_motionId][msg.sender][_vote] = stakerTotalAmount; // Increment counter & extend claim delay if staking for an expenditure state change if ( _vote == YAY && - poll.stakes[YAY] == requiredStake && - getSig(poll.action) == CHANGE_FUNCTION && - add(poll.votes[NAY], poll.votes[YAY]) == 0 + motion.stakes[YAY] == requiredStake && + getSig(motion.action) == CHANGE_FUNCTION && + add(motion.votes[NAY], motion.votes[YAY]) == 0 ) { - bytes32 structHash = hashExpenditureStruct(poll.action); - expenditurePollCounts[structHash] = add(expenditurePollCounts[structHash], 1); - bytes memory claimDelayAction = createClaimDelayAction(poll.action, UINT256_MAX); - require(executeCall(_pollId, claimDelayAction), "voting-rep-expenditure-lock-failed"); + bytes32 structHash = hashExpenditureStruct(motion.action); + expenditureMotionCounts[structHash] = add(expenditureMotionCounts[structHash], 1); + bytes memory claimDelayAction = createClaimDelayAction(motion.action, UINT256_MAX); + require(executeCall(_motionId, claimDelayAction), "voting-rep-expenditure-lock-failed"); } // Move to second staking window once one side is fully staked if ( - (_vote == YAY && poll.stakes[YAY] == requiredStake && poll.stakes[NAY] < requiredStake) || - (_vote == NAY && poll.stakes[NAY] == requiredStake && poll.stakes[YAY] < requiredStake) + (_vote == YAY && motion.stakes[YAY] == requiredStake && motion.stakes[NAY] < requiredStake) || + (_vote == NAY && motion.stakes[NAY] == requiredStake && motion.stakes[YAY] < requiredStake) ) { - poll.events[STAKE_END] = uint64(now + stakePeriod); - poll.events[SUBMIT_END] = uint64(now + stakePeriod + submitPeriod); - poll.events[REVEAL_END] = uint64(now + stakePeriod + submitPeriod + revealPeriod); - delete poll.votes; // New stake supersedes prior votes + motion.events[STAKE_END] = uint64(now + stakePeriod); + motion.events[SUBMIT_END] = uint64(now + stakePeriod + submitPeriod); + motion.events[REVEAL_END] = uint64(now + stakePeriod + submitPeriod + revealPeriod); + delete motion.votes; // New stake supersedes prior votes - emit PollEventSet(_pollId, STAKE_END); + emit MotionEventSet(_motionId, STAKE_END); } // Claim tokens once both sides are fully staked - if (poll.stakes[YAY] == requiredStake && poll.stakes[NAY] == requiredStake) { - poll.events[STAKE_END] = uint64(now); - poll.events[SUBMIT_END] = uint64(now + submitPeriod); - poll.events[REVEAL_END] = uint64(now + submitPeriod + revealPeriod); + if (motion.stakes[YAY] == requiredStake && motion.stakes[NAY] == requiredStake) { + motion.events[STAKE_END] = uint64(now); + motion.events[SUBMIT_END] = uint64(now + submitPeriod); + motion.events[REVEAL_END] = uint64(now + submitPeriod + revealPeriod); tokenLocking.claim(token, true); - emit PollEventSet(_pollId, STAKE_END); + emit MotionEventSet(_motionId, STAKE_END); } - emit PollStaked(_pollId, msg.sender, _vote, amount); + emit MotionStaked(_motionId, msg.sender, _vote, amount); } - /// @notice Submit a vote secret for a poll - /// @param _pollId The id of the poll + /// @notice Submit a vote secret for a motion + /// @param _motionId The id of the motion /// @param _voteSecret The hashed vote secret /// @param _key Reputation tree key for the staker/domain /// @param _value Reputation tree value for the staker/domain /// @param _branchMask The branchmask of the proof /// @param _siblings The siblings of the proof function submitVote( - uint256 _pollId, + uint256 _motionId, bytes32 _voteSecret, bytes memory _key, bytes memory _value, @@ -344,31 +344,31 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { ) public { - Poll storage poll = polls[_pollId]; - require(getPollState(_pollId) == PollState.Submit, "voting-rep-poll-not-open"); + Motion storage motion = motions[_motionId]; + require(getMotionState(_motionId) == MotionState.Submit, "voting-rep-motion-not-open"); require(_voteSecret != bytes32(0), "voting-rep-invalid-secret"); - uint256 userRep = getReputationFromProof(_pollId, msg.sender, _key, _value, _branchMask, _siblings); + uint256 userRep = getReputationFromProof(_motionId, msg.sender, _key, _value, _branchMask, _siblings); // Count reputation if first submission - if (voteSecrets[_pollId][msg.sender] == bytes32(0)) { - poll.repSubmitted = add(poll.repSubmitted, userRep); + if (voteSecrets[_motionId][msg.sender] == bytes32(0)) { + motion.repSubmitted = add(motion.repSubmitted, userRep); } - voteSecrets[_pollId][msg.sender] = _voteSecret; + voteSecrets[_motionId][msg.sender] = _voteSecret; - emit PollVoteSubmitted(_pollId, msg.sender); + emit MotionVoteSubmitted(_motionId, msg.sender); - if (poll.repSubmitted >= wmul(poll.skillRep, maxVoteFraction)) { - poll.events[SUBMIT_END] = uint64(now); - poll.events[REVEAL_END] = uint64(now + revealPeriod); + if (motion.repSubmitted >= wmul(motion.skillRep, maxVoteFraction)) { + motion.events[SUBMIT_END] = uint64(now); + motion.events[REVEAL_END] = uint64(now + revealPeriod); - emit PollEventSet(_pollId, SUBMIT_END); + emit MotionEventSet(_motionId, SUBMIT_END); } } - /// @notice Reveal a vote secret for a poll - /// @param _pollId The id of the poll + /// @notice Reveal a vote secret for a motion + /// @param _motionId The id of the motion /// @param _salt The salt used to hash the vote /// @param _vote The side being supported (0 = NAY, 1 = YAY) /// @param _key Reputation tree key for the staker/domain @@ -376,7 +376,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { /// @param _branchMask The branchmask of the proof /// @param _siblings The siblings of the proof function revealVote( - uint256 _pollId, + uint256 _motionId, bytes32 _salt, uint256 _vote, bytes memory _key, @@ -386,45 +386,45 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { ) public { - Poll storage poll = polls[_pollId]; - require(getPollState(_pollId) == PollState.Reveal, "voting-rep-poll-not-reveal"); + Motion storage motion = motions[_motionId]; + require(getMotionState(_motionId) == MotionState.Reveal, "voting-rep-motion-not-reveal"); - uint256 userRep = getReputationFromProof(_pollId, msg.sender, _key, _value, _branchMask, _siblings); + uint256 userRep = getReputationFromProof(_motionId, msg.sender, _key, _value, _branchMask, _siblings); - bytes32 voteSecret = voteSecrets[_pollId][msg.sender]; + bytes32 voteSecret = voteSecrets[_motionId][msg.sender]; require(voteSecret == getVoteSecret(_salt, _vote), "voting-rep-secret-no-match"); - delete voteSecrets[_pollId][msg.sender]; + delete voteSecrets[_motionId][msg.sender]; - poll.votes[_vote] = add(poll.votes[_vote], userRep); - poll.repRevealed = add(poll.repRevealed, userRep); + motion.votes[_vote] = add(motion.votes[_vote], userRep); + motion.repRevealed = add(motion.repRevealed, userRep); - uint256 fractionUserReputation = wdiv(userRep, poll.skillRep); - uint256 totalStake = add(poll.stakes[YAY], poll.stakes[NAY]); + uint256 fractionUserReputation = wdiv(userRep, motion.skillRep); + uint256 totalStake = add(motion.stakes[YAY], motion.stakes[NAY]); uint256 voterReward = wmul(wmul(fractionUserReputation, totalStake), voterRewardFraction); - poll.paidVoterComp = add(poll.paidVoterComp, voterReward); + motion.paidVoterComp = add(motion.paidVoterComp, voterReward); tokenLocking.transfer(token, voterReward, msg.sender, true); - emit PollVoteRevealed(_pollId, msg.sender, _vote); + emit MotionVoteRevealed(_motionId, msg.sender, _vote); - if (poll.repRevealed == poll.repSubmitted) { - poll.events[REVEAL_END] = uint64(now); + if (motion.repRevealed == motion.repSubmitted) { + motion.events[REVEAL_END] = uint64(now); - emit PollEventSet(_pollId, REVEAL_END); + emit MotionEventSet(_motionId, REVEAL_END); } } - /// @notice Escalate a poll to a higher domain - /// @param _pollId The id of the poll + /// @notice Escalate a motion to a higher domain + /// @param _motionId The id of the motion /// @param _newDomainId The desired domain of escalation /// @param _childSkillIndex For the current domain, relative to the escalated domain /// @param _key Reputation tree key for the new domain /// @param _value Reputation tree value for the new domain /// @param _branchMask The branchmask of the proof /// @param _siblings The siblings of the proof - function escalatePoll( - uint256 _pollId, + function escalateMotion( + uint256 _motionId, uint256 _newDomainId, uint256 _childSkillIndex, bytes memory _key, @@ -434,56 +434,56 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { ) public { - Poll storage poll = polls[_pollId]; - require(getPollState(_pollId) == PollState.Closed, "voting-rep-poll-not-closed"); + Motion storage motion = motions[_motionId]; + require(getMotionState(_motionId) == MotionState.Closed, "voting-rep-motion-not-closed"); uint256 newDomainSkillId = colony.getDomain(_newDomainId).skillId; uint256 childSkillId = colonyNetwork.getChildSkillId(newDomainSkillId, _childSkillIndex); - require(childSkillId == poll.skillId, "voting-rep-invalid-domain-proof"); + require(childSkillId == motion.skillId, "voting-rep-invalid-domain-proof"); - poll.events[STAKE_END] = uint64(now + stakePeriod); - poll.events[SUBMIT_END] = uint64(now + stakePeriod + submitPeriod); - poll.events[REVEAL_END] = uint64(now + stakePeriod + submitPeriod + revealPeriod); + motion.events[STAKE_END] = uint64(now + stakePeriod); + motion.events[SUBMIT_END] = uint64(now + stakePeriod + submitPeriod); + motion.events[REVEAL_END] = uint64(now + stakePeriod + submitPeriod + revealPeriod); - poll.domainId = _newDomainId; - poll.skillId = newDomainSkillId; - poll.skillRep = getReputationFromProof(_pollId, address(0x0), _key, _value, _branchMask, _siblings); + motion.domainId = _newDomainId; + motion.skillId = newDomainSkillId; + motion.skillRep = getReputationFromProof(_motionId, address(0x0), _key, _value, _branchMask, _siblings); - uint256 loser = (poll.votes[NAY] < poll.votes[YAY]) ? NAY : YAY; - poll.stakes[loser] = sub(poll.stakes[loser], poll.paidVoterComp); - poll.pastVoterComp[loser] = add(poll.pastVoterComp[loser], poll.paidVoterComp); - delete poll.paidVoterComp; + uint256 loser = (motion.votes[NAY] < motion.votes[YAY]) ? NAY : YAY; + motion.stakes[loser] = sub(motion.stakes[loser], motion.paidVoterComp); + motion.pastVoterComp[loser] = add(motion.pastVoterComp[loser], motion.paidVoterComp); + delete motion.paidVoterComp; - emit PollEscalated(_pollId, msg.sender, _newDomainId); + emit MotionEscalated(_motionId, msg.sender, _newDomainId); } - function finalizePoll(uint256 _pollId) public { - Poll storage poll = polls[_pollId]; - require(getPollState(_pollId) == PollState.Finalizable, "voting-rep-poll-not-executable"); + function finalizeMotion(uint256 _motionId) public { + Motion storage motion = motions[_motionId]; + require(getMotionState(_motionId) == MotionState.Finalizable, "voting-rep-motion-not-executable"); - poll.finalized = true; + motion.finalized = true; bool canExecute = ( - poll.stakes[NAY] <= poll.stakes[YAY] && - poll.votes[NAY] <= poll.votes[YAY] + motion.stakes[NAY] <= motion.stakes[YAY] && + motion.votes[NAY] <= motion.votes[YAY] ); - if (getSig(poll.action) == CHANGE_FUNCTION) { - bytes32 structHash = hashExpenditureStruct(poll.action); - expenditurePollCounts[structHash] = sub(expenditurePollCounts[structHash], 1); + if (getSig(motion.action) == CHANGE_FUNCTION) { + bytes32 structHash = hashExpenditureStruct(motion.action); + expenditureMotionCounts[structHash] = sub(expenditureMotionCounts[structHash], 1); - // Release the claimDelay if this is the last active poll - if (expenditurePollCounts[structHash] == 0) { - bytes memory claimDelayAction = createClaimDelayAction(poll.action, 0); - require(executeCall(_pollId, claimDelayAction), "voting-rep-expenditure-unlock-failed"); + // Release the claimDelay if this is the last active motion + if (expenditureMotionCounts[structHash] == 0) { + bytes memory claimDelayAction = createClaimDelayAction(motion.action, 0); + require(executeCall(_motionId, claimDelayAction), "voting-rep-expenditure-unlock-failed"); } - bytes32 slotHash = hashExpenditureSlot(poll.action); - uint256 votePower = (add(poll.votes[NAY], poll.votes[YAY]) > 0) ? - poll.votes[YAY] : poll.stakes[YAY]; + bytes32 slotHash = hashExpenditureSlot(motion.action); + uint256 votePower = (add(motion.votes[NAY], motion.votes[YAY]) > 0) ? + motion.votes[YAY] : motion.stakes[YAY]; - if (expenditurePastPolls[slotHash] < votePower) { - expenditurePastPolls[slotHash] = votePower; + if (expenditurePastMotions[slotHash] < votePower) { + expenditurePastMotions[slotHash] = votePower; canExecute = canExecute && true; } else { canExecute = canExecute && false; @@ -493,20 +493,20 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { bool executed; if (canExecute) { - executed = executeCall(_pollId, poll.action); + executed = executeCall(_motionId, motion.action); } - emit PollFinalized(_pollId, poll.action, executed); + emit MotionFinalized(_motionId, motion.action, executed); } /// @notice Claim the staker's reward - /// @param _pollId The id of the poll + /// @param _motionId The id of the motion /// @param _permissionDomainId The domain where the extension has the arbitration permission - /// @param _childSkillIndex For the domain in which the poll is occurring + /// @param _childSkillIndex For the domain in which the motion is occurring /// @param _user The user whose reward is being claimed /// @param _vote The side being supported (0 = NAY, 1 = YAY) function claimReward( - uint256 _pollId, + uint256 _motionId, uint256 _permissionDomainId, uint256 _childSkillIndex, address _user, @@ -514,33 +514,33 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { ) public { - Poll storage poll = polls[_pollId]; + Motion storage motion = motions[_motionId]; require( - getPollState(_pollId) == PollState.Finalized || - getPollState(_pollId) == PollState.Failed, + getMotionState(_motionId) == MotionState.Finalized || + getMotionState(_motionId) == MotionState.Failed, "voting-rep-not-failed-or-finalized" ); uint256 stakeFraction = wdiv( - stakes[_pollId][_user][_vote], - add(poll.stakes[_vote], poll.pastVoterComp[_vote]) + stakes[_motionId][_user][_vote], + add(motion.stakes[_vote], motion.pastVoterComp[_vote]) ); - delete stakes[_pollId][_user][_vote]; + delete stakes[_motionId][_user][_vote]; - uint256 realStake = wmul(stakeFraction, poll.stakes[_vote]); - uint256 requiredStake = getRequiredStake(_pollId); + uint256 realStake = wmul(stakeFraction, motion.stakes[_vote]); + uint256 requiredStake = getRequiredStake(_motionId); uint256 stakerReward; uint256 repPenalty; // Went to a vote, use vote to determine reward or penalty - if (add(poll.votes[NAY], poll.votes[YAY]) > 0) { - assert(poll.stakes[NAY] == requiredStake && poll.stakes[YAY] == requiredStake); + if (add(motion.votes[NAY], motion.votes[YAY]) > 0) { + assert(motion.stakes[NAY] == requiredStake && motion.stakes[YAY] == requiredStake); - uint256 loserStake = sub(requiredStake, poll.paidVoterComp); - uint256 totalVotes = add(poll.votes[NAY], poll.votes[YAY]); - uint256 winFraction = wdiv(poll.votes[_vote], totalVotes); + uint256 loserStake = sub(requiredStake, motion.paidVoterComp); + uint256 totalVotes = add(motion.votes[NAY], motion.votes[YAY]); + uint256 winFraction = wdiv(motion.votes[_vote], totalVotes); uint256 winShare = wmul(winFraction, 2 * WAD); // On a scale of 0-2 WAD if (winShare > WAD || (winShare == WAD && _vote == NAY)) { @@ -551,17 +551,17 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { } // Your side fully staked, receive 10% (proportional) of loser's stake - } else if (poll.stakes[_vote] == requiredStake) { + } else if (motion.stakes[_vote] == requiredStake) { - uint256 loserStake = sub(poll.stakes[flip(_vote)], poll.paidVoterComp); + uint256 loserStake = sub(motion.stakes[flip(_vote)], motion.paidVoterComp); uint256 totalPenalty = wmul(loserStake, WAD / 10); stakerReward = wmul(stakeFraction, add(requiredStake, totalPenalty)); // Opponent's side fully staked, pay 10% penalty - } else if (poll.stakes[flip(_vote)] == requiredStake) { + } else if (motion.stakes[flip(_vote)] == requiredStake) { - uint256 loserStake = sub(poll.stakes[_vote], poll.paidVoterComp); + uint256 loserStake = sub(motion.stakes[_vote], motion.paidVoterComp); uint256 totalPenalty = wmul(loserStake, WAD / 10); stakerReward = wmul(stakeFraction, sub(loserStake, totalPenalty)); @@ -570,8 +570,8 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { // Neither side fully staked, no reward or penalty } else { - uint256 totalStake = add(poll.stakes[NAY], poll.stakes[YAY]); - uint256 rewardShare = wdiv(sub(totalStake, poll.paidVoterComp), totalStake); + uint256 totalStake = add(motion.stakes[NAY], motion.stakes[YAY]); + uint256 rewardShare = wdiv(sub(totalStake, motion.paidVoterComp), totalStake); stakerReward = wmul(realStake, rewardShare); } @@ -581,95 +581,95 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { colony.emitDomainReputationPenalty( _permissionDomainId, _childSkillIndex, - poll.domainId, + motion.domainId, _user, -int256(repPenalty) ); } - emit PollRewardClaimed(_pollId, _user, _vote, stakerReward); + emit MotionRewardClaimed(_motionId, _user, _vote, stakerReward); } // Public view functions - /// @notice Get the total poll count - /// @return The total poll count - function getPollCount() public view returns (uint256) { - return pollCount; + /// @notice Get the total motion count + /// @return The total motion count + function getMotionCount() public view returns (uint256) { + return motionCount; } - /// @notice Get the data for a single poll - /// @param _pollId The id of the poll - /// @return poll The poll struct - function getPoll(uint256 _pollId) public view returns (Poll memory poll) { - poll = polls[_pollId]; + /// @notice Get the data for a single motion + /// @param _motionId The id of the motion + /// @return motion The motion struct + function getMotion(uint256 _motionId) public view returns (Motion memory motion) { + motion = motions[_motionId]; } - /// @notice Get a user's stake on a poll - /// @param _pollId The id of the poll + /// @notice Get a user's stake on a motion + /// @param _motionId The id of the motion /// @param _staker The staker address /// @param _vote The side being supported (0 = NAY, 1 = YAY) /// @return The user's stake - function getStake(uint256 _pollId, address _staker, uint256 _vote) public view returns (uint256) { - return stakes[_pollId][_staker][_vote]; + function getStake(uint256 _motionId, address _staker, uint256 _vote) public view returns (uint256) { + return stakes[_motionId][_staker][_vote]; } - /// @notice Get the number of ongoing polls for a single expenditure / slot + /// @notice Get the number of ongoing motions for a single expenditure / slot /// @param _structHash The hash of the expenditureId or expenditureId*expenditureSlot - /// @return The number of ongoing polls - function getExpenditurePollCount(bytes32 _structHash) public view returns (uint256) { - return expenditurePollCounts[_structHash]; + /// @return The number of ongoing motions + function getExpenditureMotionCount(bytes32 _structHash) public view returns (uint256) { + return expenditureMotionCounts[_structHash]; } /// @notice Get the largest past vote on a single expenditure variable /// @param _slotHash The hash of the particular expenditure slot /// @return The largest past vote on this variable - function getExpenditurePastPoll(bytes32 _slotHash) public view returns (uint256) { - return expenditurePastPolls[_slotHash]; + function getExpenditurePastMotion(bytes32 _slotHash) public view returns (uint256) { + return expenditurePastMotions[_slotHash]; } - /// @notice Get the current state of the poll - /// @return The current poll state - function getPollState(uint256 _pollId) public view returns (PollState) { - Poll storage poll = polls[_pollId]; - uint256 requiredStake = getRequiredStake(_pollId); + /// @notice Get the current state of the motion + /// @return The current motion state + function getMotionState(uint256 _motionId) public view returns (MotionState) { + Motion storage motion = motions[_motionId]; + uint256 requiredStake = getRequiredStake(_motionId); // If finalized, we're done - if (poll.finalized) { + if (motion.finalized) { - return PollState.Finalized; + return MotionState.Finalized; // Not fully staked } else if ( - poll.stakes[YAY] < requiredStake || - poll.stakes[NAY] < requiredStake + motion.stakes[YAY] < requiredStake || + motion.stakes[NAY] < requiredStake ) { // Are we still staking? - if (now < poll.events[STAKE_END]) { - return PollState.Staking; + if (now < motion.events[STAKE_END]) { + return MotionState.Staking; // If not, did the YAY side stake? - } else if (poll.stakes[YAY] == requiredStake) { - return PollState.Finalizable; + } else if (motion.stakes[YAY] == requiredStake) { + return MotionState.Finalizable; // If not, was there a prior vote we can fall back on? - } else if (poll.votes[NAY] > 0 || poll.votes[YAY] > 0) { - return PollState.Finalizable; - // Otherwise, the poll failed + } else if (motion.votes[NAY] > 0 || motion.votes[YAY] > 0) { + return MotionState.Finalizable; + // Otherwise, the motion failed } else { - return PollState.Failed; + return MotionState.Failed; } // Fully staked, go to a vote } else { - if (now < poll.events[SUBMIT_END]) { - return PollState.Submit; - } else if (now < poll.events[REVEAL_END]) { - return PollState.Reveal; - } else if (now < poll.events[REVEAL_END] + escalationPeriod) { - return PollState.Closed; + if (now < motion.events[SUBMIT_END]) { + return MotionState.Submit; + } else if (now < motion.events[REVEAL_END]) { + return MotionState.Reveal; + } else if (now < motion.events[REVEAL_END] + escalationPeriod) { + return MotionState.Closed; } else { - return PollState.Finalizable; + return MotionState.Finalizable; } } @@ -677,7 +677,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { // Internal functions - function createPoll( + function createMotion( address _target, bytes memory _action, uint256 _domainId, @@ -691,28 +691,31 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { { require(state == ExtensionState.Active, "voting-rep-not-active"); - pollCount += 1; + motionCount += 1; - polls[pollCount].events[STAKE_END] = uint64(now + stakePeriod); - polls[pollCount].events[SUBMIT_END] = uint64(now + stakePeriod + submitPeriod); - polls[pollCount].events[REVEAL_END] = uint64(now + stakePeriod + submitPeriod + revealPeriod); - polls[pollCount].rootHash = colonyNetwork.getReputationRootHash(); - polls[pollCount].domainId = _domainId; - polls[pollCount].skillId = _skillId; - polls[pollCount].skillRep = getReputationFromProof(pollCount, address(0x0), _key, _value, _branchMask, _siblings); - polls[pollCount].target = _target; - polls[pollCount].action = _action; + motions[motionCount].events[STAKE_END] = uint64(now + stakePeriod); + motions[motionCount].events[SUBMIT_END] = uint64(now + stakePeriod + submitPeriod); + motions[motionCount].events[REVEAL_END] = uint64(now + stakePeriod + submitPeriod + revealPeriod); - emit PollCreated(pollCount, msg.sender, _domainId); + motions[motionCount].rootHash = colonyNetwork.getReputationRootHash(); + motions[motionCount].domainId = _domainId; + motions[motionCount].skillId = _skillId; + + uint256 skillRep = getReputationFromProof(motionCount, address(0x0), _key, _value, _branchMask, _siblings); + motions[motionCount].skillRep = skillRep; + motions[motionCount].target = _target; + motions[motionCount].action = _action; + + emit MotionCreated(motionCount, msg.sender, _domainId); } function getVoteSecret(bytes32 _salt, uint256 _vote) internal pure returns (bytes32) { return keccak256(abi.encodePacked(_salt, _vote)); } - function getRequiredStake(uint256 _pollId) internal view returns (uint256) { - return wmul(polls[_pollId].skillRep, totalStakeFraction); + function getRequiredStake(uint256 _motionId) internal view returns (uint256) { + return wmul(motions[_motionId].skillRep, totalStakeFraction); } function flip(uint256 _vote) internal pure returns (uint256) { @@ -720,7 +723,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { } function getReputationFromProof( - uint256 _pollId, + uint256 _motionId, address _who, bytes memory _key, bytes memory _value, @@ -730,7 +733,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { internal view returns (uint256) { bytes32 impliedRoot = getImpliedRootHashKey(_key, _value, _branchMask, _siblings); - require(polls[_pollId].rootHash == impliedRoot, "voting-rep-invalid-root-hash"); + require(motions[_motionId].rootHash == impliedRoot, "voting-rep-invalid-root-hash"); uint256 reputationValue; address keyColonyAddress; @@ -745,7 +748,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { } require(keyColonyAddress == address(colony), "voting-rep-invalid-colony-address"); - require(keySkill == polls[_pollId].skillId, "voting-rep-invalid-skill-id"); + require(keySkill == motions[_motionId].skillId, "voting-rep-invalid-skill-id"); require(keyUserAddress == _who, "voting-rep-invalid-user-address"); return reputationValue; @@ -769,8 +772,8 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { } } - function executeCall(uint256 pollId, bytes memory action) internal returns (bool success) { - address target = polls[pollId].target; + function executeCall(uint256 motionId, bytes memory action) internal returns (bool success) { + address target = motions[motionId].target; address to = (target == address(0x0)) ? address(colony) : target; assembly { diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index a3ff589240..54812c376a 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -228,16 +228,16 @@ contract("Voting Reputation", (accounts) => { it("can deprecate the extension if root", async () => { const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); - await voting.createRootPoll(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + await voting.createRootMotion(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); // Must be root await checkErrorRevert(voting.deprecate({ from: USER2 }), "voting-rep-user-not-root"); await voting.deprecate(); - // Cant make new polls! + // Cant make new motions! await checkErrorRevert( - voting.createRootPoll(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings), + voting.createRootMotion(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings), "voting-rep-not-active" ); }); @@ -266,95 +266,97 @@ contract("Voting Reputation", (accounts) => { }); }); - describe("creating polls", async () => { - it("can create a root poll", async () => { + describe("creating motions", async () => { + it("can create a root motion", async () => { const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); - await voting.createRootPoll(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + await voting.createRootMotion(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); - const pollId = await voting.getPollCount(); - const poll = await voting.getPoll(pollId); - expect(poll.skillId).to.eq.BN(domain1.skillId); + const motionId = await voting.getMotionCount(); + const motion = await voting.getMotion(motionId); + expect(motion.skillId).to.eq.BN(domain1.skillId); }); - it("can create a domain poll in the root domain", async () => { - // Create poll in domain of action (1) + it("can create a domain motion in the root domain", async () => { + // Create motion in domain of action (1) const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); - await voting.createDomainPoll(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + await voting.createDomainMotion(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); - const pollId = await voting.getPollCount(); - const poll = await voting.getPoll(pollId); - expect(poll.skillId).to.eq.BN(domain1.skillId); + const motionId = await voting.getMotionCount(); + const motion = await voting.getMotion(motionId); + expect(motion.skillId).to.eq.BN(domain1.skillId); }); - it("can create a domain poll in a child domain", async () => { + it("can create a domain motion in a child domain", async () => { const key = makeReputationKey(colony.address, domain2.skillId); const value = makeReputationValue(WAD, 6); const [mask, siblings] = await reputationTree.getProof(key); - // Create poll in domain of action (2) + // Create motion in domain of action (2) const action = await encodeTxData(colony, "makeTask", [1, 0, FAKE, 2, 0, 0]); - await voting.createDomainPoll(2, UINT256_MAX, ADDRESS_ZERO, action, key, value, mask, siblings); + await voting.createDomainMotion(2, UINT256_MAX, ADDRESS_ZERO, action, key, value, mask, siblings); - const pollId = await voting.getPollCount(); - const poll = await voting.getPoll(pollId); - expect(poll.skillId).to.eq.BN(domain2.skillId); + const motionId = await voting.getMotionCount(); + const motion = await voting.getMotion(motionId); + expect(motion.skillId).to.eq.BN(domain2.skillId); }); - it("can externally escalate a domain poll", async () => { - // Create poll in parent domain (1) of action (2) + it("can externally escalate a domain motion", async () => { + // Create motion in parent domain (1) of action (2) const action = await encodeTxData(colony, "makeTask", [1, 0, FAKE, 2, 0, 0]); - await voting.createDomainPoll(1, 0, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + await voting.createDomainMotion(1, 0, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); - const pollId = await voting.getPollCount(); - const poll = await voting.getPoll(pollId); - expect(poll.skillId).to.eq.BN(domain1.skillId); + const motionId = await voting.getMotionCount(); + const motion = await voting.getMotion(motionId); + expect(motion.skillId).to.eq.BN(domain1.skillId); }); - it("cannot externally escalate a domain poll with an invalid domain proof", async () => { + it("cannot externally escalate a domain motion with an invalid domain proof", async () => { const key = makeReputationKey(colony.address, domain3.skillId); const value = makeReputationValue(WAD, 7); const [mask, siblings] = await reputationTree.getProof(key); // Provide proof for (3) instead of (2) const action = await encodeTxData(colony, "makeTask", [1, 0, FAKE, 2, 0, 0]); - await checkErrorRevert(voting.createDomainPoll(1, 1, ADDRESS_ZERO, action, key, value, mask, siblings), "voting-rep-invalid-domain-id"); + await checkErrorRevert(voting.createDomainMotion(1, 1, ADDRESS_ZERO, action, key, value, mask, siblings), "voting-rep-invalid-domain-id"); }); }); - describe("staking on polls", async () => { - let pollId; + describe("staking on motions", async () => { + let motionId; beforeEach(async () => { const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); - await voting.createRootPoll(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); - pollId = await voting.getPollCount(); + await voting.createRootMotion(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + motionId = await voting.getMotionCount(); }); - it("can stake on a poll", async () => { - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE.divn(2), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE.divn(2), user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + it("can stake on a motion", async () => { + const half = REQUIRED_STAKE.divn(2); - const poll = await voting.getPoll(pollId); - expect(poll.stakes[0]).to.be.zero; - expect(poll.stakes[1]).to.eq.BN(REQUIRED_STAKE); + await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, half, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, half, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - const stake0 = await voting.getStake(pollId, USER0, YAY); - const stake1 = await voting.getStake(pollId, USER1, YAY); - expect(stake0).to.eq.BN(REQUIRED_STAKE.divn(2)); - expect(stake1).to.eq.BN(REQUIRED_STAKE.divn(2)); + const motion = await voting.getMotion(motionId); + expect(motion.stakes[0]).to.be.zero; + expect(motion.stakes[1]).to.eq.BN(REQUIRED_STAKE); + + const stake0 = await voting.getStake(motionId, USER0, YAY); + const stake1 = await voting.getStake(motionId, USER1, YAY); + expect(stake0).to.eq.BN(half); + expect(stake1).to.eq.BN(half); }); - it("can update the poll states correctly", async () => { - let pollState = await voting.getPollState(pollId); - expect(pollState).to.eq.BN(STAKING); + it("can update the motion states correctly", async () => { + let motionState = await voting.getMotionState(motionId); + expect(motionState).to.eq.BN(STAKING); - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - pollState = await voting.getPollState(pollId); - expect(pollState).to.eq.BN(STAKING); + await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + motionState = await voting.getMotionState(motionId); + expect(motionState).to.eq.BN(STAKING); - await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - pollState = await voting.getPollState(pollId); - expect(pollState).to.eq.BN(SUBMIT); + await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + motionState = await voting.getMotionState(motionId); + expect(motionState).to.eq.BN(SUBMIT); }); it("can stake even with a locked token", async () => { @@ -363,8 +365,8 @@ contract("Voting Reputation", (accounts) => { await colony.claimColonyFunds(token.address); await colony.startNextRewardPayout(token.address, domain1Key, domain1Value, domain1Mask, domain1Siblings); - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); const lock = await tokenLocking.getUserLock(token.address, voting.address); expect(lock.balance).to.eq.BN(REQUIRED_STAKE.muln(2)); @@ -372,14 +374,14 @@ contract("Voting Reputation", (accounts) => { it("cannot stake 0", async () => { await checkErrorRevert( - voting.stakePoll(pollId, 1, UINT256_MAX, YAY, 0, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), + voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, 0, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), "voting-rep-bad-amount" ); }); it("cannot stake a nonexistent side", async () => { await checkErrorRevert( - voting.stakePoll(pollId, 1, UINT256_MAX, 2, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), + voting.stakeMotion(motionId, 1, UINT256_MAX, 2, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), "voting-rep-bad-vote" ); }); @@ -388,17 +390,17 @@ contract("Voting Reputation", (accounts) => { const minStake = REQUIRED_STAKE.divn(10); await checkErrorRevert( - voting.stakePoll(pollId, 1, UINT256_MAX, YAY, minStake.subn(1), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), + voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, minStake.subn(1), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), "voting-rep-insufficient-stake" ); - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, minStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, minStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); // Unless there's less than the minStake to go! const stake = REQUIRED_STAKE.sub(minStake.muln(2)).addn(1); - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, stake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, minStake.subn(1), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, stake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, minStake.subn(1), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); }); it("can update the expenditure globalClaimDelay if voting on expenditure state", async () => { @@ -408,21 +410,21 @@ contract("Voting Reputation", (accounts) => { // Set payoutModifier to 1 for expenditure slot 0 const action = await encodeTxData(colony, "setExpenditureState", [1, UINT256_MAX, expenditureId, 25, [true], [bn2bytes32(new BN(3))], WAD32]); - await voting.createDomainPoll(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); - pollId = await voting.getPollCount(); + await voting.createDomainMotion(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + motionId = await voting.getMotionCount(); - let expenditurePollCount; - expenditurePollCount = await voting.getExpenditurePollCount(soliditySha3(expenditureId)); - expect(expenditurePollCount).to.be.zero; + let expenditureMotionCount; + expenditureMotionCount = await voting.getExpenditureMotionCount(soliditySha3(expenditureId)); + expect(expenditureMotionCount).to.be.zero; let expenditureSlot; expenditureSlot = await colony.getExpenditure(expenditureId); expect(expenditureSlot.globalClaimDelay).to.be.zero; - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - expenditurePollCount = await voting.getExpenditurePollCount(soliditySha3(expenditureId)); - expect(expenditurePollCount).to.eq.BN(1); + expenditureMotionCount = await voting.getExpenditureMotionCount(soliditySha3(expenditureId)); + expect(expenditureMotionCount).to.eq.BN(1); expenditureSlot = await colony.getExpenditure(expenditureId); expect(expenditureSlot.globalClaimDelay).to.eq.BN(UINT256_MAX); @@ -443,21 +445,21 @@ contract("Voting Reputation", (accounts) => { WAD32, ]); - await voting.createDomainPoll(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); - pollId = await voting.getPollCount(); + await voting.createDomainMotion(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + motionId = await voting.getMotionCount(); - let expenditurePollCount; - expenditurePollCount = await voting.getExpenditurePollCount(soliditySha3(expenditureId, 0)); - expect(expenditurePollCount).to.be.zero; + let expenditureMotionCount; + expenditureMotionCount = await voting.getExpenditureMotionCount(soliditySha3(expenditureId, 0)); + expect(expenditureMotionCount).to.be.zero; let expenditureSlot; expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); expect(expenditureSlot.claimDelay).to.be.zero; - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - expenditurePollCount = await voting.getExpenditurePollCount(soliditySha3(expenditureId, 0)); - expect(expenditurePollCount).to.eq.BN(1); + expenditureMotionCount = await voting.getExpenditureMotionCount(soliditySha3(expenditureId, 0)); + expect(expenditureMotionCount).to.eq.BN(1); expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); expect(expenditureSlot.claimDelay).to.eq.BN(UINT256_MAX); @@ -478,21 +480,21 @@ contract("Voting Reputation", (accounts) => { WAD32, ]); - await voting.createDomainPoll(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); - pollId = await voting.getPollCount(); + await voting.createDomainMotion(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + motionId = await voting.getMotionCount(); - let expenditurePollCount; - expenditurePollCount = await voting.getExpenditurePollCount(soliditySha3(expenditureId, 0)); - expect(expenditurePollCount).to.be.zero; + let expenditureMotionCount; + expenditureMotionCount = await voting.getExpenditureMotionCount(soliditySha3(expenditureId, 0)); + expect(expenditureMotionCount).to.be.zero; let expenditureSlot; expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); expect(expenditureSlot.claimDelay).to.be.zero; - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - expenditurePollCount = await voting.getExpenditurePollCount(soliditySha3(expenditureId, 0)); - expect(expenditurePollCount).to.eq.BN(1); + expenditureMotionCount = await voting.getExpenditureMotionCount(soliditySha3(expenditureId, 0)); + expect(expenditureMotionCount).to.eq.BN(1); expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); expect(expenditureSlot.claimDelay).to.eq.BN(UINT256_MAX); @@ -502,16 +504,16 @@ contract("Voting Reputation", (accounts) => { // Create a poorly-formed action (no keys) const action = await encodeTxData(colony, "setExpenditureState", [1, UINT256_MAX, 1, 0, [], [], ethers.constants.HashZero]); - await voting.createDomainPoll(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); - pollId = await voting.getPollCount(); + await voting.createDomainMotion(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + motionId = await voting.getMotionCount(); await checkErrorRevert( - voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), + voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), "voting-rep-expenditure-lock-failed" ); }); - it("can accurately track the number of polls for a single expenditure", async () => { + it("can accurately track the number of motions for a single expenditure", async () => { await colony.makeExpenditure(1, UINT256_MAX, 1); const expenditureId = await colony.getExpenditureCount(); const expenditureHash = soliditySha3(expenditureId, 0); @@ -538,42 +540,42 @@ contract("Voting Reputation", (accounts) => { WAD32, ]); - await voting.createDomainPoll(1, UINT256_MAX, ADDRESS_ZERO, action1, domain1Key, domain1Value, domain1Mask, domain1Siblings); - const pollId1 = await voting.getPollCount(); + await voting.createDomainMotion(1, UINT256_MAX, ADDRESS_ZERO, action1, domain1Key, domain1Value, domain1Mask, domain1Siblings); + const motionId1 = await voting.getMotionCount(); - await voting.createDomainPoll(1, UINT256_MAX, ADDRESS_ZERO, action2, domain1Key, domain1Value, domain1Mask, domain1Siblings); - const pollId2 = await voting.getPollCount(); + await voting.createDomainMotion(1, UINT256_MAX, ADDRESS_ZERO, action2, domain1Key, domain1Value, domain1Mask, domain1Siblings); + const motionId2 = await voting.getMotionCount(); - let expenditurePollCount; - expenditurePollCount = await voting.getExpenditurePollCount(expenditureHash); - expect(expenditurePollCount).to.be.zero; + let expenditureMotionCount; + expenditureMotionCount = await voting.getExpenditureMotionCount(expenditureHash); + expect(expenditureMotionCount).to.be.zero; let expenditureSlot; expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); expect(expenditureSlot.claimDelay).to.be.zero; - await voting.stakePoll(pollId1, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId2, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakeMotion(motionId1, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakeMotion(motionId2, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - expenditurePollCount = await voting.getExpenditurePollCount(expenditureHash); - expect(expenditurePollCount).to.eq.BN(2); + expenditureMotionCount = await voting.getExpenditureMotionCount(expenditureHash); + expect(expenditureMotionCount).to.eq.BN(2); expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); expect(expenditureSlot.claimDelay).to.eq.BN(UINT256_MAX); await forwardTime(STAKE_PERIOD, this); - await voting.finalizePoll(pollId1); + await voting.finalizeMotion(motionId1); - expenditurePollCount = await voting.getExpenditurePollCount(expenditureHash); - expect(expenditurePollCount).to.eq.BN(1); + expenditureMotionCount = await voting.getExpenditureMotionCount(expenditureHash); + expect(expenditureMotionCount).to.eq.BN(1); expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); expect(expenditureSlot.claimDelay).to.eq.BN(UINT256_MAX); - await voting.finalizePoll(pollId2); + await voting.finalizeMotion(motionId2); - expenditurePollCount = await voting.getExpenditurePollCount(expenditureHash); - expect(expenditurePollCount).to.be.zero; + expenditureMotionCount = await voting.getExpenditureMotionCount(expenditureHash); + expect(expenditureMotionCount).to.be.zero; expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); expect(expenditureSlot.claimDelay).to.be.zero; @@ -581,7 +583,7 @@ contract("Voting Reputation", (accounts) => { it("cannot stake with someone else's reputation", async () => { await checkErrorRevert( - voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER1 }), + voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER1 }), "voting-rep-invalid-user-address" ); }); @@ -592,7 +594,7 @@ contract("Voting Reputation", (accounts) => { const [user2Mask, user2Siblings] = await reputationTree.getProof(user2Key); await checkErrorRevert( - voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user2Key, user2Value, user2Mask, user2Siblings, { from: USER2 }), + voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user2Key, user2Value, user2Mask, user2Siblings, { from: USER2 }), "voting-rep-insufficient-rep" ); }); @@ -601,95 +603,95 @@ contract("Voting Reputation", (accounts) => { await forwardTime(STAKE_PERIOD, this); await checkErrorRevert( - voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), + voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), "voting-rep-staking-closed" ); await checkErrorRevert( - voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }), + voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }), "voting-rep-staking-closed" ); }); }); - describe("voting on polls", async () => { - let pollId; + describe("voting on motions", async () => { + let motionId; beforeEach(async () => { const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); - await voting.createRootPoll(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); - pollId = await voting.getPollCount(); + await voting.createRootMotion(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + motionId = await voting.getMotionCount(); - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); }); - it("can rate and reveal for a poll", async () => { - await voting.submitVote(pollId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + it("can rate and reveal for a motion", async () => { + await voting.submitVote(motionId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await forwardTime(SUBMIT_PERIOD, this); - await voting.revealVote(pollId, SALT, NAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.revealVote(motionId, SALT, NAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); }); it("can tally votes from two users", async () => { - await voting.submitVote(pollId, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.submitVote(pollId, soliditySha3(SALT, YAY), user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.submitVote(motionId, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.submitVote(motionId, soliditySha3(SALT, YAY), user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await voting.revealVote(pollId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.revealVote(pollId, SALT, YAY, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.revealVote(motionId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.revealVote(motionId, SALT, YAY, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); // See final counts - const { votes } = await voting.getPoll(pollId); + const { votes } = await voting.getMotion(motionId); expect(votes[0]).to.be.zero; expect(votes[1]).to.eq.BN(WAD.muln(3)); }); it("can update votes, but just the last one counts", async () => { - await voting.submitVote(pollId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.submitVote(pollId, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.submitVote(motionId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.submitVote(motionId, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await forwardTime(SUBMIT_PERIOD, this); // Revealing first vote fails await checkErrorRevert( - voting.revealVote(pollId, SALT, NAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), + voting.revealVote(motionId, SALT, NAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), "voting-rep-secret-no-match" ); // Revealing second succeeds - await voting.revealVote(pollId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.revealVote(motionId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); }); it("can update votes, but the total reputation does not change", async () => { - let poll = await voting.getPoll(pollId); - expect(poll.repSubmitted).to.be.zero; + let motion = await voting.getMotion(motionId); + expect(motion.repSubmitted).to.be.zero; - await voting.submitVote(pollId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.submitVote(motionId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - poll = await voting.getPoll(pollId); - expect(poll.repSubmitted).to.eq.BN(WAD); + motion = await voting.getMotion(motionId); + expect(motion.repSubmitted).to.eq.BN(WAD); - await voting.submitVote(pollId, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.submitVote(motionId, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - poll = await voting.getPoll(pollId); - expect(poll.repSubmitted).to.eq.BN(WAD); + motion = await voting.getMotion(motionId); + expect(motion.repSubmitted).to.eq.BN(WAD); }); it("cannot reveal a vote twice, and so cannot vote twice", async () => { - await voting.submitVote(pollId, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.submitVote(pollId, soliditySha3(SALT, NAY), user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.submitVote(motionId, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.submitVote(motionId, soliditySha3(SALT, NAY), user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await voting.revealVote(pollId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.revealVote(motionId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await checkErrorRevert( - voting.revealVote(pollId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), + voting.revealVote(motionId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), "voting-rep-secret-no-match" ); }); - it("can vote in two polls with two reputation states, with different proofs", async () => { - await voting.submitVote(pollId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + it("can vote in two motions with two reputation states, with different proofs", async () => { + await voting.submitVote(motionId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); const oldRootHash = await reputationTree.getRootHash(); @@ -711,23 +713,23 @@ contract("Voting Reputation", (accounts) => { await repCycle.submitRootHash(rootHash, 0, "0x00", 10, { from: MINER }); await repCycle.confirmNewHash(0); - // Create new poll with new reputation state - await voting.createRootPoll(ADDRESS_ZERO, FAKE, domain1Key, domain1Value, domain1Mask2, domain1Siblings2); - const pollId2 = await voting.getPollCount(); - await voting.stakePoll(pollId2, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value2, user0Mask2, user0Siblings2, { from: USER0 }); - await voting.stakePoll(pollId2, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask2, user1Siblings2, { from: USER1 }); + // Create new motion with new reputation state + await voting.createRootMotion(ADDRESS_ZERO, FAKE, domain1Key, domain1Value, domain1Mask2, domain1Siblings2); + const motionId2 = await voting.getMotionCount(); + await voting.stakeMotion(motionId2, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value2, user0Mask2, user0Siblings2, { from: USER0 }); + await voting.stakeMotion(motionId2, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask2, user1Siblings2, { from: USER1 }); - await voting.submitVote(pollId2, soliditySha3(SALT, NAY), user0Key, user0Value2, user0Mask2, user0Siblings2, { from: USER0 }); + await voting.submitVote(motionId2, soliditySha3(SALT, NAY), user0Key, user0Value2, user0Mask2, user0Siblings2, { from: USER0 }); await forwardTime(SUBMIT_PERIOD, this); - await voting.revealVote(pollId, SALT, NAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.revealVote(pollId2, SALT, NAY, user0Key, user0Value2, user0Mask2, user0Siblings2, { from: USER0 }); + await voting.revealVote(motionId, SALT, NAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.revealVote(motionId2, SALT, NAY, user0Key, user0Value2, user0Mask2, user0Siblings2, { from: USER0 }); }); it("cannot submit a null vote", async () => { await checkErrorRevert( - voting.submitVote(pollId, "0x0", user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), + voting.submitVote(motionId, "0x0", user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), "voting-rep-invalid-secret" ); }); @@ -736,107 +738,111 @@ contract("Voting Reputation", (accounts) => { await forwardTime(SUBMIT_PERIOD, this); await checkErrorRevert( - voting.submitVote(pollId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), - "voting-rep-poll-not-open" + voting.submitVote(motionId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), + "voting-rep-motion-not-open" ); }); it("cannot reveal a vote if voting is open", async () => { - await voting.submitVote(pollId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await checkErrorRevert(voting.revealVote(pollId, SALT, YAY, FAKE, FAKE, 0, [], { from: USER0 }), "voting-rep-poll-not-reveal"); + await voting.submitVote(motionId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await checkErrorRevert(voting.revealVote(motionId, SALT, YAY, FAKE, FAKE, 0, [], { from: USER0 }), "voting-rep-motion-not-reveal"); }); it("cannot reveal a vote after voting closes", async () => { - await voting.submitVote(pollId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.submitVote(motionId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await forwardTime(SUBMIT_PERIOD, this); await forwardTime(REVEAL_PERIOD, this); - await checkErrorRevert(voting.revealVote(pollId, SALT, NAY, FAKE, FAKE, 0, [], { from: USER0 }), "voting-rep-poll-not-reveal"); + await checkErrorRevert(voting.revealVote(motionId, SALT, NAY, FAKE, FAKE, 0, [], { from: USER0 }), "voting-rep-motion-not-reveal"); }); it("cannot reveal a vote with a bad secret", async () => { - await voting.submitVote(pollId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.submitVote(motionId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await forwardTime(SUBMIT_PERIOD, this); await checkErrorRevert( - voting.revealVote(pollId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), + voting.revealVote(motionId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), "voting-rep-secret-no-match" ); }); it("cannot reveal a vote with a bad proof", async () => { - await voting.submitVote(pollId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.submitVote(motionId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await forwardTime(SUBMIT_PERIOD, this); // Invalid proof (wrong root hash) - await checkErrorRevert(voting.revealVote(pollId, SALT, NAY, FAKE, FAKE, 0, [], { from: USER0 }), "voting-rep-invalid-root-hash"); + await checkErrorRevert(voting.revealVote(motionId, SALT, NAY, FAKE, FAKE, 0, [], { from: USER0 }), "voting-rep-invalid-root-hash"); // Invalid colony address let key, value, mask, siblings; // eslint-disable-line one-var key = makeReputationKey(metaColony.address, domain1.skillId, USER0); value = makeReputationValue(WAD, 3); [mask, siblings] = await reputationTree.getProof(key); - await checkErrorRevert(voting.revealVote(pollId, SALT, NAY, key, value, mask, siblings, { from: USER0 }), "voting-rep-invalid-colony-address"); + + await checkErrorRevert( + voting.revealVote(motionId, SALT, NAY, key, value, mask, siblings, { from: USER0 }), + "voting-rep-invalid-colony-address" + ); // Invalid skill id key = makeReputationKey(colony.address, 1234, USER0); value = makeReputationValue(WAD, 4); [mask, siblings] = await reputationTree.getProof(key); - await checkErrorRevert(voting.revealVote(pollId, SALT, NAY, key, value, mask, siblings, { from: USER0 }), "voting-rep-invalid-skill-id"); + await checkErrorRevert(voting.revealVote(motionId, SALT, NAY, key, value, mask, siblings, { from: USER0 }), "voting-rep-invalid-skill-id"); // Invalid user address await checkErrorRevert( - voting.revealVote(pollId, SALT, NAY, user1Key, user1Value, user1Mask, user1Siblings, { from: USER0 }), + voting.revealVote(motionId, SALT, NAY, user1Key, user1Value, user1Mask, user1Siblings, { from: USER0 }), "voting-rep-invalid-user-address" ); }); }); - describe("executing polls", async () => { - let pollId; + describe("executing motions", async () => { + let motionId; beforeEach(async () => { const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); - await voting.createRootPoll(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); - pollId = await voting.getPollCount(); + await voting.createRootMotion(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + motionId = await voting.getMotionCount(); }); it("cannot take an action if there is insufficient support", async () => { - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE.subn(1), user0Key, user0Value, user0Mask, user0Siblings, { + await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE.subn(1), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0, }); await forwardTime(STAKE_PERIOD, this); - await checkErrorRevert(voting.finalizePoll(pollId), "voting-rep-poll-not-executable"); + await checkErrorRevert(voting.finalizeMotion(motionId), "voting-rep-motion-not-executable"); }); it("can take an action if there is insufficient opposition", async () => { - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE.subn(1), user1Key, user1Value, user1Mask, user1Siblings, { + await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, REQUIRED_STAKE.subn(1), user1Key, user1Value, user1Mask, user1Siblings, { from: USER1, }); await forwardTime(STAKE_PERIOD, this); - const { logs } = await voting.finalizePoll(pollId); + const { logs } = await voting.finalizeMotion(motionId); expect(logs[0].args.executed).to.be.true; }); it("can take an action with a return value", async () => { // Returns a uint256 const action = await encodeTxData(colony, "version", []); - await voting.createRootPoll(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); - pollId = await voting.getPollCount(); + await voting.createRootMotion(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + motionId = await voting.getMotionCount(); - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await forwardTime(STAKE_PERIOD, this); - const { logs } = await voting.finalizePoll(pollId); + const { logs } = await voting.finalizeMotion(motionId); expect(logs[0].args.executed).to.be.true; }); @@ -845,17 +851,17 @@ contract("Voting Reputation", (accounts) => { await token.mint(otherColony.address, WAD, { from: USER0 }); const action = await encodeTxData(colony, "claimColonyFunds", [token.address]); - await voting.createRootPoll(otherColony.address, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); - pollId = await voting.getPollCount(); + await voting.createRootMotion(otherColony.address, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + motionId = await voting.getMotionCount(); - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await forwardTime(STAKE_PERIOD, this); const balanceBefore = await otherColony.getFundingPotBalance(1, token.address); expect(balanceBefore).to.be.zero; - await voting.finalizePoll(pollId); + await voting.finalizeMotion(motionId); const balanceAfter = await otherColony.getFundingPotBalance(1, token.address); expect(balanceAfter).to.eq.BN(WAD); @@ -863,75 +869,75 @@ contract("Voting Reputation", (accounts) => { it("can take a nonexistent action", async () => { const action = soliditySha3("foo"); - await voting.createRootPoll(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); - pollId = await voting.getPollCount(); + await voting.createRootMotion(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + motionId = await voting.getMotionCount(); - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await forwardTime(STAKE_PERIOD, this); - const { logs } = await voting.finalizePoll(pollId); + const { logs } = await voting.finalizeMotion(motionId); expect(logs[0].args.executed).to.be.false; }); it("cannot take an action during staking or voting", async () => { - let pollState; - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + let motionState; + await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - pollState = await voting.getPollState(pollId); - expect(pollState).to.eq.BN(STAKING); - await checkErrorRevert(voting.finalizePoll(pollId), "voting-rep-poll-not-executable"); + motionState = await voting.getMotionState(motionId); + expect(motionState).to.eq.BN(STAKING); + await checkErrorRevert(voting.finalizeMotion(motionId), "voting-rep-motion-not-executable"); - await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - pollState = await voting.getPollState(pollId); - expect(pollState).to.eq.BN(SUBMIT); - await checkErrorRevert(voting.finalizePoll(pollId), "voting-rep-poll-not-executable"); + motionState = await voting.getMotionState(motionId); + expect(motionState).to.eq.BN(SUBMIT); + await checkErrorRevert(voting.finalizeMotion(motionId), "voting-rep-motion-not-executable"); }); it("cannot take an action twice", async () => { - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await forwardTime(STAKE_PERIOD, this); - const { logs } = await voting.finalizePoll(pollId); + const { logs } = await voting.finalizeMotion(motionId); expect(logs[0].args.executed).to.be.true; - await checkErrorRevert(voting.finalizePoll(pollId), "voting-rep-poll-not-executable"); + await checkErrorRevert(voting.finalizeMotion(motionId), "voting-rep-motion-not-executable"); }); - it("can take an action if the poll passes", async () => { - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + it("can take an action if the motion passes", async () => { + await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await voting.submitVote(pollId, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.submitVote(motionId, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await forwardTime(SUBMIT_PERIOD, this); - await voting.revealVote(pollId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.revealVote(motionId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); // Don't need to wait for the reveal period, since 100% of the secret is revealed await forwardTime(STAKE_PERIOD, this); - const { logs } = await voting.finalizePoll(pollId); + const { logs } = await voting.finalizeMotion(motionId); expect(logs[0].args.executed).to.be.true; }); - it("cannot take an action if the poll fails", async () => { - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + it("cannot take an action if the motion fails", async () => { + await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await voting.submitVote(pollId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.submitVote(motionId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await forwardTime(SUBMIT_PERIOD, this); - await voting.revealVote(pollId, SALT, NAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.revealVote(motionId, SALT, NAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await forwardTime(REVEAL_PERIOD, this); await forwardTime(STAKE_PERIOD, this); - const { logs } = await voting.finalizePoll(pollId); + const { logs } = await voting.finalizeMotion(motionId); expect(logs[0].args.executed).to.be.false; }); @@ -941,42 +947,42 @@ contract("Voting Reputation", (accounts) => { const expenditureId = await colony.getExpenditureCount(); const action = await encodeTxData(colony, "setExpenditureState", [1, UINT256_MAX, expenditureId, 25, [true], [bn2bytes32(new BN(4))], WAD32]); - await voting.createDomainPoll(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); - const pollId1 = await voting.getPollCount(); + await voting.createDomainMotion(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + const motionId1 = await voting.getMotionCount(); - await voting.stakePoll(pollId1, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId1, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakeMotion(motionId1, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakeMotion(motionId1, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await voting.submitVote(pollId1, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.submitVote(motionId1, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await forwardTime(SUBMIT_PERIOD, this); - await voting.revealVote(pollId1, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.revealVote(motionId1, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await forwardTime(REVEAL_PERIOD, this); await forwardTime(STAKE_PERIOD, this); let logs; - ({ logs } = await voting.finalizePoll(pollId1)); + ({ logs } = await voting.finalizeMotion(motionId1)); expect(logs[0].args.executed).to.be.true; - // Create another poll for the same variable - await voting.createDomainPoll(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); - const pollId2 = await voting.getPollCount(); + // Create another motion for the same variable + await voting.createDomainMotion(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + const motionId2 = await voting.getMotionCount(); - await voting.stakePoll(pollId2, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId2, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakeMotion(motionId2, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakeMotion(motionId2, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await voting.submitVote(pollId2, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.submitVote(motionId2, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await forwardTime(SUBMIT_PERIOD, this); - await voting.revealVote(pollId2, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.revealVote(motionId2, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await forwardTime(REVEAL_PERIOD, this); await forwardTime(STAKE_PERIOD, this); - ({ logs } = await voting.finalizePoll(pollId2)); + ({ logs } = await voting.finalizeMotion(motionId2)); expect(logs[0].args.executed).to.be.false; }); @@ -986,17 +992,17 @@ contract("Voting Reputation", (accounts) => { const action = await encodeTxData(colony, "setExpenditureState", [1, UINT256_MAX, expenditureId, 25, [true], ["0x0"], WAD32]); - await voting.createDomainPoll(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); - pollId = await voting.getPollCount(); + await voting.createDomainMotion(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + motionId = await voting.getMotionCount(); - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await forwardTime(STAKE_PERIOD, this); - await voting.finalizePoll(pollId); + await voting.finalizeMotion(motionId); const slotHash = soliditySha3(`0x${action.slice(2 + 8 + 128, action.length - 64)}`); - const pastPoll = await voting.getExpenditurePastPoll(slotHash); - expect(pastPoll).to.eq.BN(REQUIRED_STAKE); + const pastMotion = await voting.getExpenditurePastMotion(slotHash); + expect(pastMotion).to.eq.BN(REQUIRED_STAKE); }); it("can set vote power correctly after a vote", async () => { @@ -1005,35 +1011,35 @@ contract("Voting Reputation", (accounts) => { const action = await encodeTxData(colony, "setExpenditureState", [1, UINT256_MAX, expenditureId, 25, [true], ["0x0"], WAD32]); - await voting.createDomainPoll(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); - pollId = await voting.getPollCount(); + await voting.createDomainMotion(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + motionId = await voting.getMotionCount(); - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await voting.submitVote(pollId, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.submitVote(motionId, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await forwardTime(SUBMIT_PERIOD, this); - await voting.revealVote(pollId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.revealVote(motionId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await forwardTime(REVEAL_PERIOD, this); await forwardTime(ESCALATION_PERIOD, this); - await voting.finalizePoll(pollId); + await voting.finalizeMotion(motionId); const slotHash = soliditySha3(`0x${action.slice(2 + 8 + 128, action.length - 64)}`); - const pastPoll = await voting.getExpenditurePastPoll(slotHash); - expect(pastPoll).to.eq.BN(WAD); // USER0 had 1 WAD of reputation + const pastMotion = await voting.getExpenditurePastMotion(slotHash); + expect(pastMotion).to.eq.BN(WAD); // USER0 had 1 WAD of reputation }); }); describe("claiming rewards", async () => { - let pollId; + let motionId; beforeEach(async () => { const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); - await voting.createRootPoll(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); - pollId = await voting.getPollCount(); + await voting.createRootMotion(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + motionId = await voting.getMotionCount(); }); it("can let stakers claim rewards, based on the stake outcome", async () => { @@ -1042,18 +1048,18 @@ contract("Voting Reputation", (accounts) => { const numEntriesPrev = await repCycle.getReputationUpdateLogLength(); const nayStake = REQUIRED_STAKE.divn(2); - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, nayStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, nayStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await forwardTime(STAKE_PERIOD, this); - await voting.finalizePoll(pollId); + await voting.finalizeMotion(motionId); const user0LockPre = await tokenLocking.getUserLock(token.address, USER0); const user1LockPre = await tokenLocking.getUserLock(token.address, USER1); - await voting.claimReward(pollId, 1, UINT256_MAX, USER0, YAY); - await voting.claimReward(pollId, 1, UINT256_MAX, USER1, NAY); + await voting.claimReward(motionId, 1, UINT256_MAX, USER0, YAY); + await voting.claimReward(motionId, 1, UINT256_MAX, USER1, NAY); const user0LockPost = await tokenLocking.getUserLock(token.address, USER0); const user1LockPost = await tokenLocking.getUserLock(token.address, USER1); @@ -1079,24 +1085,24 @@ contract("Voting Reputation", (accounts) => { const repCycle = await IReputationMiningCycle.at(addr); const numEntriesPrev = await repCycle.getReputationUpdateLogLength(); - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await voting.submitVote(pollId, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.submitVote(pollId, soliditySha3(SALT, NAY), user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.submitVote(motionId, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.submitVote(motionId, soliditySha3(SALT, NAY), user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await voting.revealVote(pollId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.revealVote(pollId, SALT, NAY, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.revealVote(motionId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.revealVote(motionId, SALT, NAY, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await forwardTime(ESCALATION_PERIOD, this); - await voting.finalizePoll(pollId); + await voting.finalizeMotion(motionId); const user0LockPre = await tokenLocking.getUserLock(token.address, USER0); const user1LockPre = await tokenLocking.getUserLock(token.address, USER1); - await voting.claimReward(pollId, 1, UINT256_MAX, USER0, YAY); - await voting.claimReward(pollId, 1, UINT256_MAX, USER1, NAY); + await voting.claimReward(motionId, 1, UINT256_MAX, USER0, YAY); + await voting.claimReward(motionId, 1, UINT256_MAX, USER1, NAY); const user0LockPost = await tokenLocking.getUserLock(token.address, USER0); const user1LockPost = await tokenLocking.getUserLock(token.address, USER1); @@ -1122,29 +1128,31 @@ contract("Voting Reputation", (accounts) => { const user2Value = makeReputationValue(REQUIRED_STAKE.subn(1), 8); const [user2Mask, user2Siblings] = await reputationTree.getProof(user2Key); - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE.divn(3).muln(2), user1Key, user1Value, user1Mask, user1Siblings, { + await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, REQUIRED_STAKE.divn(3).muln(2), user1Key, user1Value, user1Mask, user1Siblings, { from: USER1, }); - await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE.divn(3), user2Key, user2Value, user2Mask, user2Siblings, { from: USER2 }); + await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, REQUIRED_STAKE.divn(3), user2Key, user2Value, user2Mask, user2Siblings, { + from: USER2, + }); - await voting.submitVote(pollId, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.submitVote(pollId, soliditySha3(SALT, NAY), user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.submitVote(motionId, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.submitVote(motionId, soliditySha3(SALT, NAY), user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await voting.revealVote(pollId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.revealVote(pollId, SALT, NAY, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.revealVote(motionId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.revealVote(motionId, SALT, NAY, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await forwardTime(ESCALATION_PERIOD, this); - await voting.finalizePoll(pollId); + await voting.finalizeMotion(motionId); const user0LockPre = await tokenLocking.getUserLock(token.address, USER0); const user1LockPre = await tokenLocking.getUserLock(token.address, USER1); const user2LockPre = await tokenLocking.getUserLock(token.address, USER2); - await voting.claimReward(pollId, 1, UINT256_MAX, USER0, YAY); - await voting.claimReward(pollId, 1, UINT256_MAX, USER1, NAY); - await voting.claimReward(pollId, 1, UINT256_MAX, USER2, NAY); + await voting.claimReward(motionId, 1, UINT256_MAX, USER0, YAY); + await voting.claimReward(motionId, 1, UINT256_MAX, USER1, NAY); + await voting.claimReward(motionId, 1, UINT256_MAX, USER2, NAY); const user0LockPost = await tokenLocking.getUserLock(token.address, USER0); const user1LockPost = await tokenLocking.getUserLock(token.address, USER1); @@ -1165,29 +1173,31 @@ contract("Voting Reputation", (accounts) => { const user2Value = makeReputationValue(REQUIRED_STAKE.subn(1), 8); const [user2Mask, user2Siblings] = await reputationTree.getProof(user2Key); - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE.divn(3).muln(2), user0Key, user0Value, user0Mask, user0Siblings, { + await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE.divn(3).muln(2), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0, }); - await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE.divn(3), user2Key, user2Value, user2Mask, user2Siblings, { from: USER2 }); + await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE.divn(3), user2Key, user2Value, user2Mask, user2Siblings, { + from: USER2, + }); - await voting.submitVote(pollId, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.submitVote(pollId, soliditySha3(SALT, NAY), user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.submitVote(motionId, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.submitVote(motionId, soliditySha3(SALT, NAY), user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await voting.revealVote(pollId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.revealVote(pollId, SALT, NAY, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.revealVote(motionId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.revealVote(motionId, SALT, NAY, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await forwardTime(ESCALATION_PERIOD, this); - await voting.finalizePoll(pollId); + await voting.finalizeMotion(motionId); const user0LockPre = await tokenLocking.getUserLock(token.address, USER0); const user1LockPre = await tokenLocking.getUserLock(token.address, USER1); const user2LockPre = await tokenLocking.getUserLock(token.address, USER2); - await voting.claimReward(pollId, 1, UINT256_MAX, USER0, YAY); - await voting.claimReward(pollId, 1, UINT256_MAX, USER1, NAY); - await voting.claimReward(pollId, 1, UINT256_MAX, USER2, YAY); + await voting.claimReward(motionId, 1, UINT256_MAX, USER0, YAY); + await voting.claimReward(motionId, 1, UINT256_MAX, USER1, NAY); + await voting.claimReward(motionId, 1, UINT256_MAX, USER2, YAY); const user0LockPost = await tokenLocking.getUserLock(token.address, USER0); const user1LockPost = await tokenLocking.getUserLock(token.address, USER1); @@ -1208,16 +1218,17 @@ contract("Voting Reputation", (accounts) => { const repCycle = await IReputationMiningCycle.at(addr); const numEntriesPrev = await repCycle.getReputationUpdateLogLength(); - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE.divn(2), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE.divn(2), user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + const half = REQUIRED_STAKE.divn(2); + await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, half, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, half, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await forwardTime(STAKE_PERIOD, this); const user0LockPre = await tokenLocking.getUserLock(token.address, USER0); const user1LockPre = await tokenLocking.getUserLock(token.address, USER1); - await voting.claimReward(pollId, 1, UINT256_MAX, USER0, YAY); - await voting.claimReward(pollId, 1, UINT256_MAX, USER1, NAY); + await voting.claimReward(motionId, 1, UINT256_MAX, USER0, YAY); + await voting.claimReward(motionId, 1, UINT256_MAX, USER1, NAY); const numEntriesPost = await repCycle.getReputationUpdateLogLength(); @@ -1225,39 +1236,39 @@ contract("Voting Reputation", (accounts) => { const user1LockPost = await tokenLocking.getUserLock(token.address, USER1); expect(numEntriesPrev).to.eq.BN(numEntriesPost); - expect(new BN(user0LockPost.balance).sub(new BN(user0LockPre.balance))).to.eq.BN(REQUIRED_STAKE.divn(2)); - expect(new BN(user1LockPost.balance).sub(new BN(user1LockPre.balance))).to.eq.BN(REQUIRED_STAKE.divn(2)); + expect(new BN(user0LockPost.balance).sub(new BN(user0LockPre.balance))).to.eq.BN(half); + expect(new BN(user1LockPost.balance).sub(new BN(user1LockPre.balance))).to.eq.BN(half); }); it("cannot claim rewards twice", async () => { - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await voting.submitVote(pollId, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.submitVote(motionId, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await forwardTime(SUBMIT_PERIOD, this); - await voting.revealVote(pollId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.revealVote(motionId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await forwardTime(REVEAL_PERIOD, this); await forwardTime(ESCALATION_PERIOD, this); - await voting.finalizePoll(pollId); + await voting.finalizeMotion(motionId); - await voting.claimReward(pollId, 1, UINT256_MAX, USER0, YAY); + await voting.claimReward(motionId, 1, UINT256_MAX, USER0, YAY); const userLock0 = await tokenLocking.getUserLock(token.address, USER0); - await voting.claimReward(pollId, 1, UINT256_MAX, USER0, YAY); + await voting.claimReward(motionId, 1, UINT256_MAX, USER0, YAY); const userLock1 = await tokenLocking.getUserLock(token.address, USER0); expect(userLock0.balance).to.eq.BN(userLock1.balance); }); - it("cannot claim rewards before a poll is finalized", async () => { - await checkErrorRevert(voting.claimReward(pollId, 1, UINT256_MAX, USER0, YAY), "voting-rep-not-failed-or-finalized"); + it("cannot claim rewards before a motion is finalized", async () => { + await checkErrorRevert(voting.claimReward(motionId, 1, UINT256_MAX, USER0, YAY), "voting-rep-not-failed-or-finalized"); }); }); - describe("escalating polls", async () => { - let pollId; + describe("escalating motions", async () => { + let motionId; beforeEach(async () => { const domain2Key = makeReputationKey(colony.address, domain2.skillId); @@ -1273,117 +1284,117 @@ contract("Voting Reputation", (accounts) => { const [user1Mask2, user1Siblings2] = await reputationTree.getProof(user1Key2); const action = await encodeTxData(colony, "makeTask", [1, 0, FAKE, 2, 0, 0]); - await voting.createDomainPoll(2, UINT256_MAX, ADDRESS_ZERO, action, domain2Key, domain2Value, domain2Mask, domain2Siblings); - pollId = await voting.getPollCount(); + await voting.createDomainMotion(2, UINT256_MAX, ADDRESS_ZERO, action, domain2Key, domain2Value, domain2Mask, domain2Siblings); + motionId = await voting.getMotionCount(); await colony.approveStake(voting.address, 2, WAD, { from: USER0 }); await colony.approveStake(voting.address, 2, WAD, { from: USER1 }); - await voting.stakePoll(pollId, 1, 0, NAY, WAD.divn(1000), user0Key2, user0Value2, user0Mask2, user0Siblings2, { from: USER0 }); - await voting.stakePoll(pollId, 1, 0, YAY, WAD.divn(1000), user1Key2, user1Value2, user1Mask2, user1Siblings2, { from: USER1 }); + await voting.stakeMotion(motionId, 1, 0, NAY, WAD.divn(1000), user0Key2, user0Value2, user0Mask2, user0Siblings2, { from: USER0 }); + await voting.stakeMotion(motionId, 1, 0, YAY, WAD.divn(1000), user1Key2, user1Value2, user1Mask2, user1Siblings2, { from: USER1 }); // Note that this is a passing vote - await voting.submitVote(pollId, soliditySha3(SALT, NAY), user0Key2, user0Value2, user0Mask2, user0Siblings2, { from: USER0 }); - await voting.submitVote(pollId, soliditySha3(SALT, YAY), user1Key2, user1Value2, user1Mask2, user1Siblings2, { from: USER1 }); + await voting.submitVote(motionId, soliditySha3(SALT, NAY), user0Key2, user0Value2, user0Mask2, user0Siblings2, { from: USER0 }); + await voting.submitVote(motionId, soliditySha3(SALT, YAY), user1Key2, user1Value2, user1Mask2, user1Siblings2, { from: USER1 }); - await voting.revealVote(pollId, SALT, NAY, user0Key2, user0Value2, user0Mask2, user0Siblings2, { from: USER0 }); - await voting.revealVote(pollId, SALT, YAY, user1Key2, user1Value2, user1Mask2, user1Siblings2, { from: USER1 }); + await voting.revealVote(motionId, SALT, NAY, user0Key2, user0Value2, user0Mask2, user0Siblings2, { from: USER0 }); + await voting.revealVote(motionId, SALT, YAY, user1Key2, user1Value2, user1Mask2, user1Siblings2, { from: USER1 }); }); - it("can internally escalate a domain poll after a vote", async () => { - await voting.escalatePoll(pollId, 1, 0, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); + it("can internally escalate a domain motion after a vote", async () => { + await voting.escalateMotion(motionId, 1, 0, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); }); - it("can internally escalate a domain poll after a vote", async () => { - await voting.escalatePoll(pollId, 1, 0, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER1 }); + it("can internally escalate a domain motion after a vote", async () => { + await voting.escalateMotion(motionId, 1, 0, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER1 }); }); - it("cannot internally escalate a domain poll if not in a 'closed' state", async () => { + it("cannot internally escalate a domain motion if not in a 'closed' state", async () => { await forwardTime(ESCALATION_PERIOD, this); - await voting.finalizePoll(pollId); + await voting.finalizeMotion(motionId); await checkErrorRevert( - voting.escalatePoll(pollId, 1, 0, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER2 }), - "voting-rep-poll-not-closed" + voting.escalateMotion(motionId, 1, 0, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER2 }), + "voting-rep-motion-not-closed" ); }); - it("cannot internally escalate a domain poll with an invalid domain proof", async () => { + it("cannot internally escalate a domain motion with an invalid domain proof", async () => { await checkErrorRevert( - voting.escalatePoll(pollId, 1, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }), + voting.escalateMotion(motionId, 1, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }), "voting-rep-invalid-domain-proof" ); }); - it("cannot internally escalate a domain poll with an invalid reputation proof", async () => { - await checkErrorRevert(voting.escalatePoll(pollId, 1, 0, "0x0", "0x0", "0x0", [], { from: USER0 }), "voting-rep-invalid-root-hash"); + it("cannot internally escalate a domain motion with an invalid reputation proof", async () => { + await checkErrorRevert(voting.escalateMotion(motionId, 1, 0, "0x0", "0x0", "0x0", [], { from: USER0 }), "voting-rep-invalid-root-hash"); }); - it("can stake after internally escalating a domain poll", async () => { - await voting.escalatePoll(pollId, 1, 0, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); + it("can stake after internally escalating a domain motion", async () => { + await voting.escalateMotion(motionId, 1, 0, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); const yayStake = REQUIRED_STAKE.sub(WAD.divn(1000)); const nayStake = yayStake.add(REQUIRED_STAKE.divn(10)); - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, yayStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, nayStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, yayStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, nayStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - const pollState = await voting.getPollState(pollId); - expect(pollState).to.eq.BN(SUBMIT); + const motionState = await voting.getMotionState(motionId); + expect(motionState).to.eq.BN(SUBMIT); }); - it("can execute after internally escalating a domain poll, if there is insufficient opposition", async () => { - await voting.escalatePoll(pollId, 1, 0, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); + it("can execute after internally escalating a domain motion, if there is insufficient opposition", async () => { + await voting.escalateMotion(motionId, 1, 0, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); const yayStake = REQUIRED_STAKE.sub(WAD.divn(1000)); - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, yayStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, yayStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await forwardTime(STAKE_PERIOD, this); - const { logs } = await voting.finalizePoll(pollId); + const { logs } = await voting.finalizeMotion(motionId); expect(logs[0].args.executed).to.be.true; }); - it("cannot execute after internally escalating a domain poll, if there is insufficient support", async () => { - await voting.escalatePoll(pollId, 1, 0, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); + it("cannot execute after internally escalating a domain motion, if there is insufficient support", async () => { + await voting.escalateMotion(motionId, 1, 0, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); const yayStake = REQUIRED_STAKE.sub(WAD.divn(1000)); - await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, yayStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, yayStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await forwardTime(STAKE_PERIOD, this); - const { logs } = await voting.finalizePoll(pollId); + const { logs } = await voting.finalizeMotion(motionId); expect(logs[0].args.executed).to.be.false; }); it("can fall back on the previous vote if both sides fail to stake", async () => { - await voting.escalatePoll(pollId, 1, 0, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); + await voting.escalateMotion(motionId, 1, 0, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); await forwardTime(STAKE_PERIOD, this); // Note that the previous vote succeeded - const { logs } = await voting.finalizePoll(pollId); + const { logs } = await voting.finalizeMotion(motionId); expect(logs[0].args.executed).to.be.true; }); - it("can use the result of a new stake after internally escalating a domain poll", async () => { - await voting.escalatePoll(pollId, 1, 0, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); + it("can use the result of a new stake after internally escalating a domain motion", async () => { + await voting.escalateMotion(motionId, 1, 0, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); const yayStake = REQUIRED_STAKE.sub(WAD.divn(1000)); const nayStake = yayStake.add(REQUIRED_STAKE.divn(10)); - await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, nayStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, nayStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await forwardTime(STAKE_PERIOD, this); - const pollState = await voting.getPollState(pollId); - expect(pollState).to.eq.BN(FAILED); + const motionState = await voting.getMotionState(motionId); + expect(motionState).to.eq.BN(FAILED); // Now check that the rewards come out properly const user0LockPre = await tokenLocking.getUserLock(token.address, USER0); const user1LockPre = await tokenLocking.getUserLock(token.address, USER1); - await voting.claimReward(pollId, 1, UINT256_MAX, USER0, YAY); - await voting.claimReward(pollId, 1, UINT256_MAX, USER1, NAY); + await voting.claimReward(motionId, 1, UINT256_MAX, USER0, YAY); + await voting.claimReward(motionId, 1, UINT256_MAX, USER1, NAY); const user0LockPost = await tokenLocking.getUserLock(token.address, USER0); const user1LockPost = await tokenLocking.getUserLock(token.address, USER1); @@ -1394,24 +1405,24 @@ contract("Voting Reputation", (accounts) => { expect(new BN(user1LockPost.balance).sub(new BN(user1LockPre.balance))).to.eq.BN(expectedReward1); }); - it("can use the result of a new vote after internally escalating a domain poll", async () => { - await voting.escalatePoll(pollId, 1, 0, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); + it("can use the result of a new vote after internally escalating a domain motion", async () => { + await voting.escalateMotion(motionId, 1, 0, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); const yayStake = REQUIRED_STAKE.sub(WAD.divn(1000)); const nayStake = yayStake.add(REQUIRED_STAKE.divn(10)); - await voting.stakePoll(pollId, 1, UINT256_MAX, YAY, yayStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakePoll(pollId, 1, UINT256_MAX, NAY, nayStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, yayStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, nayStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); // Vote fails - await voting.submitVote(pollId, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.submitVote(pollId, soliditySha3(SALT, NAY), user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.submitVote(motionId, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.submitVote(motionId, soliditySha3(SALT, NAY), user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await voting.revealVote(pollId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.revealVote(pollId, SALT, NAY, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.revealVote(motionId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.revealVote(motionId, SALT, NAY, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await forwardTime(ESCALATION_PERIOD, this); - const { logs } = await voting.finalizePoll(pollId); + const { logs } = await voting.finalizeMotion(motionId); expect(logs[0].args.executed).to.be.false; // Now check that the rewards come out properly @@ -1419,8 +1430,8 @@ contract("Voting Reputation", (accounts) => { const user0LockPre = await tokenLocking.getUserLock(token.address, USER0); const user1LockPre = await tokenLocking.getUserLock(token.address, USER1); - await voting.claimReward(pollId, 1, UINT256_MAX, USER0, YAY); - await voting.claimReward(pollId, 1, UINT256_MAX, USER1, NAY); + await voting.claimReward(motionId, 1, UINT256_MAX, USER0, YAY); + await voting.claimReward(motionId, 1, UINT256_MAX, USER1, NAY); const user0LockPost = await tokenLocking.getUserLock(token.address, USER0); const user1LockPost = await tokenLocking.getUserLock(token.address, USER1); From 15d50c4deea2a286d5632bace1e1c661da1fef4e Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Wed, 5 Aug 2020 15:57:04 -0700 Subject: [PATCH 39/61] Make ties a non-executing outcome --- contracts/extensions/VotingReputation.sol | 7 +++---- test/extensions/voting-rep.js | 7 +++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index 1461f2862e..5d9da537a7 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -464,8 +464,8 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { motion.finalized = true; bool canExecute = ( - motion.stakes[NAY] <= motion.stakes[YAY] && - motion.votes[NAY] <= motion.votes[YAY] + motion.stakes[NAY] < motion.stakes[YAY] || + motion.votes[NAY] < motion.votes[YAY] ); if (getSig(motion.action) == CHANGE_FUNCTION) { @@ -652,7 +652,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { } else if (motion.stakes[YAY] == requiredStake) { return MotionState.Finalizable; // If not, was there a prior vote we can fall back on? - } else if (motion.votes[NAY] > 0 || motion.votes[YAY] > 0) { + } else if (add(motion.votes[NAY], motion.votes[YAY]) > 0) { return MotionState.Finalizable; // Otherwise, the motion failed } else { @@ -693,7 +693,6 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { motionCount += 1; - motions[motionCount].events[STAKE_END] = uint64(now + stakePeriod); motions[motionCount].events[SUBMIT_END] = uint64(now + stakePeriod + submitPeriod); motions[motionCount].events[REVEAL_END] = uint64(now + stakePeriod + submitPeriod + revealPeriod); diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index 54812c376a..dee6b274bd 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -1358,13 +1358,12 @@ contract("Voting Reputation", (accounts) => { it("cannot execute after internally escalating a domain motion, if there is insufficient support", async () => { await voting.escalateMotion(motionId, 1, 0, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); - const yayStake = REQUIRED_STAKE.sub(WAD.divn(1000)); - await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, yayStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); await forwardTime(STAKE_PERIOD, this); - const { logs } = await voting.finalizeMotion(motionId); - expect(logs[0].args.executed).to.be.false; + const motionState = await voting.getMotionState(motionId); + expect(motionState).to.eq.BN(FAILED); }); it("can fall back on the previous vote if both sides fail to stake", async () => { From c06f414acb079099ce71f5510a6ab0630ac6ff68 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Thu, 6 Aug 2020 17:10:24 -0700 Subject: [PATCH 40/61] Response to review comments VII --- contracts/extensions/VotingReputation.sol | 75 ++++++++++++++--------- test/extensions/voting-rep.js | 8 +-- 2 files changed, 51 insertions(+), 32 deletions(-) diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index 5d9da537a7..cd4c303e2d 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -169,6 +169,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { uint256[2] pastVoterComp; // [nay, yay] uint256[2] stakes; // [nay, yay] uint256[2] votes; // [nay, yay] + bool escalated; bool finalized; address target; bytes action; @@ -291,9 +292,9 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { // Increment counter & extend claim delay if staking for an expenditure state change if ( _vote == YAY && + !motion.escalated && motion.stakes[YAY] == requiredStake && - getSig(motion.action) == CHANGE_FUNCTION && - add(motion.votes[NAY], motion.votes[YAY]) == 0 + getSig(motion.action) == CHANGE_FUNCTION ) { bytes32 structHash = hashExpenditureStruct(motion.action); expenditureMotionCounts[structHash] = add(expenditureMotionCounts[structHash], 1); @@ -301,6 +302,8 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { require(executeCall(_motionId, claimDelayAction), "voting-rep-expenditure-lock-failed"); } + emit MotionStaked(_motionId, msg.sender, _vote, amount); + // Move to second staking window once one side is fully staked if ( (_vote == YAY && motion.stakes[YAY] == requiredStake && motion.stakes[NAY] < requiredStake) || @@ -323,8 +326,6 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { emit MotionEventSet(_motionId, STAKE_END); } - - emit MotionStaked(_motionId, msg.sender, _vote, amount); } /// @notice Submit a vote secret for a motion @@ -441,10 +442,6 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { uint256 childSkillId = colonyNetwork.getChildSkillId(newDomainSkillId, _childSkillIndex); require(childSkillId == motion.skillId, "voting-rep-invalid-domain-proof"); - motion.events[STAKE_END] = uint64(now + stakePeriod); - motion.events[SUBMIT_END] = uint64(now + stakePeriod + submitPeriod); - motion.events[REVEAL_END] = uint64(now + stakePeriod + submitPeriod + revealPeriod); - motion.domainId = _newDomainId; motion.skillId = newDomainSkillId; motion.skillRep = getReputationFromProof(_motionId, address(0x0), _key, _value, _branchMask, _siblings); @@ -454,12 +451,23 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { motion.pastVoterComp[loser] = add(motion.pastVoterComp[loser], motion.paidVoterComp); delete motion.paidVoterComp; + uint256 requiredStake = getRequiredStake(_motionId); + motion.events[STAKE_END] = (motion.stakes[NAY] < requiredStake || motion.stakes[YAY] < requiredStake) ? + uint64(now + stakePeriod) : uint64(now); + + motion.events[SUBMIT_END] = motion.events[STAKE_END] + uint64(submitPeriod); + motion.events[REVEAL_END] = motion.events[STAKE_END] + uint64(submitPeriod + revealPeriod); + + motion.escalated = true; + emit MotionEscalated(_motionId, msg.sender, _newDomainId); } function finalizeMotion(uint256 _motionId) public { Motion storage motion = motions[_motionId]; - require(getMotionState(_motionId) == MotionState.Finalizable, "voting-rep-motion-not-executable"); + require(getMotionState(_motionId) == MotionState.Finalizable, "voting-rep-motion-not-finalizable"); + + assert(motion.stakes[YAY] == getRequiredStake(_motionId) || add(motion.votes[NAY], motion.votes[YAY]) > 0); motion.finalized = true; @@ -475,7 +483,8 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { // Release the claimDelay if this is the last active motion if (expenditureMotionCounts[structHash] == 0) { bytes memory claimDelayAction = createClaimDelayAction(motion.action, 0); - require(executeCall(_motionId, claimDelayAction), "voting-rep-expenditure-unlock-failed"); + // No require this time, since we don't want stakes to be permanently locked + executeCall(_motionId, claimDelayAction); } bytes32 slotHash = hashExpenditureSlot(motion.action); @@ -536,7 +545,6 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { // Went to a vote, use vote to determine reward or penalty if (add(motion.votes[NAY], motion.votes[YAY]) > 0) { - assert(motion.stakes[NAY] == requiredStake && motion.stakes[YAY] == requiredStake); uint256 loserStake = sub(requiredStake, motion.paidVoterComp); uint256 totalVotes = add(motion.votes[NAY], motion.votes[YAY]); @@ -550,29 +558,40 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { repPenalty = sub(realStake, stakerReward); } - // Your side fully staked, receive 10% (proportional) of loser's stake - } else if (motion.stakes[_vote] == requiredStake) { + // Determine rewards based on stakes alone + } else { - uint256 loserStake = sub(motion.stakes[flip(_vote)], motion.paidVoterComp); - uint256 totalPenalty = wmul(loserStake, WAD / 10); + assert(motion.paidVoterComp == 0); - stakerReward = wmul(stakeFraction, add(requiredStake, totalPenalty)); + // Your side fully staked, receive 10% (proportional) of loser's stake + if ( + motion.stakes[_vote] == requiredStake && + motion.stakes[flip(_vote)] < requiredStake + ) { - // Opponent's side fully staked, pay 10% penalty - } else if (motion.stakes[flip(_vote)] == requiredStake) { + uint256 loserStake = motion.stakes[flip(_vote)]; + uint256 totalPenalty = wmul(loserStake, WAD / 10); - uint256 loserStake = sub(motion.stakes[_vote], motion.paidVoterComp); - uint256 totalPenalty = wmul(loserStake, WAD / 10); + stakerReward = wmul(stakeFraction, add(requiredStake, totalPenalty)); - stakerReward = wmul(stakeFraction, sub(loserStake, totalPenalty)); - repPenalty = sub(realStake, stakerReward); + // Opponent's side fully staked, pay 10% penalty + } else if ( + motion.stakes[_vote] < requiredStake && + motion.stakes[flip(_vote)] == requiredStake + ) { - // Neither side fully staked, no reward or penalty - } else { + uint256 loserStake = motion.stakes[_vote]; + uint256 totalPenalty = wmul(loserStake, WAD / 10); + + stakerReward = wmul(stakeFraction, sub(loserStake, totalPenalty)); + repPenalty = sub(realStake, stakerReward); - uint256 totalStake = add(motion.stakes[NAY], motion.stakes[YAY]); - uint256 rewardShare = wdiv(sub(totalStake, motion.paidVoterComp), totalStake); - stakerReward = wmul(realStake, rewardShare); + // Neither side fully staked (or no votes were revealed), no reward or penalty + } else { + + stakerReward = realStake; + + } } tokenLocking.transfer(token, stakerReward, _user, true); @@ -800,7 +819,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { // Hash all but last (value) bytes32 // Recall: mload(action) gives length of bytes array // So skip past the three bytes32 (length + domain proof), - // and the last bytes32 (value). + // and the last bytes32 (value), plus 4 bytes for the sig. hash := keccak256(add(action, 0x64), sub(mload(action), 0x64)) } } diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index dee6b274bd..3fb5afd68c 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -817,7 +817,7 @@ contract("Voting Reputation", (accounts) => { await forwardTime(STAKE_PERIOD, this); - await checkErrorRevert(voting.finalizeMotion(motionId), "voting-rep-motion-not-executable"); + await checkErrorRevert(voting.finalizeMotion(motionId), "voting-rep-motion-not-finalizable"); }); it("can take an action if there is insufficient opposition", async () => { @@ -886,13 +886,13 @@ contract("Voting Reputation", (accounts) => { motionState = await voting.getMotionState(motionId); expect(motionState).to.eq.BN(STAKING); - await checkErrorRevert(voting.finalizeMotion(motionId), "voting-rep-motion-not-executable"); + await checkErrorRevert(voting.finalizeMotion(motionId), "voting-rep-motion-not-finalizable"); await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); motionState = await voting.getMotionState(motionId); expect(motionState).to.eq.BN(SUBMIT); - await checkErrorRevert(voting.finalizeMotion(motionId), "voting-rep-motion-not-executable"); + await checkErrorRevert(voting.finalizeMotion(motionId), "voting-rep-motion-not-finalizable"); }); it("cannot take an action twice", async () => { @@ -903,7 +903,7 @@ contract("Voting Reputation", (accounts) => { const { logs } = await voting.finalizeMotion(motionId); expect(logs[0].args.executed).to.be.true; - await checkErrorRevert(voting.finalizeMotion(motionId), "voting-rep-motion-not-executable"); + await checkErrorRevert(voting.finalizeMotion(motionId), "voting-rep-motion-not-finalizable"); }); it("can take an action if the motion passes", async () => { From 6ae334158c954d14a5fec92d30416d3909093b2c Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 14 Aug 2020 14:30:25 +0100 Subject: [PATCH 41/61] Allow setExpenditureState to edit globalClaimDelay --- contracts/colony/ColonyExpenditure.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/colony/ColonyExpenditure.sol b/contracts/colony/ColonyExpenditure.sol index 6f60645584..0610376cba 100644 --- a/contracts/colony/ColonyExpenditure.sol +++ b/contracts/colony/ColonyExpenditure.sol @@ -208,7 +208,7 @@ contract ColonyExpenditure is ColonyStorage { if (_storageSlot == EXPENDITURES_SLOT) { uint256 offset = uint256(_keys[0]); require(_keys.length == 1, "colony-expenditure-bad-keys"); - require(offset == 0 || offset == 3, "colony-expenditure-bad-offset"); + require(offset == 0 || offset == 3 || offset == 4, "colony-expenditure-bad-offset"); } executeStateChange(keccak256(abi.encode(_id, _storageSlot)), _mask, _keys, _value); From ae184a0dcf57eed8e13c0ef557cb21b071934be3 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Tue, 18 Aug 2020 11:09:03 -0700 Subject: [PATCH 42/61] Make globalClaimDelay relevant --- contracts/colony/ColonyExpenditure.sol | 2 ++ contracts/colony/ColonyFunding.sol | 16 +++++++-- test/contracts-network/colony-expenditure.js | 34 +++++++++++--------- 3 files changed, 33 insertions(+), 19 deletions(-) diff --git a/contracts/colony/ColonyExpenditure.sol b/contracts/colony/ColonyExpenditure.sol index 0610376cba..38c65583e5 100644 --- a/contracts/colony/ColonyExpenditure.sol +++ b/contracts/colony/ColonyExpenditure.sol @@ -145,6 +145,7 @@ contract ColonyExpenditure is ColonyStorage { emit ExpenditureSkillSet(_id, _slot, _skillId); } + // Can deprecate function setExpenditurePayoutModifier( uint256 _permissionDomainId, uint256 _childSkillIndex, @@ -162,6 +163,7 @@ contract ColonyExpenditure is ColonyStorage { expenditureSlots[_id][_slot].payoutModifier = _payoutModifier; } + // Can deprecate function setExpenditureClaimDelay( uint256 _permissionDomainId, uint256 _childSkillIndex, diff --git a/contracts/colony/ColonyFunding.sol b/contracts/colony/ColonyFunding.sol index 0c867a423b..b94e0933c8 100755 --- a/contracts/colony/ColonyFunding.sol +++ b/contracts/colony/ColonyFunding.sol @@ -95,6 +95,9 @@ contract ColonyFunding is ColonyStorage, PatriciaTreeProofs { // ignore-swc-123 } } + int256 constant MAX_PAYOUT_MODIFIER = int256(WAD); + int256 constant MIN_PAYOUT_MODIFIER = -int256(WAD); + function claimExpenditurePayout(uint256 _id, uint256 _slot, address _token) public stoppable expenditureExists(_id) @@ -102,17 +105,24 @@ contract ColonyFunding is ColonyStorage, PatriciaTreeProofs { // ignore-swc-123 { Expenditure storage expenditure = expenditures[_id]; ExpenditureSlot storage slot = expenditureSlots[_id][_slot]; - require(add(expenditure.finalizedTimestamp, slot.claimDelay) <= now, "colony-expenditure-cannot-claim"); + + require( + add(expenditure.finalizedTimestamp, add(expenditure.globalClaimDelay, slot.claimDelay)) <= now, + "colony-expenditure-cannot-claim" + ); FundingPot storage fundingPot = fundingPots[expenditure.fundingPotId]; assert(fundingPot.balance[_token] >= fundingPot.payouts[_token]); uint256 initialPayout = expenditureSlotPayouts[_id][_slot][_token]; - uint256 payoutScalar = uint256(slot.payoutModifier + int256(WAD)); + delete expenditureSlotPayouts[_id][_slot][_token]; + + int256 payoutModifier = imin(imax(slot.payoutModifier, MIN_PAYOUT_MODIFIER), MAX_PAYOUT_MODIFIER); + uint256 payoutScalar = uint256(payoutModifier + int256(WAD)); + uint256 repPayout = wmul(initialPayout, payoutScalar); uint256 tokenPayout = min(initialPayout, repPayout); uint256 tokenSurplus = sub(initialPayout, tokenPayout); - expenditureSlotPayouts[_id][_slot][_token] = 0; // Process reputation updates if own token if (_token == token) { diff --git a/test/contracts-network/colony-expenditure.js b/test/contracts-network/colony-expenditure.js index f56ac8165d..3c5a113ee9 100644 --- a/test/contracts-network/colony-expenditure.js +++ b/test/contracts-network/colony-expenditure.js @@ -27,6 +27,9 @@ contract("Colony Expenditure", (accounts) => { const CANCELLED = 1; const FINALIZED = 2; + const MAPPING = false; + const ARRAY = true; + const RECIPIENT = accounts[3]; const ADMIN = accounts[4]; const ARBITRATOR = accounts[5]; @@ -503,13 +506,15 @@ contract("Colony Expenditure", (accounts) => { it("should delay claims by claimDelay", async () => { await colony.setExpenditurePayout(expenditureId, SLOT0, token.address, WAD, { from: ADMIN }); await colony.setExpenditureClaimDelay(1, UINT256_MAX, expenditureId, SLOT0, SECONDS_PER_DAY); + await colony.setExpenditureState(1, UINT256_MAX, expenditureId, 25, [ARRAY], [bn2bytes32(new BN(4))], bn2bytes32(new BN(SECONDS_PER_DAY))); const expenditure = await colony.getExpenditure(expenditureId); await colony.moveFundsBetweenPots(1, UINT256_MAX, UINT256_MAX, domain1.fundingPotId, expenditure.fundingPotId, WAD, token.address); await colony.finalizeExpenditure(expenditureId, { from: ADMIN }); await checkErrorRevert(colony.claimExpenditurePayout(expenditureId, SLOT0, token.address), "colony-expenditure-cannot-claim"); - + await forwardTime(SECONDS_PER_DAY, this); + await checkErrorRevert(colony.claimExpenditurePayout(expenditureId, SLOT0, token.address), "colony-expenditure-cannot-claim"); await forwardTime(SECONDS_PER_DAY, this); await colony.claimExpenditurePayout(expenditureId, SLOT0, token.address); }); @@ -552,16 +557,13 @@ contract("Colony Expenditure", (accounts) => { describe("when arbitrating expenditures", () => { let expenditureId; - const MAPPING = false; - const OFFSET = true; - beforeEach(async () => { await colony.makeExpenditure(1, UINT256_MAX, 1, { from: ADMIN }); expenditureId = await colony.getExpenditureCount(); }); it("should allow arbitration users to update expenditure status/owner", async () => { - const mask = [OFFSET]; + const mask = [ARRAY]; const keys = [bn2bytes32(new BN(0))]; const value = bn2bytes32(new BN(USER.slice(2), 16), 62) + new BN(CANCELLED).toString(16, 2); @@ -573,7 +575,7 @@ contract("Colony Expenditure", (accounts) => { }); it("should not allow arbitration users to update expenditure fundingPotId", async () => { - const mask = [OFFSET]; + const mask = [ARRAY]; const keys = [bn2bytes32(new BN(1))]; const value = "0x0"; @@ -584,7 +586,7 @@ contract("Colony Expenditure", (accounts) => { }); it("should not allow arbitration users to update expenditure domainId", async () => { - const mask = [OFFSET]; + const mask = [ARRAY]; const keys = [bn2bytes32(new BN(2))]; const value = "0x0"; @@ -595,7 +597,7 @@ contract("Colony Expenditure", (accounts) => { }); it("should allow arbitration users to update expenditure finalizedTimestamp", async () => { - const mask = [OFFSET]; + const mask = [ARRAY]; const keys = [bn2bytes32(new BN(3))]; const value = bn2bytes32(new BN(100)); @@ -617,7 +619,7 @@ contract("Colony Expenditure", (accounts) => { }); it("should allow arbitration users to update expenditure slot claimDelay", async () => { - const mask = [MAPPING, OFFSET]; + const mask = [MAPPING, ARRAY]; const keys = ["0x0", bn2bytes32(new BN(1))]; const value = bn2bytes32(new BN(100)); @@ -628,7 +630,7 @@ contract("Colony Expenditure", (accounts) => { }); it("should allow arbitration users to update expenditure slot payoutModifier", async () => { - const mask = [MAPPING, OFFSET]; + const mask = [MAPPING, ARRAY]; const keys = ["0x0", bn2bytes32(new BN(2))]; const value = bn2bytes32(new BN(100)); @@ -641,7 +643,7 @@ contract("Colony Expenditure", (accounts) => { it("should allow arbitration users to update expenditure slot skills", async () => { await colony.setExpenditureSkill(expenditureId, 0, GLOBAL_SKILL_ID, { from: ADMIN }); - const mask = [MAPPING, OFFSET, OFFSET]; + const mask = [MAPPING, ARRAY, ARRAY]; const keys = ["0x0", bn2bytes32(new BN(3)), bn2bytes32(new BN(0))]; const value = bn2bytes32(new BN(100)); @@ -659,14 +661,14 @@ contract("Colony Expenditure", (accounts) => { expect(expenditureSlot.skills[0]).to.eq.BN(GLOBAL_SKILL_ID); // Lengthen the array - let mask = [MAPPING, OFFSET]; + let mask = [MAPPING, ARRAY]; let keys = ["0x0", bn2bytes32(new BN(3))]; let value = bn2bytes32(new BN(2)); await colony.setExpenditureState(1, UINT256_MAX, expenditureId, EXPENDITURESLOTS_SLOT, mask, keys, value, { from: ARBITRATOR }); // Set the new skillId - mask = [MAPPING, OFFSET, OFFSET]; + mask = [MAPPING, ARRAY, ARRAY]; keys = ["0x0", bn2bytes32(new BN(3)), bn2bytes32(new BN(1))]; value = bn2bytes32(new BN(100)); @@ -678,7 +680,7 @@ contract("Colony Expenditure", (accounts) => { expect(expenditureSlot.skills[1]).to.eq.BN(100); // Shrink the array - mask = [MAPPING, OFFSET]; + mask = [MAPPING, ARRAY]; keys = ["0x0", bn2bytes32(new BN(3))]; value = bn2bytes32(new BN(1)); @@ -712,7 +714,7 @@ contract("Colony Expenditure", (accounts) => { }); it("should not allow arbitration users to pass invalid slots", async () => { - const mask = [OFFSET]; + const mask = [ARRAY]; const keys = ["0x0"]; const value = "0x0"; const invalidSlot = 10; @@ -735,7 +737,7 @@ contract("Colony Expenditure", (accounts) => { }); it("should not allow arbitration users to pass offsets greater than 1024", async () => { - const mask = [OFFSET]; + const mask = [ARRAY]; const keys = [bn2bytes32(new BN(1025))]; const value = bn2bytes32(new BN(100)); From ffdbe5732a45218855a34f37ffcf1d4d2dd2401d Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Tue, 18 Aug 2020 12:09:46 -0700 Subject: [PATCH 43/61] Add check for valid motionId in Voting --- contracts/extensions/VotingReputation.sol | 17 +++++--- test/extensions/voting-rep.js | 52 ++++++++++++++++++----- 2 files changed, 53 insertions(+), 16 deletions(-) diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index cd4c303e2d..873173112c 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -155,7 +155,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { } // Data structures - enum MotionState { Staking, Submit, Reveal, Closed, Finalizable, Finalized, Failed } + enum MotionState { Null, Staking, Submit, Reveal, Closed, Finalizable, Finalized, Failed } struct Motion { uint64[3] events; // For recording motion lifecycle timestamps (STAKE, SUBMIT, REVEAL) @@ -264,14 +264,14 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { { Motion storage motion = motions[_motionId]; require(_vote <= 1, "voting-rep-bad-vote"); - require(getMotionState(_motionId) == MotionState.Staking, "voting-rep-staking-closed"); + require(getMotionState(_motionId) == MotionState.Staking, "voting-rep-motion-not-staking"); uint256 requiredStake = getRequiredStake(_motionId); uint256 amount = min(_amount, sub(requiredStake, motion.stakes[_vote])); - uint256 stakerTotalAmount = add(stakes[_motionId][msg.sender][_vote], amount); - require(amount > 0, "voting-rep-bad-amount"); + uint256 stakerTotalAmount = add(stakes[_motionId][msg.sender][_vote], amount); + require( stakerTotalAmount <= getReputationFromProof(_motionId, msg.sender, _key, _value, _branchMask, _siblings), "voting-rep-insufficient-rep" @@ -527,7 +527,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { require( getMotionState(_motionId) == MotionState.Finalized || getMotionState(_motionId) == MotionState.Failed, - "voting-rep-not-failed-or-finalized" + "voting-rep-motion-not-claimable" ); uint256 stakeFraction = wdiv( @@ -653,8 +653,13 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { Motion storage motion = motions[_motionId]; uint256 requiredStake = getRequiredStake(_motionId); + // Check for valid motion Id + if (_motionId == 0 || _motionId > motionCount) { + + return MotionState.Null; + // If finalized, we're done - if (motion.finalized) { + } else if (motion.finalized) { return MotionState.Finalized; diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index 3fb5afd68c..f210c3a1d8 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -87,13 +87,14 @@ contract("Voting Reputation", (accounts) => { const NAY = 0; const YAY = 1; - const STAKING = 0; - const SUBMIT = 1; - // const REVEAL = 2; - // const CLOSED = 3; - // const EXECUTABLE = 4; - // const EXECUTED = 5; - const FAILED = 6; + // const NULL = 0; + const STAKING = 1; + const SUBMIT = 2; + // const REVEAL = 3; + // const CLOSED = 4; + // const EXECUTABLE = 5; + // const EXECUTED = 6; + const FAILED = 7; const ADDRESS_ZERO = ethers.constants.AddressZero; const REQUIRED_STAKE = WAD.muln(3).divn(1000); @@ -372,6 +373,13 @@ contract("Voting Reputation", (accounts) => { expect(lock.balance).to.eq.BN(REQUIRED_STAKE.muln(2)); }); + it("cannot stake on a non-existent motion", async () => { + await checkErrorRevert( + voting.stakeMotion(0, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), + "voting-rep-motion-not-staking" + ); + }); + it("cannot stake 0", async () => { await checkErrorRevert( voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, 0, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), @@ -604,12 +612,12 @@ contract("Voting Reputation", (accounts) => { await checkErrorRevert( voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), - "voting-rep-staking-closed" + "voting-rep-motion-not-staking" ); await checkErrorRevert( voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }), - "voting-rep-staking-closed" + "voting-rep-motion-not-staking" ); }); }); @@ -727,6 +735,13 @@ contract("Voting Reputation", (accounts) => { await voting.revealVote(motionId2, SALT, NAY, user0Key, user0Value2, user0Mask2, user0Siblings2, { from: USER0 }); }); + it("cannot submit a vote on a non-existent motion", async () => { + await checkErrorRevert( + voting.submitVote(0, "0x0", user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), + "voting-rep-motion-not-open" + ); + }); + it("cannot submit a null vote", async () => { await checkErrorRevert( voting.submitVote(motionId, "0x0", user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), @@ -743,6 +758,15 @@ contract("Voting Reputation", (accounts) => { ); }); + it("cannot reveal a vote on a non-existent motion", async () => { + await forwardTime(SUBMIT_PERIOD, this); + + await checkErrorRevert( + voting.revealVote(0, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), + "voting-rep-motion-not-reveal" + ); + }); + it("cannot reveal a vote if voting is open", async () => { await voting.submitVote(motionId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await checkErrorRevert(voting.revealVote(motionId, SALT, YAY, FAKE, FAKE, 0, [], { from: USER0 }), "voting-rep-motion-not-reveal"); @@ -810,6 +834,10 @@ contract("Voting Reputation", (accounts) => { motionId = await voting.getMotionCount(); }); + it("cannot execute a non-existent motion", async () => { + await checkErrorRevert(voting.finalizeMotion(0), "voting-rep-motion-not-finalizable"); + }); + it("cannot take an action if there is insufficient support", async () => { await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE.subn(1), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0, @@ -1042,6 +1070,10 @@ contract("Voting Reputation", (accounts) => { motionId = await voting.getMotionCount(); }); + it("cannot claim rewards from a non-existent motion", async () => { + await checkErrorRevert(voting.claimReward(0, 1, UINT256_MAX, USER0, YAY), "voting-rep-motion-not-claimable"); + }); + it("can let stakers claim rewards, based on the stake outcome", async () => { const addr = await colonyNetwork.getReputationMiningCycle(false); const repCycle = await IReputationMiningCycle.at(addr); @@ -1263,7 +1295,7 @@ contract("Voting Reputation", (accounts) => { }); it("cannot claim rewards before a motion is finalized", async () => { - await checkErrorRevert(voting.claimReward(motionId, 1, UINT256_MAX, USER0, YAY), "voting-rep-not-failed-or-finalized"); + await checkErrorRevert(voting.claimReward(motionId, 1, UINT256_MAX, USER0, YAY), "voting-rep-motion-not-claimable"); }); }); From e7a99b56edfe4eb4cc2a09a5680f70a9960f06f2 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Tue, 18 Aug 2020 12:12:00 -0700 Subject: [PATCH 44/61] Respond to reviewer comments VIII --- contracts/extensions/VotingReputation.sol | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index 873173112c..76f5211687 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -284,6 +284,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { colony.obligateStake(msg.sender, motion.domainId, amount); colony.transferStake(_permissionDomainId, _childSkillIndex, address(this), msg.sender, motion.domainId, amount, address(this)); + tokenLocking.claim(token, true); // Update the stake motion.stakes[_vote] = add(motion.stakes[_vote], amount); @@ -322,7 +323,6 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { motion.events[STAKE_END] = uint64(now); motion.events[SUBMIT_END] = uint64(now + submitPeriod); motion.events[REVEAL_END] = uint64(now + submitPeriod + revealPeriod); - tokenLocking.claim(token, true); emit MotionEventSet(_motionId, STAKE_END); } @@ -461,6 +461,10 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { motion.escalated = true; emit MotionEscalated(_motionId, msg.sender, _newDomainId); + + if (motion.events[STAKE_END] == uint64(now)) { + emit MotionEventSet(_motionId, STAKE_END); + } } function finalizeMotion(uint256 _motionId) public { From 7f475f159cd3913d354f16aaf20e875675fca015 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Wed, 19 Aug 2020 13:23:41 -0700 Subject: [PATCH 45/61] Update setExpenditureState to explicitly check offsets --- contracts/colony/ColonyExpenditure.sol | 16 ++++++++++--- test/contracts-network/colony-expenditure.js | 25 +++++++------------- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/contracts/colony/ColonyExpenditure.sol b/contracts/colony/ColonyExpenditure.sol index 38c65583e5..08693df29e 100644 --- a/contracts/colony/ColonyExpenditure.sol +++ b/contracts/colony/ColonyExpenditure.sol @@ -196,8 +196,6 @@ contract ColonyExpenditure is ColonyStorage { expenditureExists(_id) authDomain(_permissionDomainId, _childSkillIndex, expenditures[_id].domainId) { - require(_keys.length > 0, "colony-expenditure-no-keys"); - require( _storageSlot == EXPENDITURES_SLOT || _storageSlot == EXPENDITURESLOTS_SLOT || @@ -208,11 +206,23 @@ contract ColonyExpenditure is ColonyStorage { // Only allow editing expenditure status, owner, and finalizedTimestamp // Note that status + owner occupy one slot if (_storageSlot == EXPENDITURES_SLOT) { - uint256 offset = uint256(_keys[0]); require(_keys.length == 1, "colony-expenditure-bad-keys"); + uint256 offset = uint256(_keys[0]); require(offset == 0 || offset == 3 || offset == 4, "colony-expenditure-bad-offset"); } + // Explicitly whitelist all slots, in case we add new slots in the future + if (_storageSlot == EXPENDITURESLOTS_SLOT) { + require(_keys.length >= 2, "colony-expenditure-bad-keys"); + uint256 offset = uint256(_keys[1]); + require(offset <= 3, "colony-expenditure-bad-offset"); + } + + // Should always be two mappings + if (_storageSlot == EXPENDITURESLOTPAYOUTS_SLOT) { + require(_keys.length == 2, "colony-expenditure-bad-keys"); + } + executeStateChange(keccak256(abi.encode(_id, _storageSlot)), _mask, _keys, _value); } diff --git a/test/contracts-network/colony-expenditure.js b/test/contracts-network/colony-expenditure.js index 3c5a113ee9..eef6a2d01d 100644 --- a/test/contracts-network/colony-expenditure.js +++ b/test/contracts-network/colony-expenditure.js @@ -505,8 +505,10 @@ contract("Colony Expenditure", (accounts) => { it("should delay claims by claimDelay", async () => { await colony.setExpenditurePayout(expenditureId, SLOT0, token.address, WAD, { from: ADMIN }); - await colony.setExpenditureClaimDelay(1, UINT256_MAX, expenditureId, SLOT0, SECONDS_PER_DAY); - await colony.setExpenditureState(1, UINT256_MAX, expenditureId, 25, [ARRAY], [bn2bytes32(new BN(4))], bn2bytes32(new BN(SECONDS_PER_DAY))); + + const day32 = bn2bytes32(new BN(SECONDS_PER_DAY)); + await colony.setExpenditureState(1, UINT256_MAX, expenditureId, EXPENDITURES_SLOT, [ARRAY], [bn2bytes32(new BN(4))], day32); + await colony.setExpenditureState(1, UINT256_MAX, expenditureId, EXPENDITURESLOTS_SLOT, [MAPPING, ARRAY], ["0x0", bn2bytes32(new BN(1))], day32); const expenditure = await colony.getExpenditure(expenditureId); await colony.moveFundsBetweenPots(1, UINT256_MAX, UINT256_MAX, domain1.fundingPotId, expenditure.fundingPotId, WAD, token.address); @@ -608,8 +610,8 @@ contract("Colony Expenditure", (accounts) => { }); it("should allow arbitration users to update expenditure slot recipient", async () => { - const mask = [MAPPING]; - const keys = ["0x0"]; + const mask = [MAPPING, ARRAY]; + const keys = ["0x0", "0x0"]; const value = bn2bytes32(new BN(USER.slice(2), 16)); await colony.setExpenditureState(1, UINT256_MAX, expenditureId, EXPENDITURESLOTS_SLOT, mask, keys, value, { from: ARBITRATOR }); @@ -702,17 +704,6 @@ contract("Colony Expenditure", (accounts) => { expect(expenditureSlotPayout).to.eq.BN(100); }); - it("should not allow arbitration users to pass empty keys", async () => { - const mask = []; - const keys = []; - const value = "0x0"; - - await checkErrorRevert( - colony.setExpenditureState(1, UINT256_MAX, expenditureId, EXPENDITURES_SLOT, mask, keys, value, { from: ARBITRATOR }), - "colony-expenditure-no-keys" - ); - }); - it("should not allow arbitration users to pass invalid slots", async () => { const mask = [ARRAY]; const keys = ["0x0"]; @@ -737,8 +728,8 @@ contract("Colony Expenditure", (accounts) => { }); it("should not allow arbitration users to pass offsets greater than 1024", async () => { - const mask = [ARRAY]; - const keys = [bn2bytes32(new BN(1025))]; + const mask = [MAPPING, ARRAY, ARRAY]; + const keys = ["0x0", bn2bytes32(new BN(3)), bn2bytes32(new BN(1025))]; const value = bn2bytes32(new BN(100)); await checkErrorRevert( From 565caf91714b245fd8f05f276cbc51226a71029a Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Wed, 19 Aug 2020 16:47:05 -0700 Subject: [PATCH 46/61] Update expenditure locking and add an interaction test --- contracts/extensions/VotingReputation.sol | 5 +- test/extensions/voting-rep.js | 67 ++++++++++++++++++++++- 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index 76f5211687..e7898c2595 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -299,7 +299,8 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { ) { bytes32 structHash = hashExpenditureStruct(motion.action); expenditureMotionCounts[structHash] = add(expenditureMotionCounts[structHash], 1); - bytes memory claimDelayAction = createClaimDelayAction(motion.action, UINT256_MAX); + // Set to UINT256_MAX / 3 to avoid overflow (finalizedTimestamp + globalClaimDelay + claimDelay) + bytes memory claimDelayAction = createClaimDelayAction(motion.action, UINT256_MAX / 3); require(executeCall(_motionId, claimDelayAction), "voting-rep-expenditure-lock-failed"); } @@ -318,7 +319,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { emit MotionEventSet(_motionId, STAKE_END); } - // Claim tokens once both sides are fully staked + // Move to vote submission once both sides are fully staked if (motion.stakes[YAY] == requiredStake && motion.stakes[NAY] == requiredStake) { motion.events[STAKE_END] = uint64(now); motion.events[SUBMIT_END] = uint64(now + submitPeriod); diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index f210c3a1d8..70b3e1d43a 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -414,8 +414,9 @@ contract("Voting Reputation", (accounts) => { it("can update the expenditure globalClaimDelay if voting on expenditure state", async () => { await colony.makeExpenditure(1, UINT256_MAX, 1); const expenditureId = await colony.getExpenditureCount(); + await colony.finalizeExpenditure(expenditureId); - // Set payoutModifier to 1 for expenditure slot 0 + // Set finalizedTimestamp to WAD const action = await encodeTxData(colony, "setExpenditureState", [1, UINT256_MAX, expenditureId, 25, [true], [bn2bytes32(new BN(3))], WAD32]); await voting.createDomainMotion(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); @@ -436,11 +437,14 @@ contract("Voting Reputation", (accounts) => { expenditureSlot = await colony.getExpenditure(expenditureId); expect(expenditureSlot.globalClaimDelay).to.eq.BN(UINT256_MAX); + + await checkErrorRevert(colony.claimExpenditurePayout(expenditureId, 0, token.address), "colony-expenditure-cannot-claim"); }); it("can update the expenditure slot claimDelay if voting on expenditure slot state", async () => { await colony.makeExpenditure(1, UINT256_MAX, 1); const expenditureId = await colony.getExpenditureCount(); + await colony.finalizeExpenditure(expenditureId); // Set payoutModifier to 1 for expenditure slot 0 const action = await encodeTxData(colony, "setExpenditureState", [ @@ -471,11 +475,14 @@ contract("Voting Reputation", (accounts) => { expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); expect(expenditureSlot.claimDelay).to.eq.BN(UINT256_MAX); + + await checkErrorRevert(colony.claimExpenditurePayout(expenditureId, 0, token.address), "colony-expenditure-cannot-claim"); }); it("can update the expenditure slot claimDelay if voting on expenditure payout state", async () => { await colony.makeExpenditure(1, UINT256_MAX, 1); const expenditureId = await colony.getExpenditureCount(); + await colony.finalizeExpenditure(expenditureId); // Set payout to WAD for expenditure slot 0, internal token const action = await encodeTxData(colony, "setExpenditureState", [ @@ -506,6 +513,64 @@ contract("Voting Reputation", (accounts) => { expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); expect(expenditureSlot.claimDelay).to.eq.BN(UINT256_MAX); + + await checkErrorRevert(colony.claimExpenditurePayout(expenditureId, 0, token.address), "colony-expenditure-cannot-claim"); + }); + + it("can update the expenditure slot claimDelay if voting on multiple expenditure states", async () => { + await colony.makeExpenditure(1, UINT256_MAX, 1); + const expenditureId = await colony.getExpenditureCount(); + await colony.finalizeExpenditure(expenditureId); + + let action; + + // Motion 1 + // Set finalizedTimestamp to WAD + action = await encodeTxData(colony, "setExpenditureState", [1, UINT256_MAX, expenditureId, 25, [true], [bn2bytes32(new BN(3))], WAD32]); + + await voting.createDomainMotion(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + motionId = await voting.getMotionCount(); + await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + + // Motion 2 + // Set payoutModifier to 1 for expenditure slot 0 + action = await encodeTxData(colony, "setExpenditureState", [ + 1, + UINT256_MAX, + expenditureId, + 26, + [false, true], + ["0x0", bn2bytes32(new BN(2))], + WAD32, + ]); + + await voting.createDomainMotion(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + motionId = await voting.getMotionCount(); + await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + + // Motion 2 + // Set payout to WAD for expenditure slot 0, internal token + action = await encodeTxData(colony, "setExpenditureState", [ + 1, + UINT256_MAX, + expenditureId, + 27, + [false, false], + ["0x0", bn2bytes32(new BN(token.address.slice(2), 16))], + WAD32, + ]); + + await voting.createDomainMotion(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + motionId = await voting.getMotionCount(); + await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + + const expenditure = await colony.getExpenditure(expenditureId); + expect(expenditure.globalClaimDelay).to.eq.BN(UINT256_MAX.divn(3)); + + const expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); + expect(expenditureSlot.claimDelay).to.eq.BN(UINT256_MAX.divn(3)); + + await checkErrorRevert(colony.claimExpenditurePayout(expenditureId, 0, token.address), "colony-expenditure-cannot-claim"); }); it("cannot update the expenditure slot claimDelay if given an invalid action", async () => { From 9be918cab5e725eccae7834c55f6efa408eef67a Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 20 Aug 2020 10:52:11 +0100 Subject: [PATCH 47/61] Adjust remaining tests for new delay value --- test/extensions/voting-rep.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index 70b3e1d43a..5dc6bd6cf8 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -426,17 +426,17 @@ contract("Voting Reputation", (accounts) => { expenditureMotionCount = await voting.getExpenditureMotionCount(soliditySha3(expenditureId)); expect(expenditureMotionCount).to.be.zero; - let expenditureSlot; - expenditureSlot = await colony.getExpenditure(expenditureId); - expect(expenditureSlot.globalClaimDelay).to.be.zero; + let expenditure; + expenditure = await colony.getExpenditure(expenditureId); + expect(expenditure.globalClaimDelay).to.be.zero; await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); expenditureMotionCount = await voting.getExpenditureMotionCount(soliditySha3(expenditureId)); expect(expenditureMotionCount).to.eq.BN(1); - expenditureSlot = await colony.getExpenditure(expenditureId); - expect(expenditureSlot.globalClaimDelay).to.eq.BN(UINT256_MAX); + expenditure = await colony.getExpenditure(expenditureId); + expect(expenditure.globalClaimDelay).to.eq.BN(UINT256_MAX.divn(3)); await checkErrorRevert(colony.claimExpenditurePayout(expenditureId, 0, token.address), "colony-expenditure-cannot-claim"); }); @@ -474,7 +474,7 @@ contract("Voting Reputation", (accounts) => { expect(expenditureMotionCount).to.eq.BN(1); expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); - expect(expenditureSlot.claimDelay).to.eq.BN(UINT256_MAX); + expect(expenditureSlot.claimDelay).to.eq.BN(UINT256_MAX.divn(3)); await checkErrorRevert(colony.claimExpenditurePayout(expenditureId, 0, token.address), "colony-expenditure-cannot-claim"); }); @@ -512,7 +512,7 @@ contract("Voting Reputation", (accounts) => { expect(expenditureMotionCount).to.eq.BN(1); expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); - expect(expenditureSlot.claimDelay).to.eq.BN(UINT256_MAX); + expect(expenditureSlot.claimDelay).to.eq.BN(UINT256_MAX.divn(3)); await checkErrorRevert(colony.claimExpenditurePayout(expenditureId, 0, token.address), "colony-expenditure-cannot-claim"); }); @@ -634,7 +634,7 @@ contract("Voting Reputation", (accounts) => { expect(expenditureMotionCount).to.eq.BN(2); expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); - expect(expenditureSlot.claimDelay).to.eq.BN(UINT256_MAX); + expect(expenditureSlot.claimDelay).to.eq.BN(UINT256_MAX.divn(3)); await forwardTime(STAKE_PERIOD, this); await voting.finalizeMotion(motionId1); @@ -643,7 +643,7 @@ contract("Voting Reputation", (accounts) => { expect(expenditureMotionCount).to.eq.BN(1); expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); - expect(expenditureSlot.claimDelay).to.eq.BN(UINT256_MAX); + expect(expenditureSlot.claimDelay).to.eq.BN(UINT256_MAX.divn(3)); await voting.finalizeMotion(motionId2); From a171b8945ca04b66cc8cf377a8891e763222f9a4 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Fri, 21 Aug 2020 11:22:20 -0700 Subject: [PATCH 48/61] Respond to reviewer comments IX --- contracts/extensions/VotingReputation.sol | 36 ++++++++++++----------- scripts/check-recovery.js | 1 - test/extensions/voting-rep.js | 30 +++++++++++++++++-- 3 files changed, 46 insertions(+), 21 deletions(-) diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index e7898c2595..8afcf44f51 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -295,7 +295,8 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { _vote == YAY && !motion.escalated && motion.stakes[YAY] == requiredStake && - getSig(motion.action) == CHANGE_FUNCTION + getSig(motion.action) == CHANGE_FUNCTION && + (motion.target == address(0x0) || colonyNetwork.isColony(motion.target)) ) { bytes32 structHash = hashExpenditureStruct(motion.action); expenditureMotionCounts[structHash] = add(expenditureMotionCounts[structHash], 1); @@ -312,8 +313,8 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { (_vote == NAY && motion.stakes[NAY] == requiredStake && motion.stakes[YAY] < requiredStake) ) { motion.events[STAKE_END] = uint64(now + stakePeriod); - motion.events[SUBMIT_END] = uint64(now + stakePeriod + submitPeriod); - motion.events[REVEAL_END] = uint64(now + stakePeriod + submitPeriod + revealPeriod); + motion.events[SUBMIT_END] = motion.events[STAKE_END] + uint64(submitPeriod); + motion.events[REVEAL_END] = motion.events[SUBMIT_END] + uint64(revealPeriod); delete motion.votes; // New stake supersedes prior votes emit MotionEventSet(_motionId, STAKE_END); @@ -322,8 +323,8 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { // Move to vote submission once both sides are fully staked if (motion.stakes[YAY] == requiredStake && motion.stakes[NAY] == requiredStake) { motion.events[STAKE_END] = uint64(now); - motion.events[SUBMIT_END] = uint64(now + submitPeriod); - motion.events[REVEAL_END] = uint64(now + submitPeriod + revealPeriod); + motion.events[SUBMIT_END] = motion.events[STAKE_END] + uint64(submitPeriod); + motion.events[REVEAL_END] = motion.events[SUBMIT_END] + uint64(revealPeriod); emit MotionEventSet(_motionId, STAKE_END); } @@ -390,6 +391,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { { Motion storage motion = motions[_motionId]; require(getMotionState(_motionId) == MotionState.Reveal, "voting-rep-motion-not-reveal"); + require(_vote <= 1, "voting-rep-bad-vote"); uint256 userRep = getReputationFromProof(_motionId, msg.sender, _key, _value, _branchMask, _siblings); @@ -400,7 +402,6 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { motion.votes[_vote] = add(motion.votes[_vote], userRep); motion.repRevealed = add(motion.repRevealed, userRep); - uint256 fractionUserReputation = wdiv(userRep, motion.skillRep); uint256 totalStake = add(motion.stakes[YAY], motion.stakes[NAY]); uint256 voterReward = wmul(wmul(fractionUserReputation, totalStake), voterRewardFraction); @@ -457,7 +458,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { uint64(now + stakePeriod) : uint64(now); motion.events[SUBMIT_END] = motion.events[STAKE_END] + uint64(submitPeriod); - motion.events[REVEAL_END] = motion.events[STAKE_END] + uint64(submitPeriod + revealPeriod); + motion.events[REVEAL_END] = motion.events[SUBMIT_END] + uint64(revealPeriod); motion.escalated = true; @@ -719,21 +720,22 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { internal { require(state == ExtensionState.Active, "voting-rep-not-active"); + require(_target != address(colony), "voting-rep-target-cannot-be-colony"); motionCount += 1; + Motion storage motion = motions[motionCount]; - motions[motionCount].events[STAKE_END] = uint64(now + stakePeriod); - motions[motionCount].events[SUBMIT_END] = uint64(now + stakePeriod + submitPeriod); - motions[motionCount].events[REVEAL_END] = uint64(now + stakePeriod + submitPeriod + revealPeriod); + motion.events[STAKE_END] = uint64(now + stakePeriod); + motion.events[SUBMIT_END] = motion.events[STAKE_END] + uint64(submitPeriod); + motion.events[REVEAL_END] = motion.events[SUBMIT_END] + uint64(revealPeriod); - motions[motionCount].rootHash = colonyNetwork.getReputationRootHash(); - motions[motionCount].domainId = _domainId; - motions[motionCount].skillId = _skillId; + motion.rootHash = colonyNetwork.getReputationRootHash(); + motion.domainId = _domainId; + motion.skillId = _skillId; - uint256 skillRep = getReputationFromProof(motionCount, address(0x0), _key, _value, _branchMask, _siblings); - motions[motionCount].skillRep = skillRep; - motions[motionCount].target = _target; - motions[motionCount].action = _action; + motion.skillRep = getReputationFromProof(motionCount, address(0x0), _key, _value, _branchMask, _siblings); + motion.target = _target; + motion.action = _action; emit MotionCreated(motionCount, msg.sender, _domainId); } diff --git a/scripts/check-recovery.js b/scripts/check-recovery.js index 462fc5416a..8308ac101c 100644 --- a/scripts/check-recovery.js +++ b/scripts/check-recovery.js @@ -46,7 +46,6 @@ walkSync("./contracts/").forEach((contractName) => { "contracts/extensions/FundingQueueFactory.sol", "contracts/extensions/OneTxPayment.sol", "contracts/extensions/OneTxPaymentFactory.sol", - "contracts/extensions/VotingBase.sol", "contracts/extensions/VotingReputation.sol", "contracts/extensions/VotingReputationFactory.sol", "contracts/gnosis/MultiSigWallet.sol", diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index 5dc6bd6cf8..39f2e2d98a 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -7,7 +7,7 @@ import shortid from "shortid"; import { ethers } from "ethers"; import { soliditySha3 } from "web3-utils"; -import { UINT256_MAX, WAD, MINING_CYCLE_DURATION, SECONDS_PER_DAY, DEFAULT_STAKE } from "../../helpers/constants"; +import { UINT256_MAX, WAD, MINING_CYCLE_DURATION, SECONDS_PER_DAY, DEFAULT_STAKE, SUBMITTER_ONLY_WINDOW } from "../../helpers/constants"; import { checkErrorRevert, makeReputationKey, @@ -213,7 +213,7 @@ contract("Voting Reputation", (accounts) => { const rootHash = await reputationTree.getRootHash(); const repCycle = await getActiveRepCycle(colonyNetwork); - await forwardTime(MINING_CYCLE_DURATION, this); + await forwardTime(MINING_CYCLE_DURATION + SUBMITTER_ONLY_WINDOW + 1, this); await repCycle.submitRootHash(rootHash, 0, "0x00", 10, { from: MINER }); await repCycle.confirmNewHash(0); }); @@ -311,6 +311,19 @@ contract("Voting Reputation", (accounts) => { expect(motion.skillId).to.eq.BN(domain1.skillId); }); + it("can create a motion with an alternative target", async () => { + const action = await encodeTxData(colony, "makeTask", [1, 0, FAKE, 2, 0, 0]); + await voting.createRootMotion(voting.address, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + }); + + it("can create a motion with an alternative target", async () => { + const action = await encodeTxData(colony, "makeTask", [1, 0, FAKE, 2, 0, 0]); + await checkErrorRevert( + voting.createRootMotion(colony.address, action, domain1Key, domain1Value, domain1Mask, domain1Siblings), + "voting-rep-target-cannot-be-colony" + ); + }); + it("cannot externally escalate a domain motion with an invalid domain proof", async () => { const key = makeReputationKey(colony.address, domain3.skillId); const value = makeReputationValue(WAD, 7); @@ -751,6 +764,17 @@ contract("Voting Reputation", (accounts) => { expect(motion.repSubmitted).to.eq.BN(WAD); }); + it("cannot reveal an invalid vote", async () => { + await voting.submitVote(motionId, soliditySha3(SALT, 2), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + + await forwardTime(SUBMIT_PERIOD, this); + + await checkErrorRevert( + voting.revealVote(motionId, SALT, 2, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), + "voting-rep-bad-vote" + ); + }); + it("cannot reveal a vote twice, and so cannot vote twice", async () => { await voting.submitVote(motionId, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await voting.submitVote(motionId, soliditySha3(SALT, NAY), user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); @@ -780,7 +804,7 @@ contract("Voting Reputation", (accounts) => { const rootHash = await reputationTree.getRootHash(); expect(oldRootHash).to.not.equal(rootHash); - await forwardTime(MINING_CYCLE_DURATION, this); + await forwardTime(MINING_CYCLE_DURATION + SUBMITTER_ONLY_WINDOW + 1, this); const repCycle = await getActiveRepCycle(colonyNetwork); await repCycle.submitRootHash(rootHash, 0, "0x00", 10, { from: MINER }); From 9ae28ef845c68dbccc42127da10fae45beff0421 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 21 Aug 2020 09:00:38 +0100 Subject: [PATCH 49/61] Update voting tests for new mining requirements --- test/extensions/voting-rep.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index 39f2e2d98a..316bed1c61 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -215,6 +215,7 @@ contract("Voting Reputation", (accounts) => { const repCycle = await getActiveRepCycle(colonyNetwork); await forwardTime(MINING_CYCLE_DURATION + SUBMITTER_ONLY_WINDOW + 1, this); await repCycle.submitRootHash(rootHash, 0, "0x00", 10, { from: MINER }); + await forwardTime(SUBMITTER_ONLY_WINDOW + 1, this); await repCycle.confirmNewHash(0); }); @@ -808,6 +809,7 @@ contract("Voting Reputation", (accounts) => { const repCycle = await getActiveRepCycle(colonyNetwork); await repCycle.submitRootHash(rootHash, 0, "0x00", 10, { from: MINER }); + await forwardTime(SUBMITTER_ONLY_WINDOW + 1, this); await repCycle.confirmNewHash(0); // Create new motion with new reputation state From 6fb170afb2392f5f65ab7584a66cad52146a686b Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Mon, 24 Aug 2020 06:31:38 -0700 Subject: [PATCH 50/61] Rename target to altTarget --- contracts/extensions/VotingReputation.sol | 26 +++++++++++------------ test/extensions/voting-rep.js | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index 8afcf44f51..3add30512a 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -171,7 +171,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { uint256[2] votes; // [nay, yay] bool escalated; bool finalized; - address target; + address altTarget; bytes action; } @@ -187,14 +187,14 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { // Public functions (interface) /// @notice Create a motion in the root domain - /// @param _target The contract to which we send the action (0x0 for the colony) + /// @param _altTarget The contract to which we send the action (0x0 for the colony) /// @param _action A bytes array encoding a function call /// @param _key Reputation tree key for the root domain /// @param _value Reputation tree value for the root domain /// @param _branchMask The branchmask of the proof /// @param _siblings The siblings of the proof function createRootMotion( - address _target, + address _altTarget, bytes memory _action, bytes memory _key, bytes memory _value, @@ -204,13 +204,13 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { public { uint256 rootSkillId = colony.getDomain(1).skillId; - createMotion(_target, _action, 1, rootSkillId, _key, _value, _branchMask, _siblings); + createMotion(_altTarget, _action, 1, rootSkillId, _key, _value, _branchMask, _siblings); } /// @notice Create a motion in any domain /// @param _domainId The domain where we vote on the motion /// @param _childSkillIndex The childSkillIndex pointing to the domain of the action - /// @param _target The contract to which we send the action (0x0 for the colony) + /// @param _altTarget The contract to which we send the action (0x0 for the colony) /// @param _action A bytes array encoding a function call /// @param _key Reputation tree key for the domain /// @param _value Reputation tree value for the domain @@ -219,7 +219,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { function createDomainMotion( uint256 _domainId, uint256 _childSkillIndex, - address _target, + address _altTarget, bytes memory _action, bytes memory _key, bytes memory _value, @@ -236,7 +236,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { require(childSkillId == actionDomainSkillId, "voting-rep-invalid-domain-id"); } - createMotion(_target, _action, _domainId, domainSkillId, _key, _value, _branchMask, _siblings); + createMotion(_altTarget, _action, _domainId, domainSkillId, _key, _value, _branchMask, _siblings); } /// @notice Stake on a motion @@ -296,7 +296,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { !motion.escalated && motion.stakes[YAY] == requiredStake && getSig(motion.action) == CHANGE_FUNCTION && - (motion.target == address(0x0) || colonyNetwork.isColony(motion.target)) + (motion.altTarget == address(0x0) || colonyNetwork.isColony(motion.altTarget)) ) { bytes32 structHash = hashExpenditureStruct(motion.action); expenditureMotionCounts[structHash] = add(expenditureMotionCounts[structHash], 1); @@ -708,7 +708,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { // Internal functions function createMotion( - address _target, + address _altTarget, bytes memory _action, uint256 _domainId, uint256 _skillId, @@ -720,7 +720,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { internal { require(state == ExtensionState.Active, "voting-rep-not-active"); - require(_target != address(colony), "voting-rep-target-cannot-be-colony"); + require(_altTarget != address(colony), "voting-rep-alt-target-cannot-be-colony"); motionCount += 1; Motion storage motion = motions[motionCount]; @@ -734,7 +734,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { motion.skillId = _skillId; motion.skillRep = getReputationFromProof(motionCount, address(0x0), _key, _value, _branchMask, _siblings); - motion.target = _target; + motion.altTarget = _altTarget; motion.action = _action; emit MotionCreated(motionCount, msg.sender, _domainId); @@ -803,8 +803,8 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { } function executeCall(uint256 motionId, bytes memory action) internal returns (bool success) { - address target = motions[motionId].target; - address to = (target == address(0x0)) ? address(colony) : target; + address altTarget = motions[motionId].altTarget; + address to = (altTarget == address(0x0)) ? address(colony) : altTarget; assembly { // call contract at address a with input mem[in…(in+insize)) diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index 316bed1c61..1cf1a61b07 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -321,7 +321,7 @@ contract("Voting Reputation", (accounts) => { const action = await encodeTxData(colony, "makeTask", [1, 0, FAKE, 2, 0, 0]); await checkErrorRevert( voting.createRootMotion(colony.address, action, domain1Key, domain1Value, domain1Mask, domain1Siblings), - "voting-rep-target-cannot-be-colony" + "voting-rep-alt-target-cannot-be-colony" ); }); From a0166dd46dbfd452f8844b4f2a1b9fef2a35a4e2 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Tue, 25 Aug 2020 10:12:26 -0700 Subject: [PATCH 51/61] Respond to reviewer comments X --- contracts/colony/ColonyExpenditure.sol | 4 +- contracts/colony/IColony.sol | 15 +-- contracts/extensions/VotingReputation.sol | 28 +++-- docs/_Interface_IColony.md | 14 +-- test/extensions/voting-rep.js | 118 +++++++++++++++++----- 5 files changed, 126 insertions(+), 53 deletions(-) diff --git a/contracts/colony/ColonyExpenditure.sol b/contracts/colony/ColonyExpenditure.sol index 08693df29e..0827242ff7 100644 --- a/contracts/colony/ColonyExpenditure.sol +++ b/contracts/colony/ColonyExpenditure.sol @@ -145,7 +145,7 @@ contract ColonyExpenditure is ColonyStorage { emit ExpenditureSkillSet(_id, _slot, _skillId); } - // Can deprecate + // Deprecated function setExpenditurePayoutModifier( uint256 _permissionDomainId, uint256 _childSkillIndex, @@ -163,7 +163,7 @@ contract ColonyExpenditure is ColonyStorage { expenditureSlots[_id][_slot].payoutModifier = _payoutModifier; } - // Can deprecate + // Deprecated function setExpenditureClaimDelay( uint256 _permissionDomainId, uint256 _childSkillIndex, diff --git a/contracts/colony/IColony.sol b/contracts/colony/IColony.sol index d50d9d4b6c..fea0780705 100644 --- a/contracts/colony/IColony.sol +++ b/contracts/colony/IColony.sol @@ -223,6 +223,7 @@ contract IColony is ColonyDataTypes, IRecovery { function transferExpenditure(uint256 _id, address _newOwner) public; /// @notice DEPRECATED Updates the expenditure owner. Can only be called by Arbitration role. + /// @dev This is now deprecated and will be removed in a future version /// @param _permissionDomainId The domainId in which I have the permission to take this action /// @param _childSkillIndex The index that the `_domainId` is relative to `_permissionDomainId`, /// (only used if `_permissionDomainId` is different to `_domainId`) @@ -257,7 +258,8 @@ contract IColony is ColonyDataTypes, IRecovery { /// @param _skillId Id of the new skill to set function setExpenditureSkill(uint256 _id, uint256 _slot, uint256 _skillId) public; - /// @notice Set the payout modifier on an expenditure slot. Can only be called by Arbitration role. + /// @notice DEPRECATED Set the payout modifier on an expenditure slot. Can only be called by Arbitration role. + /// @dev This is now deprecated and will be removed in a future version /// @dev Note that when determining payouts the payoutModifier is incremented by WAD and converted into payoutScalar /// @param _permissionDomainId The domainId in which I have the permission to take this action /// @param _childSkillIndex The index that the `_domainId` is relative to `_permissionDomainId`, @@ -273,7 +275,8 @@ contract IColony is ColonyDataTypes, IRecovery { int256 _payoutModifier ) public; - /// @notice Set the claim delay on an expenditure slot. Can only be called by Arbitration role. + /// @notice DEPRECATED Set the claim delay on an expenditure slot. Can only be called by Arbitration role. + /// @dev This is now deprecated and will be removed in a future version /// @param _permissionDomainId The domainId in which I have the permission to take this action /// @param _childSkillIndex The index that the `_domainId` is relative to `_permissionDomainId`, /// (only used if `_permissionDomainId` is different to `_domainId`) @@ -293,15 +296,15 @@ contract IColony is ColonyDataTypes, IRecovery { /// @param _childSkillIndex The index that the `_domainId` is relative to `_permissionDomainId`, /// (only used if `_permissionDomainId` is different to `_domainId`) /// @param _id Expenditure identifier - /// @param _slot Number of the top-level storage slot - /// @param _mask Array of booleans indicated whether a key is a mapping (F) or offset (T). - /// @param _keys Array of additional keys (mappings & offsets) + /// @param _storageSlot Number of the top-level storage slot (25, 26, or 27) + /// @param _mask Array of booleans indicated whether a key is a mapping (F) or an array index (T). + /// @param _keys Array of additional keys (for mappings & arrays) /// @param _value Value to set at location function setExpenditureState( uint256 _permissionDomainId, uint256 _childSkillIndex, uint256 _id, - uint256 _slot, + uint256 _storageSlot, bool[] memory _mask, bytes32[] memory _keys, bytes32 _value diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index 3add30512a..2be1763c19 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -164,7 +164,6 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { uint256 skillId; uint256 skillRep; uint256 repSubmitted; - uint256 repRevealed; uint256 paidVoterComp; uint256[2] pastVoterComp; // [nay, yay] uint256[2] stakes; // [nay, yay] @@ -315,7 +314,10 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { motion.events[STAKE_END] = uint64(now + stakePeriod); motion.events[SUBMIT_END] = motion.events[STAKE_END] + uint64(submitPeriod); motion.events[REVEAL_END] = motion.events[SUBMIT_END] + uint64(revealPeriod); - delete motion.votes; // New stake supersedes prior votes + + // New stake supersedes prior votes + delete motion.votes; + delete motion.repSubmitted; emit MotionEventSet(_motionId, STAKE_END); } @@ -400,7 +402,6 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { delete voteSecrets[_motionId][msg.sender]; motion.votes[_vote] = add(motion.votes[_vote], userRep); - motion.repRevealed = add(motion.repRevealed, userRep); uint256 fractionUserReputation = wdiv(userRep, motion.skillRep); uint256 totalStake = add(motion.stakes[YAY], motion.stakes[NAY]); @@ -411,7 +412,8 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { emit MotionVoteRevealed(_motionId, msg.sender, _vote); - if (motion.repRevealed == motion.repSubmitted) { + // See if reputation revealed matches reputation submitted + if (add(motion.votes[NAY], motion.votes[YAY]) == motion.repSubmitted) { motion.events[REVEAL_END] = uint64(now); emit MotionEventSet(_motionId, REVEAL_END); @@ -482,7 +484,10 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { motion.votes[NAY] < motion.votes[YAY] ); - if (getSig(motion.action) == CHANGE_FUNCTION) { + if ( + getSig(motion.action) == CHANGE_FUNCTION && + (motion.altTarget == address(0x0) || colonyNetwork.isColony(motion.altTarget)) + ) { bytes32 structHash = hashExpenditureStruct(motion.action); expenditureMotionCounts[structHash] = sub(expenditureMotionCounts[structHash], 1); @@ -541,6 +546,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { add(motion.stakes[_vote], motion.pastVoterComp[_vote]) ); + require(stakeFraction > 0, "voting-rep-nothing-to-claim"); delete stakes[_motionId][_user][_vote]; uint256 realStake = wmul(stakeFraction, motion.stakes[_vote]); @@ -749,7 +755,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { } function flip(uint256 _vote) internal pure returns (uint256) { - return 1 - _vote; + return sub(1, _vote); } function getReputationFromProof( @@ -825,14 +831,14 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { } function hashExpenditureSlot(bytes memory action) internal returns (bytes32 hash) { - assert(getSig(action) == CHANGE_FUNCTION); - assembly { - // Hash all but last (value) bytes32 + // Hash all but the domain proof and value // Recall: mload(action) gives length of bytes array // So skip past the three bytes32 (length + domain proof), - // and the last bytes32 (value), plus 4 bytes for the sig. - hash := keccak256(add(action, 0x64), sub(mload(action), 0x64)) + // plus 4 bytes for the sig. Subtract the same from the end, less + // the length bytes32. The value itself is located at 0xe4, zero it out. + mstore(add(action, 0xe4), 0x0) + hash := keccak256(add(action, 0x64), sub(mload(action), 0x44)) } } diff --git a/docs/_Interface_IColony.md b/docs/_Interface_IColony.md index 2badfa4699..8ad5813f0b 100644 --- a/docs/_Interface_IColony.md +++ b/docs/_Interface_IColony.md @@ -1090,8 +1090,9 @@ Set new colony architecture role. Can be called by root role or architecture rol ### `setExpenditureClaimDelay` -Set the claim delay on an expenditure slot. Can only be called by Arbitration role. +DEPRECATED Set the claim delay on an expenditure slot. Can only be called by Arbitration role. +*Note: This is now deprecated and will be removed in a future version* **Parameters** @@ -1121,9 +1122,9 @@ Set the token payout on an expenditure slot. Can only be called by expenditure o ### `setExpenditurePayoutModifier` -Set the payout modifier on an expenditure slot. Can only be called by Arbitration role. +DEPRECATED Set the payout modifier on an expenditure slot. Can only be called by Arbitration role. -*Note: Note that when determining payouts the payoutModifier is incremented by WAD and converted into payoutScalar* +*Note: This is now deprecated and will be removed in a future version* **Parameters** @@ -1176,9 +1177,9 @@ Set arbitrary state on an expenditure slot. Can only be called by Arbitration ro |_permissionDomainId|uint256|The domainId in which I have the permission to take this action |_childSkillIndex|uint256|The index that the `_domainId` is relative to `_permissionDomainId`, (only used if `_permissionDomainId` is different to `_domainId`) |_id|uint256|Expenditure identifier -|_slot|uint256|Number of the top-level storage slot -|_mask|bool[]|Array of booleans indicated whether a key is a mapping (F) or offset (T). -|_keys|bytes32[]|Array of additional keys (mappings & offsets) +|_storageSlot|uint256|Number of the top-level storage slot (25, 26, or 27) +|_mask|bool[]|Array of booleans indicated whether a key is a mapping (F) or an array index (T). +|_keys|bytes32[]|Array of additional keys (for mappings & arrays) |_value|bytes32|Value to set at location @@ -1471,6 +1472,7 @@ Updates the expenditure owner. Can only be called by expenditure owner. DEPRECATED Updates the expenditure owner. Can only be called by Arbitration role. +*Note: This is now deprecated and will be removed in a future version* **Parameters** diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index 1cf1a61b07..7a7f37138c 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -213,12 +213,17 @@ contract("Voting Reputation", (accounts) => { const rootHash = await reputationTree.getRootHash(); const repCycle = await getActiveRepCycle(colonyNetwork); - await forwardTime(MINING_CYCLE_DURATION + SUBMITTER_ONLY_WINDOW + 1, this); + await forwardTime(MINING_CYCLE_DURATION, this); await repCycle.submitRootHash(rootHash, 0, "0x00", 10, { from: MINER }); await forwardTime(SUBMITTER_ONLY_WINDOW + 1, this); await repCycle.confirmNewHash(0); }); + function hashExpenditureSlot(action) { + const preamble = 2 + 8 + 64 * 2; + return soliditySha3(`0x${action.slice(preamble, preamble + 64 * 4)}${"0".repeat(64)}${action.slice(preamble + 64 * 5, action.length)}`); + } + describe("deploying the extension", async () => { it("can install the extension factory once if root and uninstall", async () => { ({ colony } = await setupRandomColony(colonyNetwork)); @@ -317,7 +322,7 @@ contract("Voting Reputation", (accounts) => { await voting.createRootMotion(voting.address, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); }); - it("can create a motion with an alternative target", async () => { + it("cannot create a motion with the colony as the alternative target", async () => { const action = await encodeTxData(colony, "makeTask", [1, 0, FAKE, 2, 0, 0]); await checkErrorRevert( voting.createRootMotion(colony.address, action, domain1Key, domain1Value, domain1Mask, domain1Siblings), @@ -805,7 +810,7 @@ contract("Voting Reputation", (accounts) => { const rootHash = await reputationTree.getRootHash(); expect(oldRootHash).to.not.equal(rootHash); - await forwardTime(MINING_CYCLE_DURATION + SUBMITTER_ONLY_WINDOW + 1, this); + await forwardTime(MINING_CYCLE_DURATION, this); const repCycle = await getActiveRepCycle(colonyNetwork); await repCycle.submitRootHash(rootHash, 0, "0x00", 10, { from: MINER }); @@ -986,6 +991,40 @@ contract("Voting Reputation", (accounts) => { expect(balanceAfter).to.eq.BN(WAD); }); + it("can take an expenditure action with an arbitrary colony target", async () => { + const { colony: otherColony } = await setupRandomColony(colonyNetwork); + await otherColony.setArbitrationRole(1, UINT256_MAX, voting.address, 1, true); + + await otherColony.makeExpenditure(1, UINT256_MAX, 1); + const expenditureId = await otherColony.getExpenditureCount(); + + // Set finalizedTimestamp to WAD + const action = await encodeTxData(otherColony, "setExpenditureState", [ + 1, + UINT256_MAX, + expenditureId, + 25, + [true], + [bn2bytes32(new BN(3))], + WAD32, + ]); + + await voting.createRootMotion(otherColony.address, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + motionId = await voting.getMotionCount(); + + await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + + let expenditure = await otherColony.getExpenditure(expenditureId); + expect(expenditure.globalClaimDelay).to.eq.BN(UINT256_MAX.divn(3)); + + await forwardTime(STAKE_PERIOD, this); + + await voting.finalizeMotion(motionId); + + expenditure = await otherColony.getExpenditure(expenditureId); + expect(expenditure.globalClaimDelay).to.be.zero; + }); + it("can take a nonexistent action", async () => { const action = soliditySha3("foo"); await voting.createRootMotion(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); @@ -1105,7 +1144,7 @@ contract("Voting Reputation", (accounts) => { expect(logs[0].args.executed).to.be.false; }); - it("can set vote power correctly if there is insufficient opposition", async () => { + it("can set vote power correctly after a vote", async () => { await colony.makeExpenditure(1, UINT256_MAX, 1); const expenditureId = await colony.getExpenditureCount(); @@ -1115,16 +1154,53 @@ contract("Voting Reputation", (accounts) => { motionId = await voting.getMotionCount(); await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - await forwardTime(STAKE_PERIOD, this); + await voting.submitVote(motionId, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + + await forwardTime(SUBMIT_PERIOD, this); + + await voting.revealVote(motionId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + + await forwardTime(REVEAL_PERIOD, this); + await forwardTime(ESCALATION_PERIOD, this); await voting.finalizeMotion(motionId); - const slotHash = soliditySha3(`0x${action.slice(2 + 8 + 128, action.length - 64)}`); + const slotHash = hashExpenditureSlot(action); const pastMotion = await voting.getExpenditurePastMotion(slotHash); - expect(pastMotion).to.eq.BN(REQUIRED_STAKE); + expect(pastMotion).to.eq.BN(WAD); // USER0 had 1 WAD of reputation }); - it("can set vote power correctly after a vote", async () => { + it("can use vote power correctly for different values of the same variable", async () => { + await colony.makeExpenditure(1, UINT256_MAX, 1); + const expenditureId = await colony.getExpenditureCount(); + + // Set finalizedTimestamp + const action1 = await encodeTxData(colony, "setExpenditureState", [1, UINT256_MAX, expenditureId, 25, [true], [bn2bytes32(new BN(3))], WAD32]); + const action2 = await encodeTxData(colony, "setExpenditureState", [1, UINT256_MAX, expenditureId, 25, [true], [bn2bytes32(new BN(3))], "0x0"]); + + await voting.createRootMotion(ADDRESS_ZERO, action1, domain1Key, domain1Value, domain1Mask, domain1Siblings); + const motionId1 = await voting.getMotionCount(); + + await voting.createRootMotion(ADDRESS_ZERO, action2, domain1Key, domain1Value, domain1Mask, domain1Siblings); + const motionId2 = await voting.getMotionCount(); + + await voting.stakeMotion(motionId1, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakeMotion(motionId2, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + + await forwardTime(STAKE_PERIOD, this); + + // First motion goes through + await voting.finalizeMotion(motionId1); + let expenditure = await colony.getExpenditure(expenditureId); + expect(expenditure.finalizedTimestamp).to.eq.BN(WAD); + + // Second motion does not because of insufficient vote power + expenditure = await colony.getExpenditure(expenditureId); + expect(expenditure.finalizedTimestamp).to.eq.BN(WAD); + }); + + it("can set vote power correctly if there is insufficient opposition", async () => { await colony.makeExpenditure(1, UINT256_MAX, 1); const expenditureId = await colony.getExpenditureCount(); @@ -1134,21 +1210,13 @@ contract("Voting Reputation", (accounts) => { motionId = await voting.getMotionCount(); await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); - - await voting.submitVote(motionId, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - - await forwardTime(SUBMIT_PERIOD, this); - - await voting.revealVote(motionId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - await forwardTime(REVEAL_PERIOD, this); - await forwardTime(ESCALATION_PERIOD, this); + await forwardTime(STAKE_PERIOD, this); await voting.finalizeMotion(motionId); - const slotHash = soliditySha3(`0x${action.slice(2 + 8 + 128, action.length - 64)}`); + const slotHash = hashExpenditureSlot(action); const pastMotion = await voting.getExpenditurePastMotion(slotHash); - expect(pastMotion).to.eq.BN(WAD); // USER0 had 1 WAD of reputation + expect(pastMotion).to.eq.BN(REQUIRED_STAKE); }); }); @@ -1379,10 +1447,8 @@ contract("Voting Reputation", (accounts) => { await voting.finalizeMotion(motionId); await voting.claimReward(motionId, 1, UINT256_MAX, USER0, YAY); - const userLock0 = await tokenLocking.getUserLock(token.address, USER0); - await voting.claimReward(motionId, 1, UINT256_MAX, USER0, YAY); - const userLock1 = await tokenLocking.getUserLock(token.address, USER0); - expect(userLock0.balance).to.eq.BN(userLock1.balance); + + await checkErrorRevert(voting.claimReward(motionId, 1, UINT256_MAX, USER0, YAY), "voting-rep-nothing-to-claim"); }); it("cannot claim rewards before a motion is finalized", async () => { @@ -1512,18 +1578,14 @@ contract("Voting Reputation", (accounts) => { expect(motionState).to.eq.BN(FAILED); // Now check that the rewards come out properly - const user0LockPre = await tokenLocking.getUserLock(token.address, USER0); const user1LockPre = await tokenLocking.getUserLock(token.address, USER1); - await voting.claimReward(motionId, 1, UINT256_MAX, USER0, YAY); + await checkErrorRevert(voting.claimReward(motionId, 1, UINT256_MAX, USER0, YAY), "voting-rep-nothing-to-claim"); await voting.claimReward(motionId, 1, UINT256_MAX, USER1, NAY); - const user0LockPost = await tokenLocking.getUserLock(token.address, USER0); const user1LockPost = await tokenLocking.getUserLock(token.address, USER1); const expectedReward1 = (REQUIRED_STAKE.add(WAD.divn(1000 * 10))).divn(32).muln(22); // eslint-disable-line prettier/prettier - - expect(new BN(user0LockPost.balance).sub(new BN(user0LockPre.balance))).to.be.zero; expect(new BN(user1LockPost.balance).sub(new BN(user1LockPre.balance))).to.eq.BN(expectedReward1); }); From ecc833cebce46ddb411963ab779524c572e81078 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Thu, 27 Aug 2020 14:10:03 -0700 Subject: [PATCH 52/61] No escalation from root domain, add both domains to MotionEscalated --- contracts/extensions/VotingReputation.sol | 10 +++++++--- test/extensions/voting-rep.js | 20 +++++++++++++++++++- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index 2be1763c19..b61c1b81be 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -37,7 +37,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { event MotionVoteSubmitted(uint256 indexed motionId, address indexed voter); event MotionVoteRevealed(uint256 indexed motionId, address indexed voter, uint256 indexed vote); event MotionFinalized(uint256 indexed motionId, bytes action, bool executed); - event MotionEscalated(uint256 indexed motionId, address escalator, uint256 indexed domainId); + event MotionEscalated(uint256 indexed motionId, address escalator, uint256 indexed domainId, uint256 indexed newDomainId); event MotionRewardClaimed(uint256 indexed motionId, address indexed staker, uint256 indexed vote, uint256 amount); event MotionEventSet(uint256 indexed motionId, uint256 eventIndex); @@ -446,6 +446,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { uint256 childSkillId = colonyNetwork.getChildSkillId(newDomainSkillId, _childSkillIndex); require(childSkillId == motion.skillId, "voting-rep-invalid-domain-proof"); + uint256 domainId = motion.domainId; motion.domainId = _newDomainId; motion.skillId = newDomainSkillId; motion.skillRep = getReputationFromProof(_motionId, address(0x0), _key, _value, _branchMask, _siblings); @@ -464,7 +465,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { motion.escalated = true; - emit MotionEscalated(_motionId, msg.sender, _newDomainId); + emit MotionEscalated(_motionId, msg.sender, domainId, _newDomainId); if (motion.events[STAKE_END] == uint64(now)) { emit MotionEventSet(_motionId, STAKE_END); @@ -702,7 +703,10 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { return MotionState.Submit; } else if (now < motion.events[REVEAL_END]) { return MotionState.Reveal; - } else if (now < motion.events[REVEAL_END] + escalationPeriod) { + } else if ( + now < motion.events[REVEAL_END] + escalationPeriod && + motion.domainId > 1 + ) { return MotionState.Closed; } else { return MotionState.Finalizable; diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index 7a7f37138c..5aebcdd13d 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -92,7 +92,7 @@ contract("Voting Reputation", (accounts) => { const SUBMIT = 2; // const REVEAL = 3; // const CLOSED = 4; - // const EXECUTABLE = 5; + const EXECUTABLE = 5; // const EXECUTED = 6; const FAILED = 7; @@ -1629,5 +1629,23 @@ contract("Voting Reputation", (accounts) => { expect(new BN(user0LockPost.balance).sub(new BN(user0LockPre.balance))).to.eq.BN(expectedReward0.addn(1)); // Rounding expect(new BN(user1LockPost.balance).sub(new BN(user1LockPre.balance))).to.eq.BN(expectedReward1); }); + + it("cannot escalate a motion in the root domain", async () => { + const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); + await voting.createRootMotion(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + motionId = await voting.getMotionCount(); + + await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + + await voting.submitVote(motionId, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.submitVote(motionId, soliditySha3(SALT, NAY), user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + + await voting.revealVote(motionId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + await voting.revealVote(motionId, SALT, NAY, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }); + + const state = await voting.getMotionState(motionId); + expect(state).to.eq.BN(EXECUTABLE); + }); }); }); From 37a9133f7fa4a1bde5666fc2a8daaacdc3b9da0b Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Sun, 30 Aug 2020 19:06:09 -0700 Subject: [PATCH 53/61] Respond to review comments XI --- contracts/extensions/VotingReputation.sol | 241 ++++++++++++++-------- test/extensions/voting-rep.js | 60 ++++-- 2 files changed, 193 insertions(+), 108 deletions(-) diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index b61c1b81be..fa14532e36 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -209,7 +209,6 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { /// @notice Create a motion in any domain /// @param _domainId The domain where we vote on the motion /// @param _childSkillIndex The childSkillIndex pointing to the domain of the action - /// @param _altTarget The contract to which we send the action (0x0 for the colony) /// @param _action A bytes array encoding a function call /// @param _key Reputation tree key for the domain /// @param _value Reputation tree value for the domain @@ -218,7 +217,6 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { function createDomainMotion( uint256 _domainId, uint256 _childSkillIndex, - address _altTarget, bytes memory _action, bytes memory _key, bytes memory _value, @@ -235,7 +233,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { require(childSkillId == actionDomainSkillId, "voting-rep-invalid-domain-id"); } - createMotion(_altTarget, _action, _domainId, domainSkillId, _key, _value, _branchMask, _siblings); + createMotion(address(0x0), _action, _domainId, domainSkillId, _key, _value, _branchMask, _siblings); } /// @notice Stake on a motion @@ -306,10 +304,18 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { emit MotionStaked(_motionId, msg.sender, _vote, amount); + // Move to vote submission once both sides are fully staked + if (motion.stakes[NAY] == requiredStake && motion.stakes[YAY] == requiredStake) { + motion.events[STAKE_END] = uint64(now); + motion.events[SUBMIT_END] = motion.events[STAKE_END] + uint64(submitPeriod); + motion.events[REVEAL_END] = motion.events[SUBMIT_END] + uint64(revealPeriod); + + emit MotionEventSet(_motionId, STAKE_END); + // Move to second staking window once one side is fully staked - if ( - (_vote == YAY && motion.stakes[YAY] == requiredStake && motion.stakes[NAY] < requiredStake) || - (_vote == NAY && motion.stakes[NAY] == requiredStake && motion.stakes[YAY] < requiredStake) + } else if ( + (_vote == NAY && motion.stakes[NAY] == requiredStake) || + (_vote == YAY && motion.stakes[YAY] == requiredStake) ) { motion.events[STAKE_END] = uint64(now + stakePeriod); motion.events[SUBMIT_END] = motion.events[STAKE_END] + uint64(submitPeriod); @@ -322,14 +328,6 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { emit MotionEventSet(_motionId, STAKE_END); } - // Move to vote submission once both sides are fully staked - if (motion.stakes[YAY] == requiredStake && motion.stakes[NAY] == requiredStake) { - motion.events[STAKE_END] = uint64(now); - motion.events[SUBMIT_END] = motion.events[STAKE_END] + uint64(submitPeriod); - motion.events[REVEAL_END] = motion.events[SUBMIT_END] + uint64(revealPeriod); - - emit MotionEventSet(_motionId, STAKE_END); - } } /// @notice Submit a vote secret for a motion @@ -396,17 +394,13 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { require(_vote <= 1, "voting-rep-bad-vote"); uint256 userRep = getReputationFromProof(_motionId, msg.sender, _key, _value, _branchMask, _siblings); + motion.votes[_vote] = add(motion.votes[_vote], userRep); bytes32 voteSecret = voteSecrets[_motionId][msg.sender]; require(voteSecret == getVoteSecret(_salt, _vote), "voting-rep-secret-no-match"); delete voteSecrets[_motionId][msg.sender]; - motion.votes[_vote] = add(motion.votes[_vote], userRep); - - uint256 fractionUserReputation = wdiv(userRep, motion.skillRep); - uint256 totalStake = add(motion.stakes[YAY], motion.stakes[NAY]); - uint256 voterReward = wmul(wmul(fractionUserReputation, totalStake), voterRewardFraction); - + uint256 voterReward = getVoterReward(_motionId, userRep); motion.paidVoterComp = add(motion.paidVoterComp, voterReward); tokenLocking.transfer(token, voterReward, msg.sender, true); @@ -524,13 +518,13 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { /// @param _motionId The id of the motion /// @param _permissionDomainId The domain where the extension has the arbitration permission /// @param _childSkillIndex For the domain in which the motion is occurring - /// @param _user The user whose reward is being claimed + /// @param _staker The staker whose reward is being claimed /// @param _vote The side being supported (0 = NAY, 1 = YAY) function claimReward( uint256 _motionId, uint256 _permissionDomainId, uint256 _childSkillIndex, - address _user, + address _staker, uint256 _vote ) public @@ -542,88 +536,76 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { "voting-rep-motion-not-claimable" ); - uint256 stakeFraction = wdiv( - stakes[_motionId][_user][_vote], - add(motion.stakes[_vote], motion.pastVoterComp[_vote]) - ); - - require(stakeFraction > 0, "voting-rep-nothing-to-claim"); - delete stakes[_motionId][_user][_vote]; - - uint256 realStake = wmul(stakeFraction, motion.stakes[_vote]); - uint256 requiredStake = getRequiredStake(_motionId); - - uint256 stakerReward; - uint256 repPenalty; - - // Went to a vote, use vote to determine reward or penalty - if (add(motion.votes[NAY], motion.votes[YAY]) > 0) { - - uint256 loserStake = sub(requiredStake, motion.paidVoterComp); - uint256 totalVotes = add(motion.votes[NAY], motion.votes[YAY]); - uint256 winFraction = wdiv(motion.votes[_vote], totalVotes); - uint256 winShare = wmul(winFraction, 2 * WAD); // On a scale of 0-2 WAD - - if (winShare > WAD || (winShare == WAD && _vote == NAY)) { - stakerReward = wmul(stakeFraction, add(requiredStake, wmul(loserStake, winShare - WAD))); - } else { - stakerReward = wmul(stakeFraction, wmul(loserStake, winShare)); - repPenalty = sub(realStake, stakerReward); - } - - // Determine rewards based on stakes alone - } else { - - assert(motion.paidVoterComp == 0); - - // Your side fully staked, receive 10% (proportional) of loser's stake - if ( - motion.stakes[_vote] == requiredStake && - motion.stakes[flip(_vote)] < requiredStake - ) { - - uint256 loserStake = motion.stakes[flip(_vote)]; - uint256 totalPenalty = wmul(loserStake, WAD / 10); - - stakerReward = wmul(stakeFraction, add(requiredStake, totalPenalty)); + (uint256 stakerReward, uint256 repPenalty) = getStakerReward(_motionId, _staker, _vote); - // Opponent's side fully staked, pay 10% penalty - } else if ( - motion.stakes[_vote] < requiredStake && - motion.stakes[flip(_vote)] == requiredStake - ) { + require(stakerReward > 0, "voting-rep-nothing-to-claim"); + delete stakes[_motionId][_staker][_vote]; - uint256 loserStake = motion.stakes[_vote]; - uint256 totalPenalty = wmul(loserStake, WAD / 10); - - stakerReward = wmul(stakeFraction, sub(loserStake, totalPenalty)); - repPenalty = sub(realStake, stakerReward); - - // Neither side fully staked (or no votes were revealed), no reward or penalty - } else { - - stakerReward = realStake; - - } - } - - tokenLocking.transfer(token, stakerReward, _user, true); + tokenLocking.transfer(token, stakerReward, _staker, true); if (repPenalty > 0) { colony.emitDomainReputationPenalty( _permissionDomainId, _childSkillIndex, motion.domainId, - _user, + _staker, -int256(repPenalty) ); } - emit MotionRewardClaimed(_motionId, _user, _vote, stakerReward); + emit MotionRewardClaimed(_motionId, _staker, _vote, stakerReward); } // Public view functions + /// @notice Get the total stake fraction + /// @return The total stake fraction + function getTotalStakeFraction() public view returns (uint256) { + return totalStakeFraction; + } + + /// @notice Get the voter reward fraction + /// @return The voter reward fraction + function getVoterRewardFraction() public view returns (uint256) { + return voterRewardFraction; + } + + /// @notice Get the user min stake fraction + /// @return The user min stake fraction + function getUserMinStakeFraction() public view returns (uint256) { + return userMinStakeFraction; + } + + /// @notice Get the max vote fraction + /// @return The max vote fraction + function getMaxVoteFraction() public view returns (uint256) { + return maxVoteFraction; + } + + /// @notice Get the stake period + /// @return The stake period + function getStakePeriod() public view returns (uint256) { + return stakePeriod; + } + + /// @notice Get the submit period + /// @return The submit period + function getSubmitPeriod() public view returns (uint256) { + return submitPeriod; + } + + /// @notice Get the reveal period + /// @return The reveal period + function getRevealPeriod() public view returns (uint256) { + return revealPeriod; + } + + /// @notice Get the escalation period + /// @return The escalation period + function getEscalationPeriod() public view returns (uint256) { + return escalationPeriod; + } + /// @notice Get the total motion count /// @return The total motion count function getMotionCount() public view returns (uint256) { @@ -715,6 +697,87 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { } } + /// @notice Get the voter reward + /// @param _motionId The id of the motion + /// @param _voterRep The reputation the voter has in the domain + /// @return The voter reward + function getVoterReward(uint256 _motionId, uint256 _voterRep) public view returns (uint256) { + Motion storage motion = motions[_motionId]; + uint256 fractionUserReputation = wdiv(_voterRep, motion.skillRep); + uint256 totalStake = add(motion.stakes[YAY], motion.stakes[NAY]); + return wmul(wmul(fractionUserReputation, totalStake), voterRewardFraction); + } + + /// @notice Get the staker reward + /// @param _motionId The id of the motion + /// @param _staker The staker's address + /// @param _vote The vote (0 = NAY, 1 = YAY) + /// @return The staker reward and the reputation penalty (if any) + function getStakerReward(uint256 _motionId, address _staker, uint256 _vote) public view returns (uint256, uint256) { + Motion storage motion = motions[_motionId]; + + uint256 stakeFraction = wdiv( + stakes[_motionId][_staker][_vote], + add(motion.stakes[_vote], motion.pastVoterComp[_vote]) + ); + + uint256 realStake = wmul(stakeFraction, motion.stakes[_vote]); + uint256 requiredStake = getRequiredStake(_motionId); + + uint256 stakerReward; + uint256 repPenalty; + + // Went to a vote, use vote to determine reward or penalty + if (add(motion.votes[NAY], motion.votes[YAY]) > 0) { + + uint256 loserStake = sub(requiredStake, motion.paidVoterComp); + uint256 totalVotes = add(motion.votes[NAY], motion.votes[YAY]); + uint256 winFraction = wdiv(motion.votes[_vote], totalVotes); + uint256 winShare = wmul(winFraction, 2 * WAD); // On a scale of 0-2 WAD + + if (winShare > WAD || (winShare == WAD && _vote == NAY)) { + stakerReward = wmul(stakeFraction, add(requiredStake, wmul(loserStake, winShare - WAD))); + } else { + stakerReward = wmul(stakeFraction, wmul(loserStake, winShare)); + repPenalty = sub(realStake, stakerReward); + } + + // Determine rewards based on stakes alone + } else { + assert(motion.paidVoterComp == 0); + + // Your side fully staked, receive 10% (proportional) of loser's stake + if ( + motion.stakes[_vote] == requiredStake && + motion.stakes[flip(_vote)] < requiredStake + ) { + + uint256 loserStake = motion.stakes[flip(_vote)]; + uint256 totalPenalty = wmul(loserStake, WAD / 10); + stakerReward = wmul(stakeFraction, add(requiredStake, totalPenalty)); + + // Opponent's side fully staked, pay 10% penalty + } else if ( + motion.stakes[_vote] < requiredStake && + motion.stakes[flip(_vote)] == requiredStake + ) { + + uint256 loserStake = motion.stakes[_vote]; + uint256 totalPenalty = wmul(loserStake, WAD / 10); + stakerReward = wmul(stakeFraction, sub(loserStake, totalPenalty)); + repPenalty = sub(realStake, stakerReward); + + // Neither side fully staked (or no votes were revealed), no reward or penalty + } else { + + stakerReward = realStake; + + } + } + + return (stakerReward, repPenalty); + } + // Internal functions function createMotion( @@ -730,7 +793,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { internal { require(state == ExtensionState.Active, "voting-rep-not-active"); - require(_altTarget != address(colony), "voting-rep-alt-target-cannot-be-colony"); + require(_altTarget != address(colony), "voting-rep-alt-target-cannot-be-base-colony"); motionCount += 1; Motion storage motion = motions[motionCount]; diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index 5aebcdd13d..b573fbbdac 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -233,6 +233,28 @@ contract("Voting Reputation", (accounts) => { await votingFactory.removeExtension(colony.address, { from: USER0 }); }); + it("can query for initisalisation values", async () => { + const totalStakeFraction = await voting.getTotalStakeFraction(); + const voterRewardFraction = await voting.getVoterRewardFraction(); + const userMinStakeFraction = await voting.getUserMinStakeFraction(); + const maxVoteFraction = await voting.getMaxVoteFraction(); + + const stakePeriod = await voting.getStakePeriod(); + const submitPeriod = await voting.getSubmitPeriod(); + const revealPeriod = await voting.getRevealPeriod(); + const escalationPeriod = await voting.getEscalationPeriod(); + + expect(totalStakeFraction).to.eq.BN(TOTAL_STAKE_FRACTION); + expect(voterRewardFraction).to.eq.BN(VOTER_REWARD_FRACTION); + expect(userMinStakeFraction).to.eq.BN(USER_MIN_STAKE_FRACTION); + expect(maxVoteFraction).to.eq.BN(MAX_VOTE_FRACTION); + + expect(stakePeriod).to.eq.BN(STAKE_PERIOD); + expect(submitPeriod).to.eq.BN(SUBMIT_PERIOD); + expect(revealPeriod).to.eq.BN(REVEAL_PERIOD); + expect(escalationPeriod).to.eq.BN(ESCALATION_PERIOD); + }); + it("can deprecate the extension if root", async () => { const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); await voting.createRootMotion(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); @@ -286,7 +308,7 @@ contract("Voting Reputation", (accounts) => { it("can create a domain motion in the root domain", async () => { // Create motion in domain of action (1) const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); - await voting.createDomainMotion(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + await voting.createDomainMotion(1, UINT256_MAX, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); const motionId = await voting.getMotionCount(); const motion = await voting.getMotion(motionId); @@ -300,7 +322,7 @@ contract("Voting Reputation", (accounts) => { // Create motion in domain of action (2) const action = await encodeTxData(colony, "makeTask", [1, 0, FAKE, 2, 0, 0]); - await voting.createDomainMotion(2, UINT256_MAX, ADDRESS_ZERO, action, key, value, mask, siblings); + await voting.createDomainMotion(2, UINT256_MAX, action, key, value, mask, siblings); const motionId = await voting.getMotionCount(); const motion = await voting.getMotion(motionId); @@ -310,7 +332,7 @@ contract("Voting Reputation", (accounts) => { it("can externally escalate a domain motion", async () => { // Create motion in parent domain (1) of action (2) const action = await encodeTxData(colony, "makeTask", [1, 0, FAKE, 2, 0, 0]); - await voting.createDomainMotion(1, 0, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + await voting.createDomainMotion(1, 0, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); const motionId = await voting.getMotionCount(); const motion = await voting.getMotion(motionId); @@ -326,7 +348,7 @@ contract("Voting Reputation", (accounts) => { const action = await encodeTxData(colony, "makeTask", [1, 0, FAKE, 2, 0, 0]); await checkErrorRevert( voting.createRootMotion(colony.address, action, domain1Key, domain1Value, domain1Mask, domain1Siblings), - "voting-rep-alt-target-cannot-be-colony" + "voting-rep-alt-target-cannot-be-base-colony" ); }); @@ -337,7 +359,7 @@ contract("Voting Reputation", (accounts) => { // Provide proof for (3) instead of (2) const action = await encodeTxData(colony, "makeTask", [1, 0, FAKE, 2, 0, 0]); - await checkErrorRevert(voting.createDomainMotion(1, 1, ADDRESS_ZERO, action, key, value, mask, siblings), "voting-rep-invalid-domain-id"); + await checkErrorRevert(voting.createDomainMotion(1, 1, action, key, value, mask, siblings), "voting-rep-invalid-domain-id"); }); }); @@ -438,7 +460,7 @@ contract("Voting Reputation", (accounts) => { // Set finalizedTimestamp to WAD const action = await encodeTxData(colony, "setExpenditureState", [1, UINT256_MAX, expenditureId, 25, [true], [bn2bytes32(new BN(3))], WAD32]); - await voting.createDomainMotion(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + await voting.createDomainMotion(1, UINT256_MAX, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); motionId = await voting.getMotionCount(); let expenditureMotionCount; @@ -476,7 +498,7 @@ contract("Voting Reputation", (accounts) => { WAD32, ]); - await voting.createDomainMotion(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + await voting.createDomainMotion(1, UINT256_MAX, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); motionId = await voting.getMotionCount(); let expenditureMotionCount; @@ -514,7 +536,7 @@ contract("Voting Reputation", (accounts) => { WAD32, ]); - await voting.createDomainMotion(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + await voting.createDomainMotion(1, UINT256_MAX, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); motionId = await voting.getMotionCount(); let expenditureMotionCount; @@ -547,7 +569,7 @@ contract("Voting Reputation", (accounts) => { // Set finalizedTimestamp to WAD action = await encodeTxData(colony, "setExpenditureState", [1, UINT256_MAX, expenditureId, 25, [true], [bn2bytes32(new BN(3))], WAD32]); - await voting.createDomainMotion(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + await voting.createDomainMotion(1, UINT256_MAX, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); motionId = await voting.getMotionCount(); await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); @@ -563,7 +585,7 @@ contract("Voting Reputation", (accounts) => { WAD32, ]); - await voting.createDomainMotion(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + await voting.createDomainMotion(1, UINT256_MAX, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); motionId = await voting.getMotionCount(); await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); @@ -579,7 +601,7 @@ contract("Voting Reputation", (accounts) => { WAD32, ]); - await voting.createDomainMotion(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + await voting.createDomainMotion(1, UINT256_MAX, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); motionId = await voting.getMotionCount(); await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); @@ -596,7 +618,7 @@ contract("Voting Reputation", (accounts) => { // Create a poorly-formed action (no keys) const action = await encodeTxData(colony, "setExpenditureState", [1, UINT256_MAX, 1, 0, [], [], ethers.constants.HashZero]); - await voting.createDomainMotion(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + await voting.createDomainMotion(1, UINT256_MAX, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); motionId = await voting.getMotionCount(); await checkErrorRevert( @@ -632,10 +654,10 @@ contract("Voting Reputation", (accounts) => { WAD32, ]); - await voting.createDomainMotion(1, UINT256_MAX, ADDRESS_ZERO, action1, domain1Key, domain1Value, domain1Mask, domain1Siblings); + await voting.createDomainMotion(1, UINT256_MAX, action1, domain1Key, domain1Value, domain1Mask, domain1Siblings); const motionId1 = await voting.getMotionCount(); - await voting.createDomainMotion(1, UINT256_MAX, ADDRESS_ZERO, action2, domain1Key, domain1Value, domain1Mask, domain1Siblings); + await voting.createDomainMotion(1, UINT256_MAX, action2, domain1Key, domain1Value, domain1Mask, domain1Siblings); const motionId2 = await voting.getMotionCount(); let expenditureMotionCount; @@ -1105,7 +1127,7 @@ contract("Voting Reputation", (accounts) => { const expenditureId = await colony.getExpenditureCount(); const action = await encodeTxData(colony, "setExpenditureState", [1, UINT256_MAX, expenditureId, 25, [true], [bn2bytes32(new BN(4))], WAD32]); - await voting.createDomainMotion(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + await voting.createDomainMotion(1, UINT256_MAX, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); const motionId1 = await voting.getMotionCount(); await voting.stakeMotion(motionId1, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); @@ -1125,7 +1147,7 @@ contract("Voting Reputation", (accounts) => { expect(logs[0].args.executed).to.be.true; // Create another motion for the same variable - await voting.createDomainMotion(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + await voting.createDomainMotion(1, UINT256_MAX, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); const motionId2 = await voting.getMotionCount(); await voting.stakeMotion(motionId2, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); @@ -1150,7 +1172,7 @@ contract("Voting Reputation", (accounts) => { const action = await encodeTxData(colony, "setExpenditureState", [1, UINT256_MAX, expenditureId, 25, [true], ["0x0"], WAD32]); - await voting.createDomainMotion(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + await voting.createDomainMotion(1, UINT256_MAX, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); motionId = await voting.getMotionCount(); await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); @@ -1206,7 +1228,7 @@ contract("Voting Reputation", (accounts) => { const action = await encodeTxData(colony, "setExpenditureState", [1, UINT256_MAX, expenditureId, 25, [true], ["0x0"], WAD32]); - await voting.createDomainMotion(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + await voting.createDomainMotion(1, UINT256_MAX, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); motionId = await voting.getMotionCount(); await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); @@ -1473,7 +1495,7 @@ contract("Voting Reputation", (accounts) => { const [user1Mask2, user1Siblings2] = await reputationTree.getProof(user1Key2); const action = await encodeTxData(colony, "makeTask", [1, 0, FAKE, 2, 0, 0]); - await voting.createDomainMotion(2, UINT256_MAX, ADDRESS_ZERO, action, domain2Key, domain2Value, domain2Mask, domain2Siblings); + await voting.createDomainMotion(2, UINT256_MAX, action, domain2Key, domain2Value, domain2Mask, domain2Siblings); motionId = await voting.getMotionCount(); await colony.approveStake(voting.address, 2, WAD, { from: USER0 }); From 46a80b49d89904d7bf3d538d0a458cf0625b7037 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Mon, 31 Aug 2020 17:34:16 -0700 Subject: [PATCH 54/61] Add test demonstrating escalation with no stake --- test/extensions/voting-rep.js | 69 ++++++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index b573fbbdac..83f473eb64 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -184,7 +184,7 @@ contract("Voting Reputation", (accounts) => { ); await reputationTree.insert( makeReputationKey(colony.address, domain3.skillId), // Colony total, domain 3 - makeReputationValue(WAD, 7) + makeReputationValue(WAD.muln(3), 7) ); await reputationTree.insert( makeReputationKey(colony.address, domain1.skillId, USER2), // User2, very little rep @@ -198,6 +198,14 @@ contract("Voting Reputation", (accounts) => { makeReputationKey(colony.address, domain2.skillId, USER1), // User1, domain 2 makeReputationValue(WAD.divn(3).muln(2), 10) ); + await reputationTree.insert( + makeReputationKey(colony.address, domain3.skillId, USER0), // User0, domain 3 + makeReputationValue(WAD, 11) + ); + await reputationTree.insert( + makeReputationKey(colony.address, domain3.skillId, USER1), // User1, domain 3 + makeReputationValue(WAD.muln(2), 12) + ); domain1Key = makeReputationKey(colony.address, domain1.skillId); domain1Value = makeReputationValue(WAD.muln(3), 1); @@ -354,7 +362,7 @@ contract("Voting Reputation", (accounts) => { it("cannot externally escalate a domain motion with an invalid domain proof", async () => { const key = makeReputationKey(colony.address, domain3.skillId); - const value = makeReputationValue(WAD, 7); + const value = makeReputationValue(WAD.muln(3), 7); const [mask, siblings] = await reputationTree.getProof(key); // Provide proof for (3) instead of (2) @@ -1669,5 +1677,62 @@ contract("Voting Reputation", (accounts) => { const state = await voting.getMotionState(motionId); expect(state).to.eq.BN(EXECUTABLE); }); + + it("can skip the staking phase if no new stake is required", async () => { + // Deploy a new extension with no voter compensation + await votingFactory.removeExtension(colony.address, { from: USER0 }); + await votingFactory.deployExtension(colony.address, { from: USER0 }); + const votingAddress = await votingFactory.deployedExtensions(colony.address); + voting = await VotingReputation.at(votingAddress); + + await colony.setArbitrationRole(1, UINT256_MAX, voting.address, 1, true); + + await voting.initialise( + TOTAL_STAKE_FRACTION, + 0, // No voter compensation + USER_MIN_STAKE_FRACTION, + MAX_VOTE_FRACTION, + STAKE_PERIOD, + SUBMIT_PERIOD, + REVEAL_PERIOD, + ESCALATION_PERIOD + ); + + // Run a vote in domain 3, same rep as domain 1 + const domain3Key = makeReputationKey(colony.address, domain3.skillId); + const domain3Value = makeReputationValue(WAD.muln(3), 7); + const [domain3Mask, domain3Siblings] = await reputationTree.getProof(domain3Key); + + const user0Key3 = makeReputationKey(colony.address, domain3.skillId, USER0); + const user0Value3 = makeReputationValue(WAD, 11); + const [user0Mask3, user0Siblings3] = await reputationTree.getProof(user0Key3); + + const user1Key3 = makeReputationKey(colony.address, domain3.skillId, USER1); + const user1Value3 = makeReputationValue(WAD.muln(2), 12); + const [user1Mask3, user1Siblings3] = await reputationTree.getProof(user1Key3); + + const action = await encodeTxData(colony, "makeTask", [1, 1, FAKE, 3, 0, 0]); + await voting.createDomainMotion(3, UINT256_MAX, action, domain3Key, domain3Value, domain3Mask, domain3Siblings); + motionId = await voting.getMotionCount(); + + await colony.approveStake(voting.address, 3, WAD, { from: USER0 }); + await colony.approveStake(voting.address, 3, WAD, { from: USER1 }); + + await voting.stakeMotion(motionId, 1, 1, NAY, REQUIRED_STAKE, user0Key3, user0Value3, user0Mask3, user0Siblings3, { from: USER0 }); + await voting.stakeMotion(motionId, 1, 1, YAY, REQUIRED_STAKE, user1Key3, user1Value3, user1Mask3, user1Siblings3, { from: USER1 }); + + // Note that this is a passing vote + await voting.submitVote(motionId, soliditySha3(SALT, NAY), user0Key3, user0Value3, user0Mask3, user0Siblings3, { from: USER0 }); + await voting.submitVote(motionId, soliditySha3(SALT, YAY), user1Key3, user1Value3, user1Mask3, user1Siblings3, { from: USER1 }); + + await voting.revealVote(motionId, SALT, NAY, user0Key3, user0Value3, user0Mask3, user0Siblings3, { from: USER0 }); + await voting.revealVote(motionId, SALT, YAY, user1Key3, user1Value3, user1Mask3, user1Siblings3, { from: USER1 }); + + // Now escalate, should go directly into submit phase + await voting.escalateMotion(motionId, 1, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); + + const state = await voting.getMotionState(motionId); + expect(state).to.eq.BN(SUBMIT); + }); }); }); From 3b954cb26c93c8d58d82b96b8c8560fd76268097 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Tue, 1 Sep 2020 10:43:51 -0700 Subject: [PATCH 55/61] Remove expenditure locking for alternative targets --- contracts/extensions/VotingReputation.sol | 12 ++--- test/extensions/voting-rep.js | 55 +++++++++-------------- 2 files changed, 27 insertions(+), 40 deletions(-) diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index fa14532e36..710f0f304e 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -293,7 +293,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { !motion.escalated && motion.stakes[YAY] == requiredStake && getSig(motion.action) == CHANGE_FUNCTION && - (motion.altTarget == address(0x0) || colonyNetwork.isColony(motion.altTarget)) + motion.altTarget == address(0x0) ) { bytes32 structHash = hashExpenditureStruct(motion.action); expenditureMotionCounts[structHash] = add(expenditureMotionCounts[structHash], 1); @@ -470,7 +470,10 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { Motion storage motion = motions[_motionId]; require(getMotionState(_motionId) == MotionState.Finalizable, "voting-rep-motion-not-finalizable"); - assert(motion.stakes[YAY] == getRequiredStake(_motionId) || add(motion.votes[NAY], motion.votes[YAY]) > 0); + assert( + motion.stakes[YAY] == getRequiredStake(_motionId) || + add(motion.votes[NAY], motion.votes[YAY]) > 0 + ); motion.finalized = true; @@ -479,10 +482,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { motion.votes[NAY] < motion.votes[YAY] ); - if ( - getSig(motion.action) == CHANGE_FUNCTION && - (motion.altTarget == address(0x0) || colonyNetwork.isColony(motion.altTarget)) - ) { + if (getSig(motion.action) == CHANGE_FUNCTION && motion.altTarget == address(0x0)) { bytes32 structHash = hashExpenditureStruct(motion.action); expenditureMotionCounts[structHash] = sub(expenditureMotionCounts[structHash], 1); diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index 83f473eb64..9319b87014 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -490,6 +490,27 @@ contract("Voting Reputation", (accounts) => { await checkErrorRevert(colony.claimExpenditurePayout(expenditureId, 0, token.address), "colony-expenditure-cannot-claim"); }); + it("cannot update the expenditure globalClaimDelay if the target is another colony", async () => { + const { colony: otherColony } = await setupRandomColony(colonyNetwork); + await otherColony.makeExpenditure(1, UINT256_MAX, 1); + const expenditureId = await otherColony.getExpenditureCount(); + await otherColony.finalizeExpenditure(expenditureId); + + // Set finalizedTimestamp to WAD + const action = await encodeTxData(otherColony, "setExpenditureState", [1, UINT256_MAX, expenditureId, 25, [true], [bn2bytes32(new BN(3))], WAD32]); + + await voting.createRootMotion(otherColony.address, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + motionId = await voting.getMotionCount(); + + await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + + const expenditureMotionCount = await voting.getExpenditureMotionCount(soliditySha3(expenditureId)); + expect(expenditureMotionCount).to.be.zero; + + const expenditure = await otherColony.getExpenditure(expenditureId); + expect(expenditure.globalClaimDelay).to.be.zero; + }); + it("can update the expenditure slot claimDelay if voting on expenditure slot state", async () => { await colony.makeExpenditure(1, UINT256_MAX, 1); const expenditureId = await colony.getExpenditureCount(); @@ -1021,40 +1042,6 @@ contract("Voting Reputation", (accounts) => { expect(balanceAfter).to.eq.BN(WAD); }); - it("can take an expenditure action with an arbitrary colony target", async () => { - const { colony: otherColony } = await setupRandomColony(colonyNetwork); - await otherColony.setArbitrationRole(1, UINT256_MAX, voting.address, 1, true); - - await otherColony.makeExpenditure(1, UINT256_MAX, 1); - const expenditureId = await otherColony.getExpenditureCount(); - - // Set finalizedTimestamp to WAD - const action = await encodeTxData(otherColony, "setExpenditureState", [ - 1, - UINT256_MAX, - expenditureId, - 25, - [true], - [bn2bytes32(new BN(3))], - WAD32, - ]); - - await voting.createRootMotion(otherColony.address, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); - motionId = await voting.getMotionCount(); - - await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - - let expenditure = await otherColony.getExpenditure(expenditureId); - expect(expenditure.globalClaimDelay).to.eq.BN(UINT256_MAX.divn(3)); - - await forwardTime(STAKE_PERIOD, this); - - await voting.finalizeMotion(motionId); - - expenditure = await otherColony.getExpenditure(expenditureId); - expect(expenditure.globalClaimDelay).to.be.zero; - }); - it("can take a nonexistent action", async () => { const action = soliditySha3("foo"); await voting.createRootMotion(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); From 835848ee73fe0fa29075ac6642841f1bca13cb83 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Tue, 1 Sep 2020 17:28:52 -0700 Subject: [PATCH 56/61] Cannot create domain motions with non-permissioned functions --- contracts/colony/Colony.sol | 8 ++++++-- contracts/colony/IColony.sol | 13 +++++++++---- contracts/extensions/VotingReputation.sol | 3 +++ test/extensions/voting-rep.js | 9 +++++++++ 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/contracts/colony/Colony.sol b/contracts/colony/Colony.sol index 6f227a5087..c48f9c6079 100755 --- a/contracts/colony/Colony.sol +++ b/contracts/colony/Colony.sol @@ -105,8 +105,12 @@ contract Colony is ColonyStorage, PatriciaTreeProofs { ); } - function getUserRoles(address who, uint256 where) public view returns (bytes32) { - return ColonyAuthority(address(authority)).getUserRoles(who, where); + function getUserRoles(address _user, uint256 _domainId) public view returns (bytes32) { + return ColonyAuthority(address(authority)).getUserRoles(_user, _domainId); + } + + function getCapabilityRoles(bytes4 _sig) public view returns (bytes32) { + return ColonyAuthority(address(authority)).getCapabilityRoles(address(this), _sig); } function getColonyNetwork() public view returns (address) { diff --git a/contracts/colony/IColony.sol b/contracts/colony/IColony.sol index fea0780705..752159bb15 100644 --- a/contracts/colony/IColony.sol +++ b/contracts/colony/IColony.sol @@ -122,10 +122,15 @@ contract IColony is ColonyDataTypes, IRecovery { public view returns (bool hasRole); /// @notice Gets the bytes32 representation of the roles for a user in a given domain - /// @param who The user whose roles we want to get - /// @param where The domain where we want to get roles for - /// @return roles bytes32 representation of the roles - function getUserRoles(address who, uint256 where) public view returns (bytes32 roles); + /// @param _user The user whose roles we want to get + /// @param _domain The_domain domain where we want to get roles for + /// @return roles bytes32 representation of the held roles + function getUserRoles(address _user, uint256 _domain) public view returns (bytes32 roles); + + /// @notice Gets the bytes32 representation of the roles authorized to call a function + /// @param _sig The function signature + /// @return roles bytes32 representation of the authorized roles + function getCapabilityRoles(bytes4 _sig) public view returns (bytes32 roles); /// @notice Emit a negative domain reputation update. Available only to Arbitration role holders /// @param _permissionDomainId The domainId in which I hold the Arbitration role diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index 710f0f304e..7efdced7ad 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -19,6 +19,7 @@ pragma solidity 0.5.8; pragma experimental ABIEncoderV2; import "./../../lib/dappsys/math.sol"; +import "./../../lib/dappsys/roles.sol"; import "./../colony/ColonyDataTypes.sol"; import "./../colony/IColony.sol"; import "./../colonyNetwork/IColonyNetwork.sol"; @@ -225,6 +226,8 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { ) public { + require(colony.getCapabilityRoles(getSig(_action)) != bytes32(0), "voting-rep-invalid-function"); + uint256 domainSkillId = colony.getDomain(_domainId).skillId; uint256 actionDomainSkillId = getActionDomainSkillId(_action); diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index 9319b87014..b5f228ad15 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -360,6 +360,15 @@ contract("Voting Reputation", (accounts) => { ); }); + it("cannot create a domain motion with a non-permissioned function as the action", async () => { + const action = await encodeTxData(colony, "claimColonyFunds", [token.address]); + + await checkErrorRevert( + voting.createDomainMotion(1, UINT256_MAX, action, domain1Key, domain1Value, domain1Mask, domain1Siblings), + "voting-rep-invalid-function" + ); + }); + it("cannot externally escalate a domain motion with an invalid domain proof", async () => { const key = makeReputationKey(colony.address, domain3.skillId); const value = makeReputationValue(WAD.muln(3), 7); From fcf80fed211c9154c9aae55c6d4a2197293279bf Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Wed, 2 Sep 2020 07:21:44 -0700 Subject: [PATCH 57/61] Prevent domain motions for non- or root-permissioned functions --- contracts/extensions/VotingReputation.sol | 10 +++++++++- test/extensions/voting-rep.js | 15 ++++++++++++--- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index 7efdced7ad..8e4114414c 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -53,6 +53,11 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { uint256 constant SUBMIT_END = 1; uint256 constant REVEAL_END = 2; + bytes32 constant ROOT_ROLES = ( + bytes32(uint256(1)) << uint8(ColonyDataTypes.ColonyRole.Recovery) | + bytes32(uint256(1)) << uint8(ColonyDataTypes.ColonyRole.Root) + ); + bytes4 constant CHANGE_FUNCTION = bytes4( keccak256("setExpenditureState(uint256,uint256,uint256,uint256,bool[],bytes32[],bytes32)") ); @@ -226,7 +231,10 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { ) public { - require(colony.getCapabilityRoles(getSig(_action)) != bytes32(0), "voting-rep-invalid-function"); + require( + colony.getCapabilityRoles(getSig(_action)) & ~ROOT_ROLES != bytes32(0), + "voting-rep-invalid-function" + ); uint256 domainSkillId = colony.getDomain(_domainId).skillId; uint256 actionDomainSkillId = getActionDomainSkillId(_action); diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index b5f228ad15..bc2a00557a 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -360,11 +360,20 @@ contract("Voting Reputation", (accounts) => { ); }); - it("cannot create a domain motion with a non-permissioned function as the action", async () => { - const action = await encodeTxData(colony, "claimColonyFunds", [token.address]); + it("cannot create a domain motion with a non-domain-permissioned function as the action", async () => { + // Unpermissioned action + const action1 = await encodeTxData(colony, "claimColonyFunds", [token.address]); + + // Root permissioned action + const action2 = await encodeTxData(colony, "upgrade", [2]); + + await checkErrorRevert( + voting.createDomainMotion(1, UINT256_MAX, action1, domain1Key, domain1Value, domain1Mask, domain1Siblings), + "voting-rep-invalid-function" + ); await checkErrorRevert( - voting.createDomainMotion(1, UINT256_MAX, action, domain1Key, domain1Value, domain1Mask, domain1Siblings), + voting.createDomainMotion(1, UINT256_MAX, action2, domain1Key, domain1Value, domain1Mask, domain1Siblings), "voting-rep-invalid-function" ); }); From 83b278bfab948454a1d3facb573aeb16bf33bb7b Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Wed, 2 Sep 2020 07:28:31 -0700 Subject: [PATCH 58/61] Refactor conditional logic in setExpenditureState --- contracts/colony/ColonyExpenditure.sol | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/contracts/colony/ColonyExpenditure.sol b/contracts/colony/ColonyExpenditure.sol index 0827242ff7..6a9871a32b 100644 --- a/contracts/colony/ColonyExpenditure.sol +++ b/contracts/colony/ColonyExpenditure.sol @@ -196,31 +196,25 @@ contract ColonyExpenditure is ColonyStorage { expenditureExists(_id) authDomain(_permissionDomainId, _childSkillIndex, expenditures[_id].domainId) { - require( - _storageSlot == EXPENDITURES_SLOT || - _storageSlot == EXPENDITURESLOTS_SLOT || - _storageSlot == EXPENDITURESLOTPAYOUTS_SLOT, - "colony-expenditure-bad-slot" - ); - // Only allow editing expenditure status, owner, and finalizedTimestamp // Note that status + owner occupy one slot if (_storageSlot == EXPENDITURES_SLOT) { require(_keys.length == 1, "colony-expenditure-bad-keys"); uint256 offset = uint256(_keys[0]); require(offset == 0 || offset == 3 || offset == 4, "colony-expenditure-bad-offset"); - } // Explicitly whitelist all slots, in case we add new slots in the future - if (_storageSlot == EXPENDITURESLOTS_SLOT) { + } else if (_storageSlot == EXPENDITURESLOTS_SLOT) { require(_keys.length >= 2, "colony-expenditure-bad-keys"); uint256 offset = uint256(_keys[1]); require(offset <= 3, "colony-expenditure-bad-offset"); - } // Should always be two mappings - if (_storageSlot == EXPENDITURESLOTPAYOUTS_SLOT) { + } else if (_storageSlot == EXPENDITURESLOTPAYOUTS_SLOT) { require(_keys.length == 2, "colony-expenditure-bad-keys"); + + } else { + require(false, "colony-expenditure-bad-slot"); } executeStateChange(keccak256(abi.encode(_id, _storageSlot)), _mask, _keys, _value); From 6ab7cb8f2c06a484c0a2765ba707808462327cdc Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Wed, 2 Sep 2020 17:37:13 -0700 Subject: [PATCH 59/61] Fix eslint issues --- test/extensions/voting-rep.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index bc2a00557a..b36b0deda7 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -515,7 +515,15 @@ contract("Voting Reputation", (accounts) => { await otherColony.finalizeExpenditure(expenditureId); // Set finalizedTimestamp to WAD - const action = await encodeTxData(otherColony, "setExpenditureState", [1, UINT256_MAX, expenditureId, 25, [true], [bn2bytes32(new BN(3))], WAD32]); + const action = await encodeTxData(otherColony, "setExpenditureState", [ + 1, + UINT256_MAX, + expenditureId, + 25, + [true], + [bn2bytes32(new BN(3))], + WAD32, + ]); await voting.createRootMotion(otherColony.address, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); motionId = await voting.getMotionCount(); From dc95acca84857f676d9c3a7a6962e5dc1aa5c279 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 4 Sep 2020 12:05:39 +0100 Subject: [PATCH 60/61] Update smoke test expected stateRoot --- docs/_Interface_IColony.md | 23 ++++++++++++++++++++--- test-smoke/colony-storage-consistent.js | 10 +++++----- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/docs/_Interface_IColony.md b/docs/_Interface_IColony.md index 8ad5813f0b..1c5239e48c 100644 --- a/docs/_Interface_IColony.md +++ b/docs/_Interface_IColony.md @@ -367,6 +367,23 @@ View an approval to obligate tokens. |---|---|---| |approval|uint256|The amount the user has approved +### `getCapabilityRoles` + +Gets the bytes32 representation of the roles authorized to call a function + + +**Parameters** + +|Name|Type|Description| +|---|---|---| +|_sig|bytes4|The function signature + +**Return Parameters** + +|Name|Type|Description| +|---|---|---| +|roles|bytes32|bytes32 representation of the authorized roles + ### `getColonyNetwork` Returns the colony network address set on the Colony. @@ -801,14 +818,14 @@ Gets the bytes32 representation of the roles for a user in a given domain |Name|Type|Description| |---|---|---| -|who|address|The user whose roles we want to get -|where|uint256|The domain where we want to get roles for +|_user|address|The user whose roles we want to get +|_domain|uint256|The_domain domain where we want to get roles for **Return Parameters** |Name|Type|Description| |---|---|---| -|roles|bytes32|bytes32 representation of the roles +|roles|bytes32|bytes32 representation of the held roles ### `hasInheritedUserRole` diff --git a/test-smoke/colony-storage-consistent.js b/test-smoke/colony-storage-consistent.js index 8576618507..903f4e6c2b 100644 --- a/test-smoke/colony-storage-consistent.js +++ b/test-smoke/colony-storage-consistent.js @@ -153,15 +153,15 @@ contract("Contract Storage", (accounts) => { console.log("miningCycleStateHash:", miningCycleAccount.stateRoot.toString("hex")); console.log("tokenLockingStateHash:", tokenLockingAccount.stateRoot.toString("hex")); - expect(colonyNetworkAccount.stateRoot.toString("hex")).to.equal("0941ada3434f80dfd130d0a2dc07fbc2a13b73e415f14e650bb725a9089a061f"); + expect(colonyNetworkAccount.stateRoot.toString("hex")).to.equal("4795c6cf36580719d2b3122e8a2d314024e5eb67ef0925dc35c5bbbcb26e20c9"); - expect(colonyAccount.stateRoot.toString("hex")).to.equal("f015594ef3fa0c15991fa41d255cfb7709f477545021b8fba487a50f6e8b404a"); + expect(colonyAccount.stateRoot.toString("hex")).to.equal("d11be7ccfed823de8fff1713f0ed3c3cc25572ab32d3a05db8d02e28b6b19271"); - expect(metaColonyAccount.stateRoot.toString("hex")).to.equal("33454eeb259439cf54a923c76de531d18954bdafcf29a6ccf0be586a73a7dbd5"); + expect(metaColonyAccount.stateRoot.toString("hex")).to.equal("004cce93f8bca95ddf05b7737bc758804c0d200a361638c372910849f6ca9b63"); - expect(miningCycleAccount.stateRoot.toString("hex")).to.equal("9b743931645356d469af52165654a497ee4af95ec1029601d10162d5d2d377c0"); + expect(miningCycleAccount.stateRoot.toString("hex")).to.equal("47ed02166271709fee6816f42d511e3d0c56e4ee5fc1f2e24c5f05a3954d0292"); - expect(tokenLockingAccount.stateRoot.toString("hex")).to.equal("90f687f776283dc8c84d7de3e9d233974cdf218e2e51536643d125ae0ae43ebf"); + expect(tokenLockingAccount.stateRoot.toString("hex")).to.equal("97a15b06a12c2300869568a1b32f6f60c9b87d0102ee5de7f52290b49c0eb891"); }); }); }); From 0d0a788a3196c38d205b91b9057524bd3c6a55f4 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Fri, 4 Sep 2020 10:24:24 -0700 Subject: [PATCH 61/61] Respond to review comments XII --- contracts/colony/IColony.sol | 2 +- contracts/extensions/VotingReputation.sol | 30 ++++++++++++----------- test/extensions/voting-rep.js | 14 +++++------ 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/contracts/colony/IColony.sol b/contracts/colony/IColony.sol index 752159bb15..ce85de4209 100644 --- a/contracts/colony/IColony.sol +++ b/contracts/colony/IColony.sol @@ -123,7 +123,7 @@ contract IColony is ColonyDataTypes, IRecovery { /// @notice Gets the bytes32 representation of the roles for a user in a given domain /// @param _user The user whose roles we want to get - /// @param _domain The_domain domain where we want to get roles for + /// @param _domain The domain we want to get roles in /// @return roles bytes32 representation of the held roles function getUserRoles(address _user, uint256 _domain) public view returns (bytes32 roles); diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index 8e4114414c..2e84bcdb61 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -186,7 +186,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { mapping (uint256 => mapping (address => mapping (uint256 => uint256))) stakes; mapping (uint256 => mapping (address => bytes32)) voteSecrets; - mapping (bytes32 => uint256) expenditurePastMotions; // expenditure slot signature => voting power + mapping (bytes32 => uint256) expenditurePastVotes; // expenditure slot signature => voting power mapping (bytes32 => uint256) expenditureMotionCounts; // expenditure struct signature => count // Public functions (interface) @@ -231,8 +231,9 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { ) public { + // Check the function requires a non-root permission (and thus a domain proof) require( - colony.getCapabilityRoles(getSig(_action)) & ~ROOT_ROLES != bytes32(0), + colony.getCapabilityRoles(getSig(_action)) | ROOT_ROLES != ROOT_ROLES, "voting-rep-invalid-function" ); @@ -306,7 +307,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { getSig(motion.action) == CHANGE_FUNCTION && motion.altTarget == address(0x0) ) { - bytes32 structHash = hashExpenditureStruct(motion.action); + bytes32 structHash = hashExpenditureActionStruct(motion.action); expenditureMotionCounts[structHash] = add(expenditureMotionCounts[structHash], 1); // Set to UINT256_MAX / 3 to avoid overflow (finalizedTimestamp + globalClaimDelay + claimDelay) bytes memory claimDelayAction = createClaimDelayAction(motion.action, UINT256_MAX / 3); @@ -494,7 +495,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { ); if (getSig(motion.action) == CHANGE_FUNCTION && motion.altTarget == address(0x0)) { - bytes32 structHash = hashExpenditureStruct(motion.action); + bytes32 structHash = hashExpenditureActionStruct(motion.action); expenditureMotionCounts[structHash] = sub(expenditureMotionCounts[structHash], 1); // Release the claimDelay if this is the last active motion @@ -504,12 +505,12 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { executeCall(_motionId, claimDelayAction); } - bytes32 slotHash = hashExpenditureSlot(motion.action); + bytes32 actionHash = hashExpenditureAction(motion.action); uint256 votePower = (add(motion.votes[NAY], motion.votes[YAY]) > 0) ? motion.votes[YAY] : motion.stakes[YAY]; - if (expenditurePastMotions[slotHash] < votePower) { - expenditurePastMotions[slotHash] = votePower; + if (expenditurePastVotes[actionHash] < votePower) { + expenditurePastVotes[actionHash] = votePower; canExecute = canExecute && true; } else { canExecute = canExecute && false; @@ -639,7 +640,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { return stakes[_motionId][_staker][_vote]; } - /// @notice Get the number of ongoing motions for a single expenditure / slot + /// @notice Get the number of ongoing motions for a single expenditure / expenditure slot /// @param _structHash The hash of the expenditureId or expenditureId*expenditureSlot /// @return The number of ongoing motions function getExpenditureMotionCount(bytes32 _structHash) public view returns (uint256) { @@ -647,10 +648,10 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { } /// @notice Get the largest past vote on a single expenditure variable - /// @param _slotHash The hash of the particular expenditure slot + /// @param _actionHash The hash of the particular expenditure action /// @return The largest past vote on this variable - function getExpenditurePastMotion(bytes32 _slotHash) public view returns (uint256) { - return expenditurePastMotions[_slotHash]; + function getExpenditurePastVote(bytes32 _actionHash) public view returns (uint256) { + return expenditurePastVotes[_actionHash]; } /// @notice Get the current state of the motion @@ -908,9 +909,10 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { } } - function hashExpenditureSlot(bytes memory action) internal returns (bytes32 hash) { + function hashExpenditureAction(bytes memory action) internal returns (bytes32 hash) { assembly { - // Hash all but the domain proof and value + // Hash all but the domain proof and value, so actions for the same + // storage slot return the same value. // Recall: mload(action) gives length of bytes array // So skip past the three bytes32 (length + domain proof), // plus 4 bytes for the sig. Subtract the same from the end, less @@ -920,7 +922,7 @@ contract VotingReputation is DSMath, PatriciaTreeProofs { } } - function hashExpenditureStruct(bytes memory action) internal returns (bytes32 hash) { + function hashExpenditureActionStruct(bytes memory action) internal returns (bytes32 hash) { assert(getSig(action) == CHANGE_FUNCTION); uint256 expenditureId; diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index b36b0deda7..9d2069d26d 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -508,7 +508,7 @@ contract("Voting Reputation", (accounts) => { await checkErrorRevert(colony.claimExpenditurePayout(expenditureId, 0, token.address), "colony-expenditure-cannot-claim"); }); - it("cannot update the expenditure globalClaimDelay if the target is another colony", async () => { + it("does not update the expenditure globalClaimDelay if the target is another colony", async () => { const { colony: otherColony } = await setupRandomColony(colonyNetwork); await otherColony.makeExpenditure(1, UINT256_MAX, 1); const expenditureId = await otherColony.getExpenditureCount(); @@ -940,12 +940,12 @@ contract("Voting Reputation", (accounts) => { ); }); - it("cannot reveal a vote if voting is open", async () => { + it("cannot reveal a vote during the submit period", async () => { await voting.submitVote(motionId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await checkErrorRevert(voting.revealVote(motionId, SALT, YAY, FAKE, FAKE, 0, [], { from: USER0 }), "voting-rep-motion-not-reveal"); }); - it("cannot reveal a vote after voting closes", async () => { + it("cannot reveal a vote after the reveal period ends", async () => { await voting.submitVote(motionId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); await forwardTime(SUBMIT_PERIOD, this); @@ -1210,8 +1210,8 @@ contract("Voting Reputation", (accounts) => { await voting.finalizeMotion(motionId); const slotHash = hashExpenditureSlot(action); - const pastMotion = await voting.getExpenditurePastMotion(slotHash); - expect(pastMotion).to.eq.BN(WAD); // USER0 had 1 WAD of reputation + const pastVote = await voting.getExpenditurePastVote(slotHash); + expect(pastVote).to.eq.BN(WAD); // USER0 had 1 WAD of reputation }); it("can use vote power correctly for different values of the same variable", async () => { @@ -1258,8 +1258,8 @@ contract("Voting Reputation", (accounts) => { await voting.finalizeMotion(motionId); const slotHash = hashExpenditureSlot(action); - const pastMotion = await voting.getExpenditurePastMotion(slotHash); - expect(pastMotion).to.eq.BN(REQUIRED_STAKE); + const pastVote = await voting.getExpenditurePastVote(slotHash); + expect(pastVote).to.eq.BN(REQUIRED_STAKE); }); });