diff --git a/contracts/colony/ColonyFunding.sol b/contracts/colony/ColonyFunding.sol
index d16f0f774f..5e13c29884 100755
--- a/contracts/colony/ColonyFunding.sol
+++ b/contracts/colony/ColonyFunding.sol
@@ -277,8 +277,7 @@ contract ColonyFunding is ColonyStorage, PatriciaTreeProofs { // ignore-swc-123
function startNextRewardPayout(address _token, bytes memory key, bytes memory value, uint256 branchMask, bytes32[] memory siblings)
public stoppable auth
{
- ITokenLocking tokenLocking = ITokenLocking(tokenLockingAddress);
- uint256 totalLockCount = tokenLocking.lockToken(token);
+ uint256 totalLockCount = ITokenLocking(tokenLockingAddress).lockToken(token);
uint256 thisPayoutAmount = sub(fundingPots[0].balance[_token], pendingRewardPayments[_token]);
require(thisPayoutAmount > 0, "colony-reward-payout-no-rewards");
pendingRewardPayments[_token] = add(pendingRewardPayments[_token], thisPayoutAmount);
@@ -444,7 +443,6 @@ contract ColonyFunding is ColonyStorage, PatriciaTreeProofs { // ignore-swc-123
require(mul(squareRoots[4], squareRoots[4]) <= numerator, "colony-reward-payout-invalid-parameter-numerator");
require(mul(squareRoots[5], squareRoots[5]) >= denominator, "colony-reward-payout-invalid-parameter-denominator");
-
uint256 reward = (mul(squareRoots[4], squareRoots[6]) / squareRoots[5]) ** 2;
return (payout.tokenAddress, reward);
diff --git a/contracts/extensions/VotingBase.sol b/contracts/extensions/VotingBase.sol
new file mode 100644
index 0000000000..9f91445d9f
--- /dev/null
+++ b/contracts/extensions/VotingBase.sol
@@ -0,0 +1,1050 @@
+/*
+ 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.7.3;
+pragma experimental ABIEncoderV2;
+
+import "./../colony/ColonyRoles.sol";
+import "./../colonyNetwork/IColonyNetwork.sol";
+import "./../patriciaTree/PatriciaTreeProofs.sol";
+import "./../tokenLocking/ITokenLocking.sol";
+import "./ColonyExtensionMeta.sol";
+
+
+abstract contract VotingBase is ColonyExtensionMeta, PatriciaTreeProofs {
+
+ // Events
+
+ 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, uint256 indexed newDomainId);
+ event MotionRewardClaimed(uint256 indexed motionId, address indexed staker, uint256 indexed vote, uint256 amount);
+ event MotionEventSet(uint256 indexed motionId, uint256 eventIndex);
+
+ // Constants
+
+ uint256 constant UINT128_MAX = 2**128 - 1;
+
+ uint256 constant NAY = 0;
+ uint256 constant YAY = 1;
+
+ uint256 constant STAKE_END = 0;
+ 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_SIG = bytes4(keccak256(
+ "setExpenditureState(uint256,uint256,uint256,uint256,bool[],bytes32[],bytes32)"
+ ));
+
+ bytes4 constant OLD_MOVE_FUNDS_SIG = bytes4(keccak256(
+ "moveFundsBetweenPots(uint256,uint256,uint256,uint256,uint256,uint256,address)"
+ ));
+
+ enum ExtensionState { Deployed, Active, Deprecated }
+
+ // Data structures
+
+ enum MotionState { Null, Staking, Submit, Reveal, Closed, Finalizable, Finalized, Failed }
+
+ struct Motion {
+ uint64[3] events; // For recording motion lifecycle timestamps (STAKE, SUBMIT, REVEAL)
+ bytes32 rootHash;
+ uint256 domainId;
+ uint256 skillId;
+ uint256 paidVoterComp;
+ uint256[2] pastVoterComp; // [nay, yay]
+ uint256[2] stakes; // [nay, yay]
+ uint256[2][] votes; // [nay, yay]
+ uint256[] totalVotes;
+ uint256[] maxVotes;
+ address target;
+ bool escalated;
+ bool finalized;
+ bytes action;
+ }
+
+ // Storage variables
+
+ ExtensionState state;
+
+ IColonyNetwork colonyNetwork;
+ ITokenLocking tokenLocking;
+ address token;
+
+ // 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 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.
+
+ 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.
+
+ // 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
+ uint256 escalationPeriod; // Length of time for escalating after a vote
+
+ uint256 motionCount;
+ mapping (uint256 => Motion) motions;
+ mapping (uint256 => mapping (address => mapping (uint256 => uint256))) stakes;
+ mapping (uint256 => mapping (address => bytes32)) voteSecrets;
+
+ mapping (bytes32 => uint256) expenditurePastVotes; // expenditure slot signature => voting power
+
+ // Modifiers
+
+ modifier onlyRoot() {
+ require(colony.hasUserRole(msg.sender, 1, ColonyDataTypes.ColonyRole.Root), "voting-not-root");
+ _;
+ }
+
+ // Virtual functions
+
+ function postSubmit(uint256 _motionId, address _user) internal virtual;
+ function postReveal(uint256 _motionId, address _user) internal virtual;
+ function postClaim(uint256 _motionId, address _user) internal virtual;
+
+ // Public functions
+
+ /// @notice Install the extension
+ /// @param _colony Base colony for the installation
+ function install(address _colony) public override {
+ require(address(colony) == address(0x0), "extension-already-installed");
+
+ colony = IColony(_colony);
+ colonyNetwork = IColonyNetwork(colony.getColonyNetwork());
+ tokenLocking = ITokenLocking(colonyNetwork.getTokenLocking());
+ token = colony.getToken();
+ }
+
+ /// @notice Called when upgrading the extension
+ function finishUpgrade() public override auth {} // solhint-disable-line no-empty-blocks
+
+ /// @notice Called when deprecating (or undeprecating) the extension
+ function deprecate(bool _deprecated) public override auth {
+ deprecated = _deprecated;
+ }
+
+ /// @notice Called when uninstalling the extension
+ function uninstall() public override auth {
+ selfdestruct(address(uint160(address(colony))));
+ }
+
+ /// @notice Initialise the extension
+ /// @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
+ /// @param _escalationPeriod The length of the escalation period in seconds
+ function initialise(
+ uint256 _totalStakeFraction,
+ uint256 _voterRewardFraction,
+ uint256 _userMinStakeFraction,
+ uint256 _maxVoteFraction,
+ uint256 _stakePeriod,
+ uint256 _submitPeriod,
+ uint256 _revealPeriod,
+ uint256 _escalationPeriod
+ )
+ public
+ onlyRoot
+ {
+ require(state == ExtensionState.Deployed, "voting-already-initialised");
+
+ string memory valueError = "voting-invalid-value";
+
+ require(_totalStakeFraction <= WAD / 2, valueError);
+ require(_voterRewardFraction <= WAD / 2, valueError);
+
+ require(_userMinStakeFraction <= WAD, valueError);
+ require(_maxVoteFraction <= WAD, valueError);
+
+ require(_stakePeriod <= 365 days, valueError);
+ require(_submitPeriod <= 365 days, valueError);
+ require(_revealPeriod <= 365 days, valueError);
+ require(_escalationPeriod <= 365 days, valueError);
+
+ state = ExtensionState.Active;
+
+ totalStakeFraction = _totalStakeFraction;
+ voterRewardFraction = _voterRewardFraction;
+
+ userMinStakeFraction = _userMinStakeFraction;
+ maxVoteFraction = _maxVoteFraction;
+
+ stakePeriod = _stakePeriod;
+ submitPeriod = _submitPeriod;
+ revealPeriod = _revealPeriod;
+ escalationPeriod = _escalationPeriod;
+
+ emit ExtensionInitialised();
+ }
+
+ /// @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)
+ function internalRevealVote(uint256 _motionId, bytes32 _salt, uint256 _vote, uint256[] memory _influence) public {
+
+ Motion storage motion = motions[_motionId];
+
+ require(getMotionState(_motionId) == MotionState.Reveal, "voting-not-reveal");
+ require(_vote <= 1, "voting-bad-vote");
+
+ for (uint256 i; i < motion.votes.length; i++) {
+ motion.votes[i][_vote] = add(motion.votes[i][_vote], _influence[i]);
+ }
+
+ bytes32 voteSecret = voteSecrets[_motionId][msg.sender];
+ require(voteSecret == getVoteSecret(_salt, _vote), "voting-secret-no-match");
+ delete voteSecrets[_motionId][msg.sender];
+
+ uint256 voterReward = getVoterReward(_motionId, msg.sender, _influence);
+ motion.paidVoterComp = add(motion.paidVoterComp, voterReward);
+
+ emit MotionVoteRevealed(_motionId, msg.sender, _vote);
+
+ postReveal(_motionId, msg.sender);
+
+ tokenLockingTransfer(voterReward, msg.sender);
+ }
+
+ function finalizeMotion(uint256 _motionId) public {
+ Motion storage motion = motions[_motionId];
+
+ require(getMotionState(_motionId) == MotionState.Finalizable, "voting-not-finalizable");
+
+ motion.finalized = true;
+
+ uint256 sumVotes;
+ uint256 yayVotes;
+
+ for (uint256 i; i < motion.votes.length; i++) {
+ sumVotes = add(sumVotes, add(motion.votes[i][NAY], motion.votes[i][YAY]));
+ yayVotes = add(yayVotes, motion.votes[i][YAY]);
+ }
+
+ // Either we're fully staked YAY or we've gone to a vote
+ assert(motion.stakes[YAY] == getRequiredStake(_motionId) || sumVotes > 0);
+
+ bool canExecute = true;
+
+ // If we went to a vote, check if every sub-vote passed
+ if (sumVotes > 0) {
+ for (uint256 j; j < motion.votes.length && canExecute; j++) {
+ canExecute = canExecute && motion.votes[j][NAY] < motion.votes[j][YAY];
+ }
+ }
+
+ // Handle expenditure-related bookkeeping (claim delays, repeated vote checks)
+ if (
+ getSig(motion.action) == CHANGE_FUNCTION_SIG &&
+ motion.target == address(colony)
+ ) {
+ bytes memory claimDelayAction = createClaimDelayAction(motion.action, false);
+ // No require this time, since we don't want stakes to be permanently locked
+ executeCall(address(colony), claimDelayAction);
+
+ bytes32 actionHash = hashExpenditureAction(motion.action);
+ uint256 votePower = (sumVotes > 0) ? yayVotes : motion.stakes[YAY];
+
+ if (expenditurePastVotes[actionHash] < votePower) {
+ expenditurePastVotes[actionHash] = votePower;
+ // slither-disable-next-line boolean-cst
+ canExecute = canExecute && true;
+ } else {
+ // slither-disable-next-line boolean-cst
+ canExecute = canExecute && false;
+ }
+ }
+
+ bool executed;
+
+ if (canExecute) {
+ executed = executeCall(motion.target, motion.action);
+ }
+
+ emit MotionFinalized(_motionId, motion.action, executed);
+ }
+
+ /// @notice Claim the staker's reward
+ /// @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 _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 _staker,
+ uint256 _vote
+ )
+ public
+ {
+ require(
+ getMotionState(_motionId) == MotionState.Finalized ||
+ getMotionState(_motionId) == MotionState.Failed,
+ "voting-not-claimable"
+ );
+
+ (uint256 stakerReward, uint256 repPenalty) = getStakerReward(_motionId, _staker, _vote);
+
+ require(stakes[_motionId][_staker][_vote] > 0, "voting-nothing-to-claim");
+ delete stakes[_motionId][_staker][_vote];
+
+ postClaim(_motionId, _staker);
+
+ tokenLockingTransfer(stakerReward, _staker);
+
+ if (repPenalty > 0) {
+ colony.emitDomainReputationPenalty(
+ _permissionDomainId,
+ _childSkillIndex,
+ motions[_motionId].domainId,
+ _staker,
+ -int256(repPenalty)
+ );
+ }
+
+ 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) {
+ return motionCount;
+ }
+
+ /// @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 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 _motionId, address _staker, uint256 _vote) public view returns (uint256) {
+ return stakes[_motionId][_staker][_vote];
+ }
+
+ /// @notice Get the largest past vote on a single expenditure variable
+ /// @param _actionHash The hash of the particular expenditure action
+ /// @return The largest past vote on this variable
+ function getExpenditurePastVote(bytes32 _actionHash) public view returns (uint256) {
+ return expenditurePastVotes[_actionHash];
+ }
+
+ /// @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);
+
+ // Check for valid motion Id
+ if (_motionId == 0 || _motionId > motionCount) {
+
+ return MotionState.Null;
+
+ // If finalized, we're done
+ } else if (motion.finalized) {
+
+ return MotionState.Finalized;
+
+ // Not fully staked
+ } else if (motion.stakes[YAY] < requiredStake || motion.stakes[NAY] < requiredStake) {
+
+ // Are we still staking?
+ if (block.timestamp < motion.events[STAKE_END]) {
+ return MotionState.Staking;
+ // If not, did the YAY side stake?
+ } else if (motion.stakes[YAY] == requiredStake) {
+ return MotionState.Finalizable;
+ // If not, was there a prior (reputation) vote we can fall back on?
+ } else if (
+ (identifier() == keccak256("VotingReputation") ||
+ identifier() == keccak256("VotingReputation2")) &&
+ add(motion.votes[0][NAY], motion.votes[0][YAY]) > 0
+ ) {
+ return MotionState.Finalizable;
+ // Otherwise, the motion failed
+ } else {
+ return MotionState.Failed;
+ }
+
+ // Fully staked, go to a vote
+ } else {
+
+ if (block.timestamp < motion.events[SUBMIT_END]) {
+ return MotionState.Submit;
+ } else if (block.timestamp < motion.events[REVEAL_END]) {
+ return MotionState.Reveal;
+ } else if (
+ motion.domainId > 1 &&
+ block.timestamp < motion.events[REVEAL_END] + escalationPeriod
+ ) {
+ return MotionState.Closed;
+ } else {
+ return MotionState.Finalizable;
+ }
+
+ }
+ }
+
+ /// @notice Get the voter reward
+ /// NB This function will only return a meaningful value if in the reveal state.
+ /// Prior to the reveal state, getVoterRewardRange should be used.
+ /// @param _motionId The id of the motion
+ /// @param _user The address of the the voter
+ /// @return The voter reward
+ function getVoterReward(uint256 _motionId, address _user, uint256[] memory _influence) public view returns (uint256) {
+ Motion storage motion = motions[_motionId];
+
+ assert(_influence.length == motion.totalVotes.length);
+
+ // Get the average per-influence fraction
+ uint256 fractionUserInfluence;
+
+ for (uint256 i; i < _influence.length; i++) {
+ if (motion.totalVotes[i] > 0) {
+ fractionUserInfluence = add(fractionUserInfluence, wdiv(_influence[i], motion.totalVotes[i]));
+ }
+ }
+
+ fractionUserInfluence = fractionUserInfluence / _influence.length;
+
+ return wmul(wmul(fractionUserInfluence, add(motion.stakes[YAY], motion.stakes[NAY])), voterRewardFraction);
+ }
+
+ /// @notice Get the range of potential rewards for a voter on a specific motion, intended to be
+ /// used when the motion is in the reveal state.
+ /// Once a motion is in the reveal state the reward is known, and getVoterRewardRange should be used.
+ /// @param _motionId The id of the motion
+ /// @param _user The address of the user
+ /// @return The voter reward
+ function getVoterRewardRange(uint256 _motionId, address _user, uint256[] memory _influence) public view returns (uint256, uint256) {
+ Motion storage motion = motions[_motionId];
+
+ assert(_influence.length == motion.totalVotes.length);
+
+ uint256 minFractionUserInfluence;
+ uint256 maxFractionUserInfluence;
+
+ // The minimum reward is when everyone has voted, with a total weight of motion.maxVotes
+ // The maximum reward is when this user is the only other person who votes (if they haven't already),
+ // aside from those who have already done so
+
+ for (uint256 i; i < _influence.length; i++) {
+ // If user hasn't voted, add their influence to totalVotes
+ uint256 pendingVote = (voteSecrets[_motionId][_user] == bytes32(0)) ? _influence[i] : 0;
+
+ if (motion.maxVotes[i] > 0) {
+ minFractionUserInfluence = add(minFractionUserInfluence, wdiv(_influence[i], motion.maxVotes[i]));
+ }
+
+ if (add(motion.totalVotes[i], pendingVote) > 0) {
+ maxFractionUserInfluence = add(maxFractionUserInfluence, wdiv(_influence[i], add(motion.totalVotes[i], pendingVote)));
+ }
+ }
+
+ minFractionUserInfluence = minFractionUserInfluence / _influence.length;
+ maxFractionUserInfluence = maxFractionUserInfluence / _influence.length;
+
+ return (
+ wmul(wmul(minFractionUserInfluence, add(motion.stakes[YAY], motion.stakes[NAY])), voterRewardFraction),
+ wmul(wmul(maxFractionUserInfluence, add(motion.stakes[YAY], motion.stakes[NAY])), 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 totalSideStake = add(motion.stakes[_vote], motion.pastVoterComp[_vote]);
+ if (totalSideStake == 0) { return (0, 0); }
+
+ uint256 stakeFraction = wdiv(stakes[_motionId][_staker][_vote], totalSideStake);
+ uint256 realStake = wmul(stakeFraction, motion.stakes[_vote]);
+
+ uint256 stakerReward;
+ uint256 repPenalty;
+
+ bool wasVote;
+
+ for (uint256 i; i < motion.votes.length && !wasVote; i++) {
+ wasVote = add(motion.votes[i][NAY], motion.votes[i][YAY]) > 0;
+ }
+
+ if (wasVote) {
+ // Went to a vote, use vote to determine reward or penalty
+ (stakerReward, repPenalty) = getStakerRewardByVote(_motionId, _vote, stakeFraction, realStake);
+ } else {
+ // Determine rewards based on stakes alone
+ (stakerReward, repPenalty) = getStakerRewardByStake(_motionId, _vote, stakeFraction, realStake);
+ }
+
+ return (stakerReward, repPenalty);
+ }
+
+ // Internal functions
+
+ function createMotionInternal(
+ uint256 _domainId,
+ uint256 _childSkillIndex,
+ address _target,
+ bytes memory _action,
+ uint256 _numInfluences
+ )
+ internal
+ {
+ require(state == ExtensionState.Active, "voting-not-active");
+
+ address target = (_target == address(0x0)) ? address(colony) : _target;
+ bytes4 actionSig = getSig(_action);
+ uint256 skillId = getDomainSkillId(_domainId);
+
+ require(actionSig != OLD_MOVE_FUNDS_SIG, "voting-bad-function");
+
+ if (ColonyRoles(target).getCapabilityRoles(actionSig) | ROOT_ROLES == ROOT_ROLES) {
+
+ // A root or unpermissioned function
+ require(_domainId == 1 && _childSkillIndex == UINT256_MAX, "voting-invalid-domain");
+
+ } else {
+
+ // A domain permissioned function
+ uint256 actionDomainSkillId = getActionDomainSkillId(_action);
+
+ if (skillId != actionDomainSkillId) {
+ uint256 childSkillId = getChildSkillId(skillId, _childSkillIndex);
+ require(childSkillId == actionDomainSkillId, "voting-invalid-domain");
+ } else {
+ require(_childSkillIndex == UINT256_MAX, "voting-invalid-domain");
+ }
+ }
+
+ motionCount += 1;
+ Motion storage motion = motions[motionCount];
+
+ motion.events[STAKE_END] = uint64(block.timestamp + stakePeriod);
+ motion.events[SUBMIT_END] = motion.events[STAKE_END] + uint64(submitPeriod);
+ motion.events[REVEAL_END] = motion.events[SUBMIT_END] + uint64(revealPeriod);
+
+ motion.rootHash = colonyNetwork.getReputationRootHash();
+ motion.domainId = _domainId;
+ motion.skillId = skillId;
+
+ motion.target = target;
+ motion.action = _action;
+
+ motion.votes = new uint256[2][](_numInfluences);
+ motion.totalVotes = new uint256[](_numInfluences);
+ motion.maxVotes = new uint256[](_numInfluences);
+
+ emit MotionCreated(motionCount, msg.sender, _domainId);
+ }
+
+ function internalStakeMotion(
+ uint256 _motionId,
+ uint256 _permissionDomainId,
+ uint256 _childSkillIndex,
+ uint256 _vote,
+ uint256 _amount,
+ uint256[] memory _influence
+ )
+ internal
+ {
+ Motion storage motion = motions[_motionId];
+
+ require(_vote <= 1, "voting-bad-vote");
+ require(getMotionState(_motionId) == MotionState.Staking, "voting-not-staking");
+
+ uint256 requiredStake = getRequiredStake(_motionId);
+ uint256 amount = min(_amount, sub(requiredStake, motion.stakes[_vote]));
+ require(amount > 0, "voting-bad-amount");
+
+ uint256 stakerTotalAmount = add(stakes[_motionId][msg.sender][_vote], amount);
+
+ uint256 sumInfluence;
+
+ for (uint256 i; i < _influence.length; i++) {
+ sumInfluence = add(sumInfluence, _influence[i]);
+ }
+
+ require(
+ stakerTotalAmount <= sumInfluence,
+ "voting-insufficient-influence"
+ );
+ require(
+ stakerTotalAmount >= wmul(requiredStake, userMinStakeFraction) ||
+ add(motion.stakes[_vote], amount) == requiredStake, // To prevent a residual stake from being un-stakable
+ "voting-insufficient-stake"
+ );
+
+ // Update the stake
+ 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 &&
+ !motion.escalated &&
+ motion.stakes[YAY] == requiredStake &&
+ getSig(motion.action) == CHANGE_FUNCTION_SIG &&
+ motion.target == address(colony)
+ ) {
+ bytes memory claimDelayAction = createClaimDelayAction(motion.action, true);
+ require(executeCall(address(colony), claimDelayAction), "voting-lock-failed");
+ }
+
+ 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(block.timestamp);
+ 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
+ } else if (
+ (_vote == NAY && motion.stakes[NAY] == requiredStake) ||
+ (_vote == YAY && motion.stakes[YAY] == requiredStake)
+ ) {
+ motion.events[STAKE_END] = uint64(block.timestamp + stakePeriod);
+ motion.events[SUBMIT_END] = motion.events[STAKE_END] + uint64(submitPeriod);
+ motion.events[REVEAL_END] = motion.events[SUBMIT_END] + uint64(revealPeriod);
+
+ // New stake supersedes prior votes
+ for (uint256 j; j < motion.votes.length; j++) {
+ delete motion.votes[j];
+ delete motion.totalVotes[j];
+ }
+
+ emit MotionEventSet(_motionId, STAKE_END);
+ }
+
+ // Do the external bookkeeping
+ tokenLocking.deposit(token, 0, true); // Faux deposit to clear any locks
+ colony.obligateStake(msg.sender, motion.domainId, amount);
+ colony.transferStake(_permissionDomainId, _childSkillIndex, address(this), msg.sender, motion.domainId, amount, address(this));
+ }
+
+ /// @notice Submit a vote secret for a motion
+ /// @param _motionId The id of the motion
+ /// @param _voteSecret The hashed vote secret
+ function internalSubmitVote(uint256 _motionId, bytes32 _voteSecret, uint256[] memory _influence) internal {
+ Motion storage motion = motions[_motionId];
+
+ require(getMotionState(_motionId) == MotionState.Submit, "voting-not-open");
+ require(_voteSecret != bytes32(0), "voting-invalid-secret");
+
+ // Add influence to totals if first submission
+ if (voteSecrets[_motionId][msg.sender] == bytes32(0)) {
+ for (uint256 i; i < motion.totalVotes.length; i++) {
+ motion.totalVotes[i] = add(motion.totalVotes[i], _influence[i]);
+ }
+ }
+
+ voteSecrets[_motionId][msg.sender] = _voteSecret;
+
+ emit MotionVoteSubmitted(_motionId, msg.sender);
+
+ postSubmit(_motionId, msg.sender);
+ }
+
+ function getStakerRewardByVote(
+ uint256 _motionId,
+ uint256 _vote,
+ uint256 _stakeFraction,
+ uint256 _realStake
+ )
+ internal
+ view
+ returns (uint256, uint256)
+ {
+ Motion storage motion = motions[_motionId];
+
+ uint256 stakerReward;
+ uint256 repPenalty;
+
+ bool yayWon = true;
+ uint256 winFraction;
+
+ // Check if every sub-vote passed, and calculate the win fraction
+ for (uint256 i; i < motion.votes.length; i++) {
+ yayWon = yayWon && motion.votes[i][NAY] < motion.votes[i][YAY];
+
+ if (motion.totalVotes[i] > 0) {
+ winFraction = add(winFraction, wdiv(motion.votes[i][_vote], motion.totalVotes[i]));
+ }
+ }
+
+ winFraction = winFraction / motion.votes.length;
+
+ uint256 winnerStake;
+ uint256 loserStake;
+
+ if (yayWon) {
+ winnerStake = motion.stakes[YAY];
+ loserStake = sub(motion.stakes[NAY], motion.paidVoterComp);
+ } else {
+ winnerStake = motion.stakes[NAY];
+ loserStake = sub(motion.stakes[YAY], motion.paidVoterComp);
+ }
+
+ uint256 winShare = wmul(winFraction, 2 * WAD); // On a scale of 0-2 WAD
+
+ if (winShare > WAD || (winShare == WAD && _vote == NAY)) {
+ stakerReward = wmul(_stakeFraction, add(winnerStake, wmul(loserStake, winShare - WAD)));
+ } else {
+ stakerReward = wmul(_stakeFraction, wmul(loserStake, winShare));
+ repPenalty = sub(_realStake, stakerReward);
+ }
+
+ return (stakerReward, repPenalty);
+ }
+
+ function getStakerRewardByStake(
+ uint256 _motionId,
+ uint256 _vote,
+ uint256 _stakeFraction,
+ uint256 _realStake
+ )
+ internal
+ view
+ returns (uint256, uint256)
+ {
+ Motion storage motion = motions[_motionId];
+ assert(motion.paidVoterComp == 0);
+
+ uint256 stakerReward;
+ uint256 repPenalty;
+
+ uint256 requiredStake = getRequiredStake(_motionId);
+
+ // 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);
+ }
+
+ function getVoteSecret(bytes32 _salt, uint256 _vote) internal pure returns (bytes32) {
+ return keccak256(abi.encodePacked(_salt, _vote));
+ }
+
+ function getRequiredStake(uint256 _motionId) public view returns (uint256) {
+ Motion storage motion = motions[_motionId];
+
+ uint256 sumMaxVotes;
+ for (uint256 i; i < motion.maxVotes.length; i++) {
+ sumMaxVotes = add(sumMaxVotes, motion.maxVotes[i]);
+ }
+
+ return wmul(sumMaxVotes, totalStakeFraction);
+ }
+
+ function flip(uint256 _vote) internal pure returns (uint256) {
+ return sub(1, _vote);
+ }
+
+ function getDomainSkillId(uint256 _domainId) internal view returns (uint256) {
+ return colony.getDomain(_domainId).skillId;
+ }
+
+ function getChildSkillId(uint256 _skillId, uint256 _index) internal view returns (uint256) {
+ return colonyNetwork.getChildSkillId(_skillId, _index);
+ }
+
+ function tokenLockingTransfer(uint256 _amount, address _recipient) internal {
+ tokenLocking.transfer(token, _amount, _recipient, true);
+ }
+
+ function getActionDomainSkillId(bytes memory _action) internal view returns (uint256) {
+ uint256 permissionDomainId;
+ uint256 childSkillIndex;
+
+ // By convention, these are the first two arguments to the function
+ assembly {
+ permissionDomainId := mload(add(_action, 0x24))
+ childSkillIndex := mload(add(_action, 0x44))
+ }
+
+ uint256 permissionSkillId = getDomainSkillId(permissionDomainId);
+ return getChildSkillId(permissionSkillId, childSkillIndex);
+ }
+
+ function executeCall(address target, 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(), target, 0, add(action, 0x20), mload(action), 0, 0)
+ }
+ }
+
+ function getSig(bytes memory action) internal returns (bytes4 sig) {
+ assembly {
+ sig := mload(add(action, 0x20))
+ }
+ }
+
+ function hashExpenditureAction(bytes memory action) internal returns (bytes32 hash) {
+ assembly {
+ // 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
+ // 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))
+ }
+ }
+
+ function createClaimDelayAction(bytes memory action, bool increment)
+ 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
+ // 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;
+ 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) {
+
+ uint256 claimDelay = colony.getExpenditure(expenditureId).globalClaimDelay;
+ claimDelay = increment ? add(claimDelay, 365 days) : sub(claimDelay, 365 days);
+
+ bytes memory mainClaimDelayAction = new bytes(4 + 32 * 11); // 356 bytes
+
+ assembly {
+ mstore(add(mainClaimDelayAction, 0x20), functionSignature)
+ mstore(add(mainClaimDelayAction, 0x24), permissionDomainId)
+ mstore(add(mainClaimDelayAction, 0x44), childSkillIndex)
+ mstore(add(mainClaimDelayAction, 0x64), expenditureId)
+ mstore(add(mainClaimDelayAction, 0x84), 25) // expenditure storage slot
+ mstore(add(mainClaimDelayAction, 0xa4), 0xe0) // mask location
+ mstore(add(mainClaimDelayAction, 0xc4), 0x120) // keys location
+ mstore(add(mainClaimDelayAction, 0xe4), claimDelay)
+ mstore(add(mainClaimDelayAction, 0x104), 1) // mask length
+ mstore(add(mainClaimDelayAction, 0x124), 1) // offset
+ mstore(add(mainClaimDelayAction, 0x144), 1) // keys length
+ mstore(add(mainClaimDelayAction, 0x164), 4) // globalClaimDelay offset
+ }
+ return mainClaimDelayAction;
+
+ // If we are editing an expenditure slot
+ } else {
+
+ uint256 expenditureSlot;
+
+ assembly {
+ expenditureSlot := mload(add(action, 0x184))
+ }
+
+ uint256 claimDelay = colony.getExpenditureSlot(expenditureId, expenditureSlot).claimDelay;
+ claimDelay = increment ? add(claimDelay, 365 days) : sub(claimDelay, 365 days);
+
+ bytes memory slotClaimDelayAction = new bytes(4 + 32 * 13); // 420 bytes
+
+ assembly {
+ mstore(add(slotClaimDelayAction, 0x20), functionSignature)
+ mstore(add(slotClaimDelayAction, 0x24), permissionDomainId)
+ mstore(add(slotClaimDelayAction, 0x44), childSkillIndex)
+ mstore(add(slotClaimDelayAction, 0x64), expenditureId)
+ mstore(add(slotClaimDelayAction, 0x84), 26) // expenditureSlot storage slot
+ mstore(add(slotClaimDelayAction, 0xa4), 0xe0) // mask location
+ mstore(add(slotClaimDelayAction, 0xc4), 0x140) // keys location
+ mstore(add(slotClaimDelayAction, 0xe4), claimDelay)
+ mstore(add(slotClaimDelayAction, 0x104), 2) // mask length
+ mstore(add(slotClaimDelayAction, 0x124), 0) // mapping
+ mstore(add(slotClaimDelayAction, 0x144), 1) // offset
+ mstore(add(slotClaimDelayAction, 0x164), 2) // keys length
+ mstore(add(slotClaimDelayAction, 0x184), expenditureSlot)
+ mstore(add(slotClaimDelayAction, 0x1a4), 1) // claimDelay offset
+ }
+ return slotClaimDelayAction;
+
+ }
+ }
+
+ function getReputationFromProof(
+ uint256 _motionId,
+ address _who,
+ bytes memory _key,
+ bytes memory _value,
+ uint256 _branchMask,
+ bytes32[] memory _siblings
+ )
+ internal view returns (uint256)
+ {
+ bytes32 impliedRoot = getImpliedRootHashKey(_key, _value, _branchMask, _siblings);
+ require(motions[_motionId].rootHash == impliedRoot, "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), "voting-invalid-colony");
+ require(keySkill == motions[_motionId].skillId, "voting-invalid-skill");
+ require(keyUserAddress == _who, "voting-invalid-user");
+
+ return reputationValue;
+ }
+
+}
diff --git a/contracts/extensions/VotingHybrid.sol b/contracts/extensions/VotingHybrid.sol
new file mode 100644
index 0000000000..547d7a8d60
--- /dev/null
+++ b/contracts/extensions/VotingHybrid.sol
@@ -0,0 +1,188 @@
+/*
+ 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.7.3;
+pragma experimental ABIEncoderV2;
+
+import "./../common/ERC20Extended.sol";
+import "./VotingBase.sol";
+
+
+contract VotingHybrid is VotingBase {
+
+ uint256 constant NUM_INFLUENCES = 2;
+
+ /// @notice Returns the identifier of the extension
+ function identifier() public override pure returns (bytes32) {
+ return keccak256("VotingHybrid");
+ }
+
+ /// @notice Return the version number
+ /// @return The version number
+ function version() public pure override returns (uint256) {
+ return 1;
+ }
+
+ // [motionId] => lockId
+ mapping (uint256 => uint256) lockIds;
+
+ // Public
+
+ /// @notice Get the user influence in the motion
+ /// @param _motionId The id of the motion
+ /// @param _user The user in question
+ /// @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 getInfluence(
+ uint256 _motionId,
+ address _user,
+ bytes memory _key,
+ bytes memory _value,
+ uint256 _branchMask,
+ bytes32[] memory _siblings
+ )
+ public
+ view
+ returns (uint256[] memory influence)
+ {
+ influence = new uint256[](NUM_INFLUENCES);
+ influence[0] = getReputationFromProof(_motionId, _user, _key, _value, _branchMask, _siblings);
+ influence[1] = add(
+ tokenLocking.getUserLock(token, _user).balance,
+ add(stakes[_motionId][_user][NAY], stakes[_motionId][_user][YAY])
+ );
+ }
+
+ function postSubmit(uint256 _motionId, address _user) internal override {}
+
+ /// @notice Perform post-reveal bookkeeping
+ /// @param _motionId The id of the motion
+ /// @param _user The user in question
+ function postReveal(uint256 _motionId, address _user) internal override {
+ if (lockIds[_motionId] == 0) {
+ // This is the first reveal that has taken place in this motion.
+ // We lock the token for everyone to avoid double-counting,
+ lockIds[_motionId] = colony.lockToken();
+ }
+
+ colony.unlockTokenForUser(_user, lockIds[_motionId]);
+ }
+
+ /// @notice Perform post-claim bookkeeping
+ /// @param _motionId The id of the motion
+ /// @param _user The user in question
+ function postClaim(uint256 _motionId, address _user) internal override {
+ uint256 lockCount = tokenLocking.getUserLock(token, _user).lockCount;
+
+ // Lock may have already been released during reveal
+ if (lockCount < lockIds[_motionId]) {
+ colony.unlockTokenForUser(_user, lockIds[_motionId]);
+ }
+ }
+
+ /// @notice Create a motion in the root domain
+ /// @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 createMotion(
+ address _altTarget,
+ bytes memory _action,
+ bytes memory _key,
+ bytes memory _value,
+ uint256 _branchMask,
+ bytes32[] memory _siblings
+ )
+ public
+ notDeprecated
+ {
+ createMotionInternal(1, UINT256_MAX, _altTarget, _action, NUM_INFLUENCES);
+ motions[motionCount].maxVotes[0] = getReputationFromProof(motionCount, address(0x0), _key, _value, _branchMask, _siblings);
+ motions[motionCount].maxVotes[1] = ERC20Extended(token).totalSupply();
+ }
+
+ /// @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 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 stakeMotion(
+ uint256 _motionId,
+ uint256 _permissionDomainId,
+ uint256 _childSkillIndex,
+ uint256 _vote,
+ uint256 _amount,
+ bytes memory _key,
+ bytes memory _value,
+ uint256 _branchMask,
+ bytes32[] memory _siblings
+ )
+ public
+ {
+ uint256[] memory influence = getInfluence(_motionId, msg.sender, _key, _value, _branchMask, _siblings);
+ internalStakeMotion(_motionId, _permissionDomainId, _childSkillIndex, _vote, _amount, influence);
+ }
+
+ /// @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 _motionId,
+ bytes32 _voteSecret,
+ bytes memory _key,
+ bytes memory _value,
+ uint256 _branchMask,
+ bytes32[] memory _siblings
+ )
+ public
+ {
+ uint256[] memory influence = getInfluence(_motionId, msg.sender, _key, _value, _branchMask, _siblings);
+ internalSubmitVote(_motionId, _voteSecret, influence);
+ }
+
+ function revealVote(
+ uint256 _motionId,
+ bytes32 _salt,
+ uint256 _vote,
+ bytes memory _key,
+ bytes memory _value,
+ uint256 _branchMask,
+ bytes32[] memory _siblings
+ )
+ public
+ {
+ uint256[] memory influence = getInfluence(_motionId, msg.sender, _key, _value, _branchMask, _siblings);
+ internalRevealVote(_motionId, _salt, _vote, influence);
+ }
+
+ function getLockId(uint256 _motionId) public view returns (uint256) {
+ return lockIds[_motionId];
+ }
+}
diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol
index 6346763ce7..2ee4e0c020 100644
--- a/contracts/extensions/VotingReputation.sol
+++ b/contracts/extensions/VotingReputation.sol
@@ -91,7 +91,9 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs, BasicMetaTrans
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
+
mapping(address => uint256) metatransactionNonces;
+
function getMetatransactionNonce(address userAddress) override public view returns (uint256 nonce){
return metatransactionNonces[userAddress];
}
diff --git a/contracts/extensions/VotingReputation2.sol b/contracts/extensions/VotingReputation2.sol
new file mode 100644
index 0000000000..44e85ab7f0
--- /dev/null
+++ b/contracts/extensions/VotingReputation2.sol
@@ -0,0 +1,295 @@
+/*
+ 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.7.3;
+pragma experimental ABIEncoderV2;
+
+import "./VotingBase.sol";
+
+
+contract VotingReputation2 is VotingBase {
+
+ uint256 constant NUM_INFLUENCES = 1;
+
+ /// @notice Returns the identifier of the extension
+ function identifier() public override pure returns (bytes32) {
+ return keccak256("VotingReputation2");
+ }
+
+ /// @notice Return the version number
+ /// @return The version number
+ function version() public pure override returns (uint256) {
+ return 1;
+ }
+
+ // Public
+
+ /// @notice Create a motion
+ /// @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 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 createMotion(
+ uint256 _domainId,
+ uint256 _childSkillIndex,
+ address _altTarget,
+ bytes memory _action,
+ bytes memory _key,
+ bytes memory _value,
+ uint256 _branchMask,
+ bytes32[] memory _siblings
+ )
+ public
+ notDeprecated
+ {
+ createMotionInternal(_domainId, _childSkillIndex, _altTarget, _action, NUM_INFLUENCES);
+ motions[motionCount].maxVotes[0] = getReputationFromProof(motionCount, address(0x0), _key, _value, _branchMask, _siblings);
+ }
+
+ /// @notice Deprecated
+ /// @notice Create a motion in the root domain
+ /// @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 _altTarget,
+ bytes memory _action,
+ bytes memory _key,
+ bytes memory _value,
+ uint256 _branchMask,
+ bytes32[] memory _siblings
+ )
+ public
+ notDeprecated
+ {
+ createMotionInternal(1, UINT256_MAX, _altTarget, _action, NUM_INFLUENCES);
+ Motion storage motion = motions[motionCount];
+
+ motion.maxVotes[0] = getReputationFromProof(motionCount, address(0x0), _key, _value, _branchMask, _siblings);
+ }
+
+ /// @notice Deprecated
+ /// @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 _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 createDomainMotion(
+ uint256 _domainId,
+ uint256 _childSkillIndex,
+ bytes memory _action,
+ bytes memory _key,
+ bytes memory _value,
+ uint256 _branchMask,
+ bytes32[] memory _siblings
+ )
+ public
+ notDeprecated
+ {
+ createMotionInternal(_domainId, _childSkillIndex, address(0x0), _action, NUM_INFLUENCES);
+ Motion storage motion = motions[motionCount];
+
+ motion.maxVotes[0] = getReputationFromProof(motionCount, address(0x0), _key, _value, _branchMask, _siblings);
+ }
+
+ /// @notice Get the user influence in the motion
+ /// @param _motionId The id of the motion
+ /// @param _user The user in question
+ /// @param _key Reputation tree key for the user
+ /// @param _value Reputation tree value for the user
+ /// @param _branchMask The branchmask of the proof
+ /// @param _siblings The siblings of the proof
+ function getInfluence(
+ uint256 _motionId,
+ address _user,
+ bytes memory _key,
+ bytes memory _value,
+ uint256 _branchMask,
+ bytes32[] memory _siblings
+ )
+ public
+ view
+ returns (uint256[] memory influence)
+ {
+ influence = new uint256[](NUM_INFLUENCES);
+ influence[0] = getReputationFromProof(_motionId, msg.sender, _key, _value, _branchMask, _siblings);
+ }
+
+ function postSubmit(uint256 _motionId, address _user) internal override {
+ Motion storage motion = motions[_motionId];
+
+ bool submissionsComplete = true;
+
+ for (uint256 i; i < motion.votes.length; i++) {
+ submissionsComplete = submissionsComplete &&
+ motion.totalVotes[i] >= wmul(motion.maxVotes[i], maxVoteFraction);
+ }
+
+ if (submissionsComplete) {
+ motion.events[SUBMIT_END] = uint64(block.timestamp);
+ motion.events[REVEAL_END] = uint64(block.timestamp + revealPeriod);
+
+ emit MotionEventSet(_motionId, SUBMIT_END);
+ }
+ }
+
+ function postReveal(uint256 _motionId, address _user) internal override {
+ Motion storage motion = motions[_motionId];
+
+ // See if reputation revealed matches reputation submitted
+ bool fullyRevealed = true;
+
+ for (uint256 j; j < motion.totalVotes.length && fullyRevealed; j++) {
+ fullyRevealed = fullyRevealed &&
+ add(motion.votes[j][NAY], motion.votes[j][YAY]) == motion.totalVotes[j];
+ }
+
+ if (fullyRevealed) {
+ motion.events[REVEAL_END] = uint64(block.timestamp);
+
+ emit MotionEventSet(_motionId, REVEAL_END);
+ }
+ }
+
+ function postClaim(uint256 _motionId, address _user) internal override {}
+
+ /// @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 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 stakeMotion(
+ uint256 _motionId,
+ uint256 _permissionDomainId,
+ uint256 _childSkillIndex,
+ uint256 _vote,
+ uint256 _amount,
+ bytes memory _key,
+ bytes memory _value,
+ uint256 _branchMask,
+ bytes32[] memory _siblings
+ )
+ public
+ {
+ uint256[] memory influence = getInfluence(_motionId, msg.sender, _key, _value, _branchMask, _siblings);
+ internalStakeMotion(_motionId, _permissionDomainId, _childSkillIndex, _vote, _amount, influence);
+ }
+
+ /// @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 _motionId,
+ bytes32 _voteSecret,
+ bytes memory _key,
+ bytes memory _value,
+ uint256 _branchMask,
+ bytes32[] memory _siblings
+ )
+ public
+ {
+ uint256[] memory influence = getInfluence(_motionId, msg.sender, _key, _value, _branchMask, _siblings);
+ internalSubmitVote(_motionId, _voteSecret, influence);
+ }
+
+ function revealVote(
+ uint256 _motionId,
+ bytes32 _salt,
+ uint256 _vote,
+ bytes memory _key,
+ bytes memory _value,
+ uint256 _branchMask,
+ bytes32[] memory _siblings
+ )
+ public
+ {
+ uint256[] memory influence = getInfluence(_motionId, msg.sender, _key, _value, _branchMask, _siblings);
+ internalRevealVote(_motionId, _salt, _vote, influence);
+ }
+
+ /// @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 escalateMotion(
+ uint256 _motionId,
+ uint256 _newDomainId,
+ uint256 _childSkillIndex,
+ bytes memory _key,
+ bytes memory _value,
+ uint256 _branchMask,
+ bytes32[] memory _siblings
+ )
+ public
+ {
+ Motion storage motion = motions[_motionId];
+
+ require(getMotionState(_motionId) == MotionState.Closed, "voting-not-closed");
+
+ uint256 newDomainSkillId = getDomainSkillId(_newDomainId);
+ uint256 childSkillId = getChildSkillId(newDomainSkillId, _childSkillIndex);
+ require(childSkillId == motion.skillId, "voting-invalid-domain-proof");
+
+ uint256 domainId = motion.domainId;
+ motion.domainId = _newDomainId;
+ motion.skillId = newDomainSkillId;
+ motion.maxVotes[0] = getReputationFromProof(_motionId, address(0x0), _key, _value, _branchMask, _siblings);
+
+ uint256 loser = (motion.votes[0][NAY] < motion.votes[0][YAY]) ? NAY : YAY;
+ motion.stakes[loser] = sub(motion.stakes[loser], motion.paidVoterComp);
+ 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(block.timestamp + stakePeriod) : uint64(block.timestamp);
+
+ motion.events[SUBMIT_END] = motion.events[STAKE_END] + uint64(submitPeriod);
+ motion.events[REVEAL_END] = motion.events[SUBMIT_END] + uint64(revealPeriod);
+
+ motion.escalated = true;
+
+ emit MotionEscalated(_motionId, msg.sender, domainId, _newDomainId);
+
+ if (motion.events[STAKE_END] <= uint64(block.timestamp)) {
+ emit MotionEventSet(_motionId, STAKE_END);
+ }
+ }
+}
diff --git a/contracts/extensions/VotingToken.sol b/contracts/extensions/VotingToken.sol
new file mode 100644
index 0000000000..96f4aea384
--- /dev/null
+++ b/contracts/extensions/VotingToken.sol
@@ -0,0 +1,132 @@
+/*
+ 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.7.3;
+pragma experimental ABIEncoderV2;
+
+import "./../common/ERC20Extended.sol";
+import "./VotingBase.sol";
+
+
+contract VotingToken is VotingBase {
+
+ uint256 constant NUM_INFLUENCES = 1;
+
+ /// @notice Returns the identifier of the extension
+ function identifier() public override pure returns (bytes32) {
+ return keccak256("VotingToken");
+ }
+
+ /// @notice Return the version number
+ /// @return The version number
+ function version() public pure override returns (uint256) {
+ return 1;
+ }
+
+ // [motionId] => lockId
+ mapping (uint256 => uint256) lockIds;
+
+ // Public
+
+ /// @notice Get the user influence in the motion
+ /// @param _motionId The id of the motion
+ /// @param _user The user in question
+ function getInfluence(uint256 _motionId, address _user)
+ public
+ view
+ returns (uint256[] memory influence)
+ {
+ influence = new uint256[](NUM_INFLUENCES);
+ influence[0] = add(
+ tokenLocking.getUserLock(token, _user).balance,
+ add(stakes[_motionId][_user][NAY], stakes[_motionId][_user][YAY])
+ );
+ }
+
+ function postSubmit(uint256 _motionId, address _user) internal override {}
+
+ function postReveal(uint256 _motionId, address _user) internal override {
+ if (lockIds[_motionId] == 0) {
+ // This is the first reveal that has taken place in this motion.
+ // We lock the token for everyone to avoid double-counting,
+ lockIds[_motionId] = colony.lockToken();
+ }
+
+ colony.unlockTokenForUser(_user, lockIds[_motionId]);
+ }
+
+ function postClaim(uint256 _motionId, address _user) internal override {
+ uint256 lockCount = tokenLocking.getUserLock(token, _user).lockCount;
+
+ // Lock may have already been released during reveal
+ if (lockCount < lockIds[_motionId]) {
+ colony.unlockTokenForUser(_user, lockIds[_motionId]);
+ }
+ }
+
+ /// @notice Create a motion in the root domain
+ /// @param _altTarget The contract to which we send the action (0x0 for the colony)
+ /// @param _action A bytes array encoding a function call
+ function createMotion(address _altTarget, bytes memory _action)
+ public
+ notDeprecated
+ {
+ createMotionInternal(1, UINT256_MAX, _altTarget, _action, NUM_INFLUENCES);
+ motions[motionCount].maxVotes[0] = ERC20Extended(token).totalSupply();
+ }
+
+ /// @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 motion is occurring
+ /// @param _vote The side being supported (0 = NAY, 1 = YAY)
+ /// @param _amount The amount of tokens being staked
+ function stakeMotion(
+ uint256 _motionId,
+ uint256 _permissionDomainId,
+ uint256 _childSkillIndex,
+ uint256 _vote,
+ uint256 _amount
+ )
+ public
+ {
+ uint256[] memory influence = getInfluence(_motionId, msg.sender);
+ internalStakeMotion(_motionId, _permissionDomainId, _childSkillIndex, _vote, _amount, influence);
+ }
+
+ /// @notice Submit a vote secret for a motion
+ /// @param _motionId The id of the motion
+ /// @param _voteSecret The hashed vote secret
+ function submitVote(uint256 _motionId, bytes32 _voteSecret)
+ public
+ {
+ uint256[] memory influence = getInfluence(_motionId, msg.sender);
+ internalSubmitVote(_motionId, _voteSecret, influence);
+ }
+
+ function revealVote(uint256 _motionId, bytes32 _salt, uint256 _vote)
+ public
+ {
+ uint256[] memory influence = getInfluence(_motionId, msg.sender);
+ internalRevealVote(_motionId, _salt, _vote, influence);
+ }
+
+ function getLockId(uint256 _motionId) public view returns (uint256) {
+ return lockIds[_motionId];
+ }
+
+}
diff --git a/contracts/testHelpers/TestExtensions.sol b/contracts/testHelpers/TestExtensions.sol
index c2992874df..7caaff7eb5 100644
--- a/contracts/testHelpers/TestExtensions.sol
+++ b/contracts/testHelpers/TestExtensions.sol
@@ -66,7 +66,7 @@ contract TestExtension3 is TestExtension {
}
contract TestVotingToken is TestExtension {
- function identifier() public pure override returns (bytes32) { return keccak256("VotingToken"); }
+ function identifier() public pure override returns (bytes32) { return keccak256("TestVotingToken"); }
function version() public pure override returns (uint256) { return 1; }
function lockToken() public returns (uint256) {
return colony.lockToken();
diff --git a/migrations/9_setup_extensions.js b/migrations/9_setup_extensions.js
index 038e73ad22..c388bbd71c 100644
--- a/migrations/9_setup_extensions.js
+++ b/migrations/9_setup_extensions.js
@@ -9,6 +9,9 @@ const EvaluatedExpenditure = artifacts.require("./EvaluatedExpenditure");
const FundingQueue = artifacts.require("./FundingQueue");
const OneTxPayment = artifacts.require("./OneTxPayment");
const VotingReputation = artifacts.require("./VotingReputation");
+const VotingReputation2 = artifacts.require("./VotingReputation2");
+const VotingToken = artifacts.require("./VotingToken");
+const VotingHybrid = artifacts.require("./VotingHybrid");
const TokenSupplier = artifacts.require("./TokenSupplier");
const Whitelist = artifacts.require("./Whitelist");
@@ -40,6 +43,9 @@ module.exports = async function (deployer, network, accounts) {
await addExtension(colonyNetwork, "FundingQueue", FundingQueue);
await addExtension(colonyNetwork, "OneTxPayment", OneTxPayment);
await addExtension(colonyNetwork, "VotingReputation", VotingReputation);
+ await addExtension(colonyNetwork, "VotingReputation2", VotingReputation2);
+ await addExtension(colonyNetwork, "VotingToken", VotingToken);
+ await addExtension(colonyNetwork, "VotingHybrid", VotingHybrid);
await addExtension(colonyNetwork, "TokenSupplier", TokenSupplier);
await addExtension(colonyNetwork, "Whitelist", Whitelist);
};
diff --git a/package.json b/package.json
index c611028401..2650fdd9b8 100644
--- a/package.json
+++ b/package.json
@@ -18,7 +18,7 @@
"generate:test:contracts": "bash ./scripts/generate-test-contracts.sh",
"clean:test:contracts": "rimraf ./contracts/*Updated*.*",
"clean:contracts": "rimraf ./build/contracts/*",
- "clean:ganache": "rimraf ./ganache-chain-db/",
+ "clean:ganache": "rimraf ./ganache-chain-db/; rm ganache-accounts.json",
"provision:token:contracts": "truffle compile && truffle compile --contracts_directory 'lib/dappsys/[!note][!stop][!proxy][!thing][!token]*.sol' && bash ./scripts/provision-token-contracts.sh",
"start:blockchain:client": "bash ./scripts/start-blockchain-client.sh",
"stop:blockchain:client": "bash ./scripts/stop-blockchain-client.sh",
diff --git a/scripts/check-recovery.js b/scripts/check-recovery.js
index fc0b173570..0ff08cdce4 100644
--- a/scripts/check-recovery.js
+++ b/scripts/check-recovery.js
@@ -49,7 +49,11 @@ walkSync("./contracts/").forEach((contractName) => {
"contracts/extensions/FundingQueue.sol",
"contracts/extensions/OneTxPayment.sol",
"contracts/extensions/TokenSupplier.sol",
+ "contracts/extensions/VotingBase.sol",
+ "contracts/extensions/VotingHybrid.sol",
"contracts/extensions/VotingReputation.sol",
+ "contracts/extensions/VotingReputation2.sol",
+ "contracts/extensions/VotingToken.sol",
"contracts/extensions/Whitelist.sol",
"contracts/gnosis/MultiSigWallet.sol",
"contracts/patriciaTree/Bits.sol",
diff --git a/scripts/check-storage.js b/scripts/check-storage.js
index f32c85c1c7..c2484f5741 100644
--- a/scripts/check-storage.js
+++ b/scripts/check-storage.js
@@ -32,7 +32,11 @@ walkSync("./contracts/").forEach((contractName) => {
"contracts/extensions/ColonyExtensionMeta.sol",
"contracts/extensions/OneTxPayment.sol",
"contracts/extensions/TokenSupplier.sol",
+ "contracts/extensions/VotingBase.sol",
+ "contracts/extensions/VotingHybrid.sol",
"contracts/extensions/VotingReputation.sol",
+ "contracts/extensions/VotingReputation2.sol",
+ "contracts/extensions/VotingToken.sol",
"contracts/extensions/Whitelist.sol",
"contracts/gnosis/MultiSigWallet.sol", // Not directly used by any colony contracts
"contracts/patriciaTree/PatriciaTreeBase.sol", // Only used by mining clients
diff --git a/test-smoke/colony-storage-consistent.js b/test-smoke/colony-storage-consistent.js
index 8a83d7a29e..b79ad23395 100644
--- a/test-smoke/colony-storage-consistent.js
+++ b/test-smoke/colony-storage-consistent.js
@@ -154,8 +154,8 @@ 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("cbe7c27231f4c94f1fdff92a685599c6ada69af16a0f32ab3a72e85334a4a2ca");
- expect(colonyAccount.stateRoot.toString("hex")).to.equal("b198bd282eac14f7dc4d71817c4184462c6db7ae11a8f7662edecd4afbba6234");
+ expect(colonyNetworkAccount.stateRoot.toString("hex")).to.equal("171572a74d1573190a3764941638089a7ff51611172986ba5c7e7115eb7d5d08");
+ expect(colonyAccount.stateRoot.toString("hex")).to.equal("b535ed20810ef45ca468bb02b0e277e8a461a58844248ffd5ccee877def42170");
expect(metaColonyAccount.stateRoot.toString("hex")).to.equal("c91229b9b01734f45e65feea0561ed90bee1365c953ceb187e87e80e9a96ef86");
expect(miningCycleAccount.stateRoot.toString("hex")).to.equal("f1ae4c855f083837446bc31b3355ca0291daa363dd9afb655de51829a46fc635");
expect(tokenLockingAccount.stateRoot.toString("hex")).to.equal("860aa632a7a9a21119b0e27c30d0c3f4da5916eb5263c7870d4dda3eb7162e32");
diff --git a/test/contracts-network/colony-network-extensions.js b/test/contracts-network/colony-network-extensions.js
index 45419d6c35..aeb8eda09c 100644
--- a/test/contracts-network/colony-network-extensions.js
+++ b/test/contracts-network/colony-network-extensions.js
@@ -49,7 +49,7 @@ contract("Colony Network Extensions", (accounts) => {
const USER = accounts[2];
const TEST_EXTENSION = soliditySha3("TestExtension");
- const TEST_VOTING_TOKEN = soliditySha3("VotingToken");
+ const TEST_VOTING_TOKEN = soliditySha3("TestVotingToken");
before(async () => {
testExtension0Resolver = await Resolver.new();
diff --git a/test/contracts-network/token-locking.js b/test/contracts-network/token-locking.js
index 2ada308eb8..3ffc6d2d83 100644
--- a/test/contracts-network/token-locking.js
+++ b/test/contracts-network/token-locking.js
@@ -337,7 +337,7 @@ contract("Token Locking", (addresses) => {
const testVotingTokenResolver = await Resolver.new();
const testVotingToken = await TestVotingToken.new();
await setupEtherRouter("TestVotingToken", { TestVotingToken: testVotingToken.address }, testVotingTokenResolver);
- TEST_VOTING_TOKEN = soliditySha3("VotingToken");
+ TEST_VOTING_TOKEN = soliditySha3("TestVotingToken");
const metaColonyAddress = await colonyNetwork.getMetaColony();
const metaColony = await IMetaColony.at(metaColonyAddress);
diff --git a/test/extensions/voting-hybrid.js b/test/extensions/voting-hybrid.js
new file mode 100644
index 0000000000..046ae9b1f4
--- /dev/null
+++ b/test/extensions/voting-hybrid.js
@@ -0,0 +1,1371 @@
+/* globals artifacts */
+
+import BN from "bn.js";
+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, SUBMITTER_ONLY_WINDOW } from "../../helpers/constants";
+
+import {
+ checkErrorRevert,
+ web3GetCode,
+ makeReputationKey,
+ makeReputationValue,
+ getActiveRepCycle,
+ forwardTime,
+ encodeTxData,
+ bn2bytes32,
+ expectEvent,
+} from "../../helpers/test-helper";
+
+import {
+ setupColonyNetwork,
+ setupMetaColonyWithLockedCLNYToken,
+ setupRandomColony,
+ giveUserCLNYTokensAndStake,
+} from "../../helpers/test-data-generator";
+
+import { setupEtherRouter } from "../../helpers/upgradable-contracts";
+
+import PatriciaTree from "../../packages/reputation-miner/patricia";
+
+const { expect } = chai;
+chai.use(bnChai(web3.utils.BN));
+
+const IReputationMiningCycle = artifacts.require("IReputationMiningCycle");
+const TokenLocking = artifacts.require("TokenLocking");
+const VotingHybrid = artifacts.require("VotingHybrid");
+const Resolver = artifacts.require("Resolver");
+
+const VOTING_HYBRID = soliditySha3("VotingHybrid");
+
+contract("Voting Hybrid", (accounts) => {
+ let colony;
+ let token;
+ let domain1;
+ let metaColony;
+ let colonyNetwork;
+ let tokenLocking;
+
+ let voting;
+
+ let reputationTree;
+
+ let requiredStake;
+
+ let domain1Key;
+ let domain1Value;
+ let domain1Mask;
+ let domain1Siblings;
+
+ let user0Key;
+ let user0Value;
+ let user0Mask;
+ let user0Siblings;
+
+ let user1Key;
+ let user1Value;
+ let user1Mask;
+ let user1Siblings;
+
+ 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 STAKE_PERIOD = SECONDS_PER_DAY * 3;
+ const SUBMIT_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];
+ const USER2 = accounts[2];
+ const MINER = accounts[5];
+
+ const SALT = soliditySha3({ type: "string", value: shortid.generate() });
+ const FAKE = soliditySha3({ type: "string", value: shortid.generate() });
+
+ const NAY = 0;
+ const YAY = 1;
+
+ // 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 WAD32 = bn2bytes32(WAD);
+ const HALF = WAD.divn(2);
+ const YEAR = SECONDS_PER_DAY * 365;
+
+ 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);
+
+ const votingImplementation = await VotingHybrid.new();
+ const resolver = await Resolver.new();
+ await setupEtherRouter("VotingHybrid", { VotingHybrid: votingImplementation.address }, resolver);
+ await metaColony.addExtensionToNetwork(VOTING_HYBRID, resolver.address);
+ });
+
+ beforeEach(async () => {
+ ({ colony, token } = await setupRandomColony(colonyNetwork));
+ domain1 = await colony.getDomain(1);
+
+ await colony.installExtension(VOTING_HYBRID, 1);
+ const votingAddress = await colonyNetwork.getExtensionInstallation(VOTING_HYBRID, colony.address);
+ voting = await VotingHybrid.at(votingAddress);
+
+ await voting.initialise(
+ TOTAL_STAKE_FRACTION,
+ VOTER_REWARD_FRACTION,
+ USER_MIN_STAKE_FRACTION,
+ MAX_VOTE_FRACTION,
+ STAKE_PERIOD,
+ SUBMIT_PERIOD,
+ REVEAL_PERIOD,
+ ESCALATION_PERIOD
+ );
+
+ await colony.setRootRole(voting.address, true);
+ await colony.setArbitrationRole(1, UINT256_MAX, voting.address, 1, true);
+ await colony.setAdministrationRole(1, UINT256_MAX, voting.address, 1, true);
+
+ const user0Influence = WAD;
+ const user1Influence = WAD.muln(2);
+ const user2Influence = WAD.divn(10000);
+ const totalInfluence = user0Influence.add(user1Influence).add(user2Influence);
+
+ // Setup reputation values
+
+ reputationTree = new PatriciaTree();
+ await reputationTree.insert(
+ makeReputationKey(colony.address, domain1.skillId), // Colony total
+ makeReputationValue(totalInfluence, 1)
+ );
+ await reputationTree.insert(
+ makeReputationKey(colony.address, domain1.skillId, USER0), // User0
+ makeReputationValue(user0Influence, 2)
+ );
+ await reputationTree.insert(
+ makeReputationKey(colony.address, domain1.skillId, USER1), // User1
+ makeReputationValue(user1Influence, 3)
+ );
+ await reputationTree.insert(
+ makeReputationKey(colony.address, domain1.skillId, USER2), // User2
+ makeReputationValue(user2Influence, 4)
+ );
+
+ domain1Key = makeReputationKey(colony.address, domain1.skillId);
+ domain1Value = makeReputationValue(totalInfluence, 1);
+ [domain1Mask, domain1Siblings] = await reputationTree.getProof(domain1Key);
+
+ user0Key = makeReputationKey(colony.address, domain1.skillId, USER0);
+ user0Value = makeReputationValue(user0Influence, 2);
+ [user0Mask, user0Siblings] = await reputationTree.getProof(user0Key);
+
+ user1Key = makeReputationKey(colony.address, domain1.skillId, USER1);
+ user1Value = makeReputationValue(user1Influence, 3);
+ [user1Mask, user1Siblings] = await reputationTree.getProof(user1Key);
+
+ 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 forwardTime(SUBMITTER_ONLY_WINDOW + 1, this);
+ await repCycle.confirmNewHash(0);
+
+ // Setup token values
+
+ await token.mint(USER0, user0Influence);
+ await token.mint(USER1, user1Influence);
+ await token.mint(USER2, user2Influence);
+ await token.approve(tokenLocking.address, user0Influence, { from: USER0 });
+ await token.approve(tokenLocking.address, user1Influence, { from: USER1 });
+ await token.approve(tokenLocking.address, user2Influence, { from: USER2 });
+ await tokenLocking.methods["deposit(address,uint256,bool)"](token.address, user0Influence, true, { from: USER0 });
+ await tokenLocking.methods["deposit(address,uint256,bool)"](token.address, user1Influence, true, { from: USER1 });
+ await tokenLocking.methods["deposit(address,uint256,bool)"](token.address, user2Influence, true, { from: USER2 });
+ await colony.approveStake(voting.address, 1, user0Influence, { from: USER0 });
+ await colony.approveStake(voting.address, 1, user1Influence, { from: USER1 });
+ await colony.approveStake(voting.address, 1, user2Influence, { from: USER2 });
+
+ // Add both reputation and token influence
+ requiredStake = totalInfluence.muln(2).divn(1000);
+ });
+
+ 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("managing the extension", async () => {
+ it("can install the extension manually", async () => {
+ voting = await VotingHybrid.new();
+ await voting.install(colony.address);
+
+ await checkErrorRevert(voting.install(colony.address), "extension-already-installed");
+
+ const identifier = await voting.identifier();
+ const version = await voting.version();
+ expect(identifier).to.equal(VOTING_HYBRID);
+ expect(version).to.eq.BN(1);
+
+ await voting.finishUpgrade();
+ await voting.deprecate(true);
+ await voting.uninstall();
+
+ const code = await web3GetCode(voting.address);
+ expect(code).to.equal("0x");
+ });
+
+ it("can install the extension with the extension manager", async () => {
+ ({ colony } = await setupRandomColony(colonyNetwork));
+ await colony.installExtension(VOTING_HYBRID, 1, { from: USER0 });
+
+ await checkErrorRevert(colony.installExtension(VOTING_HYBRID, 1, { from: USER0 }), "colony-network-extension-already-installed");
+ await checkErrorRevert(colony.uninstallExtension(VOTING_HYBRID, { from: USER1 }), "ds-auth-unauthorized");
+
+ await colony.uninstallExtension(VOTING_HYBRID, { from: USER0 });
+ });
+
+ it("can deprecate the extension if root", async () => {
+ let deprecated = await voting.getDeprecated();
+ expect(deprecated).to.equal(false);
+
+ await checkErrorRevert(colony.deprecateExtension(VOTING_HYBRID, true, { from: USER2 }), "ds-auth-unauthorized");
+ await colony.deprecateExtension(VOTING_HYBRID, true);
+
+ // Cant make new motions!
+ const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]);
+ await checkErrorRevert(
+ voting.createMotion(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings),
+ "colony-extension-deprecated"
+ );
+
+ deprecated = await voting.getDeprecated();
+ expect(deprecated).to.equal(true);
+ });
+
+ it("cannot make a motion before initialised", async () => {
+ voting = await VotingHybrid.new();
+ await voting.install(colony.address);
+
+ const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]);
+ await checkErrorRevert(voting.createMotion(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings), "voting-not-active");
+ });
+
+ it("cannot initialise twice or more if not root", async () => {
+ await checkErrorRevert(voting.initialise(HALF, HALF, WAD, WAD, YEAR, YEAR, YEAR, YEAR), "voting-already-initialised");
+ await checkErrorRevert(voting.initialise(HALF, HALF, WAD, WAD, YEAR, YEAR, YEAR, YEAR, { from: USER2 }), "voting-not-root");
+ });
+
+ it("cannot initialise with invalid values", async () => {
+ voting = await VotingHybrid.new();
+ await voting.install(colony.address);
+
+ await checkErrorRevert(voting.initialise(HALF.addn(1), HALF, WAD, WAD, YEAR, YEAR, YEAR, YEAR), "voting-invalid-value");
+ await checkErrorRevert(voting.initialise(HALF, HALF.addn(1), WAD, WAD, YEAR, YEAR, YEAR, YEAR), "voting-invalid-value");
+ await checkErrorRevert(voting.initialise(HALF, HALF, WAD.addn(1), WAD, YEAR, YEAR, YEAR, YEAR), "voting-invalid-value");
+ await checkErrorRevert(voting.initialise(HALF, HALF, WAD, WAD.addn(1), YEAR, YEAR, YEAR, YEAR), "voting-invalid-value");
+ await checkErrorRevert(voting.initialise(HALF, HALF, WAD, WAD, YEAR + 1, YEAR, YEAR, YEAR), "voting-invalid-value");
+ await checkErrorRevert(voting.initialise(HALF, HALF, WAD, WAD, YEAR, YEAR + 1, YEAR, YEAR), "voting-invalid-value");
+ await checkErrorRevert(voting.initialise(HALF, HALF, WAD, WAD, YEAR, YEAR, YEAR + 1, YEAR), "voting-invalid-value");
+ await checkErrorRevert(voting.initialise(HALF, HALF, WAD, WAD, YEAR, YEAR, YEAR, YEAR + 1), "voting-invalid-value");
+ });
+
+ it("can initialised with valid values and emit expected event", async () => {
+ voting = await VotingHybrid.new();
+ await voting.install(colony.address);
+
+ await expectEvent(voting.initialise(HALF, HALF, WAD, WAD, YEAR, YEAR, YEAR, YEAR), "ExtensionInitialised", []);
+ });
+
+ it("can query for initialisation 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);
+ });
+ });
+
+ 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.createMotion(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings);
+
+ const motionId = await voting.getMotionCount();
+ const motion = await voting.getMotion(motionId);
+ expect(motion.skillId).to.eq.BN(domain1.skillId);
+ });
+
+ it("does not lock the token when a motion is created", async () => {
+ const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]);
+ await voting.createMotion(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings);
+ const motionId = await voting.getMotionCount();
+
+ const lockId = await voting.getLockId(motionId);
+ expect(lockId).to.be.zero;
+ });
+
+ it("can create a motion with an alternative target", async () => {
+ const action = await encodeTxData(colony, "makeTask", [1, 0, FAKE, 2, 0, 0]);
+ await voting.createMotion(voting.address, action, domain1Key, domain1Value, domain1Mask, domain1Siblings);
+ });
+ });
+
+ describe("staking on motions", async () => {
+ let motionId;
+
+ beforeEach(async () => {
+ const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]);
+ await voting.createMotion(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings);
+ motionId = await voting.getMotionCount();
+ });
+
+ it("can stake on a motion", async () => {
+ const requiredStakeOnChain = await voting.getRequiredStake(motionId);
+ expect(requiredStake).to.eq.BN(requiredStakeOnChain);
+
+ const half = requiredStake.divn(2);
+
+ 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 motion = await voting.getMotion(motionId);
+ expect(motion.stakes[0]).to.be.zero;
+ expect(motion.stakes[1]).to.eq.BN(requiredStake);
+
+ 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 motion states correctly", async () => {
+ let motionState = await voting.getMotionState(motionId);
+ expect(motionState).to.eq.BN(STAKING);
+
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+ motionState = await voting.getMotionState(motionId);
+ expect(motionState).to.eq.BN(STAKING);
+
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, requiredStake, 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 () => {
+ 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.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, requiredStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 });
+
+ const lock = await tokenLocking.getUserLock(token.address, voting.address);
+ expect(lock.balance).to.eq.BN(requiredStake.muln(2));
+ });
+
+ it("cannot stake 0", async () => {
+ await checkErrorRevert(
+ voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, 0, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }),
+ "voting-bad-amount"
+ );
+ });
+
+ it("cannot stake a nonexistent side", async () => {
+ await checkErrorRevert(
+ voting.stakeMotion(motionId, 1, UINT256_MAX, 2, requiredStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }),
+ "voting-bad-vote"
+ );
+ });
+
+ it("cannot stake less than the minStake, unless there is less than minStake to go", async () => {
+ const minStake = requiredStake.divn(10);
+
+ await checkErrorRevert(
+ voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, minStake.subn(1), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }),
+ "voting-insufficient-stake"
+ );
+
+ 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 = requiredStake.sub(minStake.muln(2)).addn(1);
+ 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 () => {
+ await colony.makeExpenditure(1, UINT256_MAX, 1);
+ const expenditureId = await colony.getExpenditureCount();
+ await colony.finalizeExpenditure(expenditureId);
+
+ // Set finalizedTimestamp to WAD
+ const action = await encodeTxData(colony, "setExpenditureState", [1, UINT256_MAX, expenditureId, 25, [true], [bn2bytes32(new BN(3))], WAD32]);
+
+ await voting.createMotion(ethers.constants.AddressZero, action, domain1Key, domain1Value, domain1Mask, domain1Siblings);
+ motionId = await voting.getMotionCount();
+
+ let expenditure;
+ expenditure = await colony.getExpenditure(expenditureId);
+ expect(expenditure.globalClaimDelay).to.be.zero;
+
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+
+ expenditure = await colony.getExpenditure(expenditureId);
+ expect(expenditure.globalClaimDelay).to.eq.BN(SECONDS_PER_DAY * 365);
+
+ await checkErrorRevert(colony.claimExpenditurePayout(expenditureId, 0, token.address), "colony-expenditure-cannot-claim");
+ });
+
+ 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();
+ 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.createMotion(otherColony.address, action, domain1Key, domain1Value, domain1Mask, domain1Siblings);
+ motionId = await voting.getMotionCount();
+
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+
+ 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();
+ await colony.finalizeExpenditure(expenditureId);
+
+ // 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))],
+ WAD32,
+ ]);
+
+ await voting.createMotion(ethers.constants.AddressZero, action, domain1Key, domain1Value, domain1Mask, domain1Siblings);
+ motionId = await voting.getMotionCount();
+
+ let expenditureSlot;
+ expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0);
+ expect(expenditureSlot.claimDelay).to.be.zero;
+
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+
+ expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0);
+ expect(expenditureSlot.claimDelay).to.eq.BN(SECONDS_PER_DAY * 365);
+
+ 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", [
+ 1,
+ UINT256_MAX,
+ expenditureId,
+ 27,
+ [false, false],
+ ["0x0", bn2bytes32(new BN(token.address.slice(2), 16))],
+ WAD32,
+ ]);
+
+ await voting.createMotion(ethers.constants.AddressZero, action, domain1Key, domain1Value, domain1Mask, domain1Siblings);
+ motionId = await voting.getMotionCount();
+
+ let expenditureSlot;
+ expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0);
+ expect(expenditureSlot.claimDelay).to.be.zero;
+
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+
+ expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0);
+ expect(expenditureSlot.claimDelay).to.eq.BN(SECONDS_PER_DAY * 365);
+
+ 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.createMotion(ethers.constants.AddressZero, action, domain1Key, domain1Value, domain1Mask, domain1Siblings);
+ motionId = await voting.getMotionCount();
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, 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.createMotion(ethers.constants.AddressZero, action, domain1Key, domain1Value, domain1Mask, domain1Siblings);
+ motionId = await voting.getMotionCount();
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+
+ // Motion 3
+ // 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.createMotion(ethers.constants.AddressZero, action, domain1Key, domain1Value, domain1Mask, domain1Siblings);
+ motionId = await voting.getMotionCount();
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+
+ const expenditure = await colony.getExpenditure(expenditureId);
+ expect(expenditure.globalClaimDelay).to.eq.BN(SECONDS_PER_DAY * 365);
+
+ const expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0);
+ expect(expenditureSlot.claimDelay).to.eq.BN(SECONDS_PER_DAY * 365 * 2);
+
+ 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 () => {
+ // Create a poorly-formed action (no keys)
+ const action = await encodeTxData(colony, "setExpenditureState", [1, UINT256_MAX, 1, 0, [], [], ethers.constants.HashZero]);
+
+ await voting.createMotion(ethers.constants.AddressZero, action, domain1Key, domain1Value, domain1Mask, domain1Siblings);
+ motionId = await voting.getMotionCount();
+
+ await checkErrorRevert(
+ voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }),
+ "voting-lock-failed"
+ );
+ });
+
+ 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();
+
+ // 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))],
+ WAD32,
+ ]);
+
+ // 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))],
+ WAD32,
+ ]);
+
+ await voting.createMotion(ethers.constants.AddressZero, action1, domain1Key, domain1Value, domain1Mask, domain1Siblings);
+ const motionId1 = await voting.getMotionCount();
+
+ await voting.createMotion(ethers.constants.AddressZero, action2, domain1Key, domain1Value, domain1Mask, domain1Siblings);
+ const motionId2 = await voting.getMotionCount();
+
+ let expenditureSlot;
+ expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0);
+ expect(expenditureSlot.claimDelay).to.be.zero;
+
+ await voting.stakeMotion(motionId1, 1, UINT256_MAX, YAY, requiredStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+ await voting.stakeMotion(motionId2, 1, UINT256_MAX, YAY, requiredStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+
+ expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0);
+ expect(expenditureSlot.claimDelay).to.eq.BN(SECONDS_PER_DAY * 365 * 2);
+
+ await forwardTime(STAKE_PERIOD, this);
+ await voting.finalizeMotion(motionId1);
+
+ expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0);
+ expect(expenditureSlot.claimDelay).to.eq.BN(SECONDS_PER_DAY * 365);
+
+ await voting.finalizeMotion(motionId2);
+
+ expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0);
+ expect(expenditureSlot.claimDelay).to.be.zero;
+ });
+
+ it("cannot stake with someone else's reputation", async () => {
+ await checkErrorRevert(
+ voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER1 }),
+ "voting-invalid-user"
+ );
+ });
+
+ it("cannot stake with insufficient reputation", async () => {
+ const user2Key = makeReputationKey(colony.address, domain1.skillId, USER2);
+ const user2Value = makeReputationValue(WAD.divn(10000), 4);
+ const [user2Mask, user2Siblings] = await reputationTree.getProof(user2Key);
+
+ await checkErrorRevert(
+ voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, user2Key, user2Value, user2Mask, user2Siblings, { from: USER2 }),
+ "voting-insufficient-influence"
+ );
+ });
+
+ it("cannot stake once time runs out", async () => {
+ await forwardTime(STAKE_PERIOD, this);
+
+ await checkErrorRevert(
+ voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }),
+ "voting-not-staking"
+ );
+
+ await checkErrorRevert(
+ voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, requiredStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }),
+ "voting-not-staking"
+ );
+ });
+ });
+
+ describe("voting on motions", async () => {
+ let motionId;
+
+ beforeEach(async () => {
+ const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]);
+ await voting.createMotion(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings);
+ motionId = await voting.getMotionCount();
+
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, requiredStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 });
+ });
+
+ 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(motionId, SALT, NAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+ });
+
+ it("locks the token when the first reveal is made", async () => {
+ await voting.submitVote(motionId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+
+ await forwardTime(SUBMIT_PERIOD, this);
+
+ let lockId = await voting.getLockId(motionId);
+ expect(lockId).to.be.zero;
+
+ await voting.revealVote(motionId, SALT, NAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+
+ lockId = await voting.getLockId(motionId);
+ expect(lockId).to.not.be.zero;
+ });
+
+ it("can unlock the token once revealed", async () => {
+ await voting.submitVote(motionId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+
+ await forwardTime(SUBMIT_PERIOD, this);
+
+ await voting.revealVote(motionId, SALT, NAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+
+ const lockId = await voting.getLockId(motionId);
+ const { lockCount } = await tokenLocking.getUserLock(token.address, USER0);
+ expect(lockCount).to.eq.BN(lockId);
+ });
+
+ it("can tally votes from two users", async () => {
+ 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 forwardTime(SUBMIT_PERIOD, this);
+
+ 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.getMotion(motionId);
+ expect(votes[0][0]).to.be.zero;
+ expect(votes[0][1]).to.eq.BN(WAD.muln(3));
+ });
+
+ it("can update votes, but just the last one counts", async () => {
+ 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(motionId, SALT, NAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }),
+ "voting-secret-no-match"
+ );
+
+ // Revealing second succeeds
+ 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 motion = await voting.getMotion(motionId);
+ expect(motion.totalVotes[0]).to.be.zero;
+
+ await voting.submitVote(motionId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+
+ motion = await voting.getMotion(motionId);
+ expect(motion.totalVotes[0]).to.eq.BN(WAD);
+
+ await voting.submitVote(motionId, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+
+ motion = await voting.getMotion(motionId);
+ expect(motion.totalVotes[0]).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-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 });
+
+ await forwardTime(SUBMIT_PERIOD, this);
+
+ await voting.revealVote(motionId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+
+ await checkErrorRevert(
+ voting.revealVote(motionId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }),
+ "voting-secret-no-match"
+ );
+ });
+
+ 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();
+
+ // Update reputation state
+ const user0Value2 = makeReputationValue(WAD.muln(2), 2);
+ await reputationTree.insert(user0Key, user0Value2);
+
+ 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();
+ 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 forwardTime(SUBMITTER_ONLY_WINDOW + 1, this);
+ await repCycle.confirmNewHash(0);
+
+ // Create new motion with new reputation state
+ await voting.createMotion(ADDRESS_ZERO, FAKE, domain1Key, domain1Value, domain1Mask2, domain1Siblings2);
+ const motionId2 = await voting.getMotionCount();
+ await voting.stakeMotion(motionId2, 1, UINT256_MAX, YAY, requiredStake, user0Key, user0Value2, user0Mask2, user0Siblings2, { from: USER0 });
+ await voting.stakeMotion(motionId2, 1, UINT256_MAX, NAY, requiredStake, user1Key, user1Value, user1Mask2, user1Siblings2, { from: USER1 });
+
+ await voting.submitVote(motionId2, soliditySha3(SALT, NAY), user0Key, user0Value2, user0Mask2, user0Siblings2, { from: USER0 });
+
+ await forwardTime(SUBMIT_PERIOD, this);
+
+ 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(motionId, "0x0", user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }),
+ "voting-invalid-secret"
+ );
+ });
+
+ it("cannot submit a vote if voting is closed", async () => {
+ await forwardTime(SUBMIT_PERIOD, this);
+
+ await checkErrorRevert(
+ voting.submitVote(motionId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }),
+ "voting-not-open"
+ );
+ });
+
+ 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-invalid-root-hash"
+ );
+ });
+
+ 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, NAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }),
+ "voting-not-reveal"
+ );
+ });
+
+ 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);
+ await forwardTime(REVEAL_PERIOD, this);
+
+ await checkErrorRevert(
+ voting.revealVote(motionId, SALT, NAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }),
+ "voting-not-reveal"
+ );
+ });
+
+ it("cannot reveal a vote with a bad secret", async () => {
+ await voting.submitVote(motionId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+
+ await forwardTime(SUBMIT_PERIOD, this);
+
+ await checkErrorRevert(
+ voting.revealVote(motionId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }),
+ "voting-secret-no-match"
+ );
+ });
+ });
+
+ describe("executing motions", async () => {
+ let motionId;
+
+ beforeEach(async () => {
+ const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]);
+ await voting.createMotion(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings);
+ motionId = await voting.getMotionCount();
+ });
+
+ it("cannot execute a non-existent motion", async () => {
+ await checkErrorRevert(voting.finalizeMotion(0), "voting-not-finalizable");
+ });
+
+ it("motion has no effect if extension does not have permissions", async () => {
+ await colony.setAdministrationRole(1, UINT256_MAX, voting.address, 1, false);
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+
+ await forwardTime(STAKE_PERIOD, this);
+ const tasksBefore = await colony.getTaskCount();
+
+ const { logs } = await voting.finalizeMotion(motionId);
+ expect(logs[0].args.executed).to.be.false;
+
+ const tasksAfter = await colony.getTaskCount();
+ expect(tasksAfter).to.eq.BN(tasksBefore);
+ await colony.setAdministrationRole(1, UINT256_MAX, voting.address, 1, true);
+ });
+
+ it("cannot take an action if there is insufficient support", async () => {
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake.subn(1), user0Key, user0Value, user0Mask, user0Siblings, {
+ from: USER0,
+ });
+
+ await forwardTime(STAKE_PERIOD, this);
+
+ await checkErrorRevert(voting.finalizeMotion(motionId), "voting-not-finalizable");
+ });
+
+ it("can take an action if there is insufficient opposition", async () => {
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, requiredStake.subn(1), user1Key, user1Value, user1Mask, user1Siblings, {
+ from: USER1,
+ });
+
+ await forwardTime(STAKE_PERIOD, this);
+
+ 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.createMotion(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings);
+ motionId = await voting.getMotionCount();
+
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+
+ await forwardTime(STAKE_PERIOD, this);
+
+ const { logs } = await voting.finalizeMotion(motionId);
+ expect(logs[0].args.executed).to.be.true;
+ });
+
+ it("can take an action with an arbitrary target", async () => {
+ const { colony: otherColony, token: otherToken } = await setupRandomColony(colonyNetwork);
+ await otherToken.mint(otherColony.address, WAD, { from: USER0 });
+
+ const action = await encodeTxData(colony, "claimColonyFunds", [otherToken.address]);
+ await voting.createMotion(otherColony.address, action, domain1Key, domain1Value, domain1Mask, domain1Siblings);
+ motionId = await voting.getMotionCount();
+
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+
+ await forwardTime(STAKE_PERIOD, this);
+
+ const balanceBefore = await otherColony.getFundingPotBalance(1, otherToken.address);
+ expect(balanceBefore).to.be.zero;
+
+ await voting.finalizeMotion(motionId);
+
+ const balanceAfter = await otherColony.getFundingPotBalance(1, otherToken.address);
+ expect(balanceAfter).to.eq.BN(WAD);
+ });
+
+ it("can take a nonexistent action", async () => {
+ const action = soliditySha3("foo");
+ await voting.createMotion(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings);
+ motionId = await voting.getMotionCount();
+
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+
+ await forwardTime(STAKE_PERIOD, this);
+
+ 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 motionState;
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+
+ motionState = await voting.getMotionState(motionId);
+ expect(motionState).to.eq.BN(STAKING);
+ await checkErrorRevert(voting.finalizeMotion(motionId), "voting-not-finalizable");
+
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, requiredStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 });
+
+ motionState = await voting.getMotionState(motionId);
+ expect(motionState).to.eq.BN(SUBMIT);
+ await checkErrorRevert(voting.finalizeMotion(motionId), "voting-not-finalizable");
+ });
+
+ it("cannot take an action twice", async () => {
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+
+ await forwardTime(STAKE_PERIOD, this);
+
+ const { logs } = await voting.finalizeMotion(motionId);
+ expect(logs[0].args.executed).to.be.true;
+
+ await checkErrorRevert(voting.finalizeMotion(motionId), "voting-not-finalizable");
+ });
+
+ it("can take an action if the motion passes", async () => {
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, requiredStake, 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 });
+
+ // 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.finalizeMotion(motionId);
+ expect(logs[0].args.executed).to.be.true;
+ });
+
+ it("cannot take an action if the motion fails", async () => {
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, requiredStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 });
+
+ await voting.submitVote(motionId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+
+ await forwardTime(SUBMIT_PERIOD, this);
+
+ 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.finalizeMotion(motionId);
+ expect(logs[0].args.executed).to.be.false;
+ });
+
+ it("cannot take an action if there is insufficient voting power (state change actions)", async () => {
+ // 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.createMotion(ethers.constants.AddressZero, action, domain1Key, domain1Value, domain1Mask, domain1Siblings);
+ const motionId1 = await voting.getMotionCount();
+
+ await voting.stakeMotion(motionId1, 1, UINT256_MAX, YAY, requiredStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+ await voting.stakeMotion(motionId1, 1, UINT256_MAX, NAY, requiredStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 });
+
+ await voting.submitVote(motionId1, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+
+ await forwardTime(SUBMIT_PERIOD, this);
+
+ 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.finalizeMotion(motionId1));
+ expect(logs[0].args.executed).to.be.true;
+
+ // Create another motion for the same variable
+ await voting.createMotion(ethers.constants.AddressZero, action, domain1Key, domain1Value, domain1Mask, domain1Siblings);
+ const motionId2 = await voting.getMotionCount();
+
+ await voting.stakeMotion(motionId2, 1, UINT256_MAX, YAY, requiredStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+ await voting.stakeMotion(motionId2, 1, UINT256_MAX, NAY, requiredStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 });
+
+ await voting.submitVote(motionId2, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+
+ await forwardTime(SUBMIT_PERIOD, this);
+
+ 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.finalizeMotion(motionId2));
+ expect(logs[0].args.executed).to.be.false;
+ });
+
+ it("can set vote power correctly after a vote", async () => {
+ 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.createMotion(ethers.constants.AddressZero, action, domain1Key, domain1Value, domain1Mask, domain1Siblings);
+ motionId = await voting.getMotionCount();
+
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, requiredStake, 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 voting.finalizeMotion(motionId);
+ const slotHash = hashExpenditureSlot(action);
+ const pastVote = await voting.getExpenditurePastVote(slotHash);
+ expect(pastVote).to.eq.BN(WAD.muln(2)); // USER0 had 1 WAD each of reputation and tokens
+ });
+
+ 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.createMotion(ADDRESS_ZERO, action1, domain1Key, domain1Value, domain1Mask, domain1Siblings);
+ const motionId1 = await voting.getMotionCount();
+
+ await voting.createMotion(ADDRESS_ZERO, action2, domain1Key, domain1Value, domain1Mask, domain1Siblings);
+ const motionId2 = await voting.getMotionCount();
+
+ await voting.stakeMotion(motionId1, 1, UINT256_MAX, YAY, requiredStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+ await voting.stakeMotion(motionId2, 1, UINT256_MAX, YAY, requiredStake, 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();
+
+ const action = await encodeTxData(colony, "setExpenditureState", [1, UINT256_MAX, expenditureId, 25, [true], ["0x0"], WAD32]);
+
+ await voting.createMotion(ethers.constants.AddressZero, action, domain1Key, domain1Value, domain1Mask, domain1Siblings);
+ motionId = await voting.getMotionCount();
+
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+
+ await forwardTime(STAKE_PERIOD, this);
+
+ await voting.finalizeMotion(motionId);
+ const slotHash = hashExpenditureSlot(action);
+ const pastVote = await voting.getExpenditurePastVote(slotHash);
+ expect(pastVote).to.eq.BN(requiredStake);
+ });
+ });
+
+ describe("claiming rewards", async () => {
+ let motionId;
+
+ beforeEach(async () => {
+ const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]);
+ await voting.createMotion(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings);
+ 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-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);
+ const numEntriesPrev = await repCycle.getReputationUpdateLogLength();
+
+ const nayStake = requiredStake.divn(2);
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, 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.finalizeMotion(motionId);
+
+ 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 voting.claimReward(motionId, 1, UINT256_MAX, USER1, NAY);
+
+ 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 = requiredStake.add(requiredStake.divn(20)); // 110% of stake
+ const expectedReward1 = requiredStake.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);
+
+ // 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(requiredStake.divn(20).neg());
+ });
+
+ it("can let stakers claim rewards, based on the vote outcome", async () => {
+ const addr = await colonyNetwork.getReputationMiningCycle(false);
+ const repCycle = await IReputationMiningCycle.at(addr);
+ const numEntriesPrev = await repCycle.getReputationUpdateLogLength();
+
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, requiredStake, 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 forwardTime(SUBMIT_PERIOD, this);
+
+ 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(REVEAL_PERIOD, this);
+
+ await voting.finalizeMotion(motionId);
+
+ 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 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 motion = await voting.getMotion(motionId);
+ const loserStake = requiredStake.sub(new BN(motion.paidVoterComp));
+ const expectedReward0 = loserStake.divn(3).muln(2).addn(1); // (stake * .8) * (winPct = 1/3 * 2) + dust
+ const expectedReward1 = requiredStake.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);
+
+ // 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(requiredStake.sub(expectedReward0).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();
+
+ const half = requiredStake.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(motionId, 1, UINT256_MAX, USER0, YAY);
+ await voting.claimReward(motionId, 1, UINT256_MAX, USER1, NAY);
+
+ 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(half);
+ expect(new BN(user1LockPost.balance).sub(new BN(user1LockPre.balance))).to.eq.BN(half);
+ });
+
+ it("cannot claim rewards twice", async () => {
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, requiredStake, 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 voting.finalizeMotion(motionId);
+
+ await voting.claimReward(motionId, 1, UINT256_MAX, USER0, YAY);
+
+ await checkErrorRevert(voting.claimReward(motionId, 1, UINT256_MAX, USER0, YAY), "voting-nothing-to-claim");
+ });
+
+ it("cannot claim rewards before a motion is finalized", async () => {
+ await checkErrorRevert(voting.claimReward(motionId, 1, UINT256_MAX, USER0, YAY), "voting-not-claimable");
+ });
+
+ it("can unlock the token after claiming", async () => {
+ const user2Key = makeReputationKey(colony.address, domain1.skillId, USER2);
+ const user2Value = makeReputationValue(WAD.divn(10000), 4);
+ const [user2Mask, user2Siblings] = await reputationTree.getProof(user2Key);
+
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, requiredStake, 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), user2Key, user2Value, user2Mask, user2Siblings, { from: USER2 });
+
+ await forwardTime(SUBMIT_PERIOD, this);
+
+ await voting.revealVote(motionId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+ await voting.revealVote(motionId, SALT, NAY, user2Key, user2Value, user2Mask, user2Siblings, { from: USER2 });
+
+ await forwardTime(REVEAL_PERIOD, this);
+ await forwardTime(ESCALATION_PERIOD, this);
+
+ await voting.finalizeMotion(motionId);
+
+ let lockCount;
+ const lockId = await voting.getLockId(motionId);
+
+ ({ lockCount } = await tokenLocking.getUserLock(token.address, USER1));
+ expect(lockCount).to.be.zero;
+
+ await voting.claimReward(motionId, 1, UINT256_MAX, USER1, NAY);
+
+ ({ lockCount } = await tokenLocking.getUserLock(token.address, USER1));
+ expect(lockCount).to.eq.BN(lockId);
+ });
+ });
+});
diff --git a/test/extensions/voting-rep-2.js b/test/extensions/voting-rep-2.js
new file mode 100644
index 0000000000..da6cee8f11
--- /dev/null
+++ b/test/extensions/voting-rep-2.js
@@ -0,0 +1,2011 @@
+/* globals artifacts */
+
+import BN from "bn.js";
+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, SUBMITTER_ONLY_WINDOW } from "../../helpers/constants";
+
+import {
+ checkErrorRevert,
+ web3GetCode,
+ makeReputationKey,
+ makeReputationValue,
+ getActiveRepCycle,
+ forwardTime,
+ encodeTxData,
+ bn2bytes32,
+ expectEvent,
+} from "../../helpers/test-helper";
+
+import {
+ setupColonyNetwork,
+ setupMetaColonyWithLockedCLNYToken,
+ setupRandomColony,
+ giveUserCLNYTokensAndStake,
+} from "../../helpers/test-data-generator";
+
+import { setupEtherRouter } from "../../helpers/upgradable-contracts";
+
+import PatriciaTree from "../../packages/reputation-miner/patricia";
+
+const { expect } = chai;
+chai.use(bnChai(web3.utils.BN));
+
+const IReputationMiningCycle = artifacts.require("IReputationMiningCycle");
+const TokenLocking = artifacts.require("TokenLocking");
+const VotingReputation2 = artifacts.require("VotingReputation2");
+const OneTxPayment = artifacts.require("OneTxPayment");
+const Resolver = artifacts.require("Resolver");
+const ColonyExtension = artifacts.require("ColonyExtension");
+
+const VOTING_REPUTATION = soliditySha3("VotingReputation2");
+
+contract("Voting Reputation 2", (accounts) => {
+ let colony;
+ let token;
+ let domain1;
+ let domain2;
+ let domain3;
+ let metaColony;
+ let colonyNetwork;
+ let tokenLocking;
+ let reputationVotingVersion;
+
+ let voting;
+
+ let reputationTree;
+
+ let domain1Key;
+ let domain1Value;
+ let domain1Mask;
+ let domain1Siblings;
+
+ let user0Key;
+ let user0Value;
+ let user0Mask;
+ let user0Siblings;
+
+ let user1Key;
+ let user1Value;
+ let user1Mask;
+ let user1Siblings;
+
+ 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 STAKE_PERIOD = SECONDS_PER_DAY * 3;
+ const SUBMIT_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];
+ const USER2 = accounts[2];
+ const MINER = accounts[5];
+
+ const SALT = soliditySha3({ type: "string", value: shortid.generate() });
+ const FAKE = soliditySha3({ type: "string", value: shortid.generate() });
+
+ const NAY = 0;
+ const YAY = 1;
+
+ // 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);
+ const REQUIRED_STAKE_DOMAIN_2 = WAD.divn(1000);
+ const WAD32 = bn2bytes32(WAD);
+ const HALF = WAD.divn(2);
+ const YEAR = SECONDS_PER_DAY * 365;
+
+ 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);
+
+ const votingImplementation = await VotingReputation2.new();
+ const resolver = await Resolver.new();
+ await setupEtherRouter("VotingReputation2", { VotingReputation2: votingImplementation.address }, resolver);
+ await metaColony.addExtensionToNetwork(VOTING_REPUTATION, resolver.address);
+ const versionSig = await resolver.stringToSig("version()");
+ const target = await resolver.lookup(versionSig);
+ const extensionImplementation = await ColonyExtension.at(target);
+ reputationVotingVersion = await extensionImplementation.version();
+ });
+
+ beforeEach(async () => {
+ ({ colony, token } = await setupRandomColony(colonyNetwork));
+
+ // 1 => { 2, 3 }
+ 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);
+
+ await colony.installExtension(VOTING_REPUTATION, reputationVotingVersion);
+ const votingAddress = await colonyNetwork.getExtensionInstallation(VOTING_REPUTATION, colony.address);
+ voting = await VotingReputation2.at(votingAddress);
+
+ await voting.initialise(
+ TOTAL_STAKE_FRACTION,
+ VOTER_REWARD_FRACTION,
+ USER_MIN_STAKE_FRACTION,
+ MAX_VOTE_FRACTION,
+ STAKE_PERIOD,
+ SUBMIT_PERIOD,
+ REVEAL_PERIOD,
+ ESCALATION_PERIOD
+ );
+
+ await colony.setRootRole(voting.address, 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);
+ 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.methods["deposit(address,uint256,bool)"](token.address, WAD, true, { from: USER0 });
+ await tokenLocking.methods["deposit(address,uint256,bool)"](token.address, WAD, true, { from: USER1 });
+ await tokenLocking.methods["deposit(address,uint256,bool)"](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 });
+
+ reputationTree = new PatriciaTree();
+ await reputationTree.insert(
+ makeReputationKey(colony.address, domain1.skillId), // Colony total
+ makeReputationValue(WAD.muln(3), 1)
+ );
+ await reputationTree.insert(
+ makeReputationKey(colony.address, domain1.skillId, USER0), // User0
+ makeReputationValue(WAD, 2)
+ );
+ await reputationTree.insert(
+ makeReputationKey(metaColony.address, domain1.skillId, USER0), // Wrong colony
+ makeReputationValue(WAD, 3)
+ );
+ await reputationTree.insert(
+ makeReputationKey(colony.address, 1234, USER0), // Wrong skill
+ makeReputationValue(WAD, 4)
+ );
+ await reputationTree.insert(
+ makeReputationKey(colony.address, domain1.skillId, USER1), // User1 (and 2x value)
+ makeReputationValue(WAD.muln(2), 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.muln(3), 7)
+ );
+ await reputationTree.insert(
+ 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)
+ );
+ 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);
+ [domain1Mask, domain1Siblings] = await reputationTree.getProof(domain1Key);
+
+ 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);
+ 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("managing the extension", async () => {
+ it("can install the extension manually", async () => {
+ voting = await VotingReputation2.new();
+ await voting.install(colony.address);
+
+ await checkErrorRevert(voting.install(colony.address), "extension-already-installed");
+
+ const identifier = await voting.identifier();
+ const version = await voting.version();
+ expect(identifier).to.equal(VOTING_REPUTATION);
+ expect(version).to.eq.BN(reputationVotingVersion);
+
+ const capabilityRoles = await voting.getCapabilityRoles("0x0");
+ expect(capabilityRoles).to.equal(ethers.constants.HashZero);
+
+ await voting.finishUpgrade();
+ await voting.deprecate(true);
+ await voting.uninstall();
+
+ const code = await web3GetCode(voting.address);
+ expect(code).to.equal("0x");
+ });
+
+ it("can install the extension with the extension manager", async () => {
+ ({ colony } = await setupRandomColony(colonyNetwork));
+ await colony.installExtension(VOTING_REPUTATION, reputationVotingVersion, { from: USER0 });
+
+ await checkErrorRevert(
+ colony.installExtension(VOTING_REPUTATION, reputationVotingVersion, { from: USER0 }),
+ "colony-network-extension-already-installed"
+ );
+ await checkErrorRevert(colony.uninstallExtension(VOTING_REPUTATION, { from: USER1 }), "ds-auth-unauthorized");
+
+ await colony.uninstallExtension(VOTING_REPUTATION, { from: USER0 });
+ });
+
+ it("can deprecate the extension if root", async () => {
+ let deprecated = await voting.getDeprecated();
+ expect(deprecated).to.equal(false);
+
+ await checkErrorRevert(colony.deprecateExtension(VOTING_REPUTATION, true, { from: USER2 }), "ds-auth-unauthorized");
+ await colony.deprecateExtension(VOTING_REPUTATION, true);
+
+ // Can't make new motions!
+ const action = await encodeTxData(colony, "mintTokens", [WAD]);
+ await checkErrorRevert(
+ voting.createMotion(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings),
+ "colony-extension-deprecated"
+ );
+
+ deprecated = await voting.getDeprecated();
+ expect(deprecated).to.equal(true);
+ });
+
+ it("can initialise with valid values and emit expected event", async () => {
+ voting = await VotingReputation2.new();
+ await voting.install(colony.address);
+
+ await expectEvent(voting.initialise(HALF, HALF, WAD, WAD, YEAR, YEAR, YEAR, YEAR), "ExtensionInitialised", []);
+ });
+
+ it("cannot make a motion before initialised", async () => {
+ voting = await VotingReputation2.new();
+ await voting.install(colony.address);
+
+ const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]);
+ await checkErrorRevert(
+ voting.createRootMotion(ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings),
+ "voting-not-active"
+ );
+ });
+
+ it("cannot initialise twice or more if not root", async () => {
+ await checkErrorRevert(voting.initialise(HALF, HALF, WAD, WAD, YEAR, YEAR, YEAR, YEAR), "voting-already-initialised");
+ await checkErrorRevert(voting.initialise(HALF, HALF, WAD, WAD, YEAR, YEAR, YEAR, YEAR, { from: USER2 }), "voting-not-root");
+ });
+
+ it("cannot initialise with invalid values", async () => {
+ voting = await VotingReputation2.new();
+ await voting.install(colony.address);
+
+ await checkErrorRevert(voting.initialise(HALF.addn(1), HALF, WAD, WAD, YEAR, YEAR, YEAR, YEAR), "voting-invalid-value");
+ await checkErrorRevert(voting.initialise(HALF, HALF.addn(1), WAD, WAD, YEAR, YEAR, YEAR, YEAR), "voting-invalid-value");
+ await checkErrorRevert(voting.initialise(HALF, HALF, WAD.addn(1), WAD, YEAR, YEAR, YEAR, YEAR), "voting-invalid-value");
+ await checkErrorRevert(voting.initialise(HALF, HALF, WAD, WAD.addn(1), YEAR, YEAR, YEAR, YEAR), "voting-invalid-value");
+ await checkErrorRevert(voting.initialise(HALF, HALF, WAD, WAD, YEAR + 1, YEAR, YEAR, YEAR), "voting-invalid-value");
+ await checkErrorRevert(voting.initialise(HALF, HALF, WAD, WAD, YEAR, YEAR + 1, YEAR, YEAR), "voting-invalid-value");
+ await checkErrorRevert(voting.initialise(HALF, HALF, WAD, WAD, YEAR, YEAR, YEAR + 1, YEAR), "voting-invalid-value");
+ await checkErrorRevert(voting.initialise(HALF, HALF, WAD, WAD, YEAR, YEAR, YEAR, YEAR + 1), "voting-invalid-value");
+ });
+
+ it("can query for initialisation 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);
+ });
+ });
+
+ describe("creating motions", async () => {
+ it("can create a root motion", async () => {
+ const action = await encodeTxData(colony, "mintTokens", [WAD]);
+ await voting.createMotion(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings);
+
+ const motionId = await voting.getMotionCount();
+ const motion = await voting.getMotion(motionId);
+ expect(motion.skillId).to.eq.BN(domain1.skillId);
+ });
+
+ 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.createMotion(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings);
+
+ const motionId = await voting.getMotionCount();
+ const motion = await voting.getMotion(motionId);
+ expect(motion.skillId).to.eq.BN(domain1.skillId);
+ });
+
+ it("cannot create a domain motion in the root domain with an invalid reputation proof", async () => {
+ // Create motion in domain of action (1)
+ const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]);
+ await checkErrorRevert(
+ voting.createMotion(1, 0, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings),
+ "voting-invalid-domain"
+ );
+ });
+
+ 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 motion in domain of action (2)
+ const action = await encodeTxData(colony, "makeTask", [1, 0, FAKE, 2, 0, 0]);
+ await voting.createMotion(2, UINT256_MAX, ADDRESS_ZERO, action, key, value, mask, siblings);
+
+ 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 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.createMotion(1, 0, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings);
+
+ const motionId = await voting.getMotionCount();
+ const motion = await voting.getMotion(motionId);
+ expect(motion.skillId).to.eq.BN(domain1.skillId);
+ });
+
+ it("can create a root motion with an alternative target", async () => {
+ const { colony: otherColony } = await setupRandomColony(colonyNetwork);
+
+ const action = await encodeTxData(colony, "mintTokens", [WAD]);
+ await voting.createMotion(1, UINT256_MAX, otherColony.address, action, domain1Key, domain1Value, domain1Mask, domain1Siblings);
+ });
+
+ it("can create a domain motion with an alternative target", async () => {
+ const oneTxPayment = await OneTxPayment.new();
+ await oneTxPayment.install(colony.address);
+
+ const action = await encodeTxData(oneTxPayment, "makePaymentFundedFromDomain", [1, 0, 1, 0, [USER0], [token.address], [10], 2, 0]);
+ await voting.createMotion(1, 0, oneTxPayment.address, action, domain1Key, domain1Value, domain1Mask, domain1Siblings);
+
+ const motionId = await voting.getMotionCount();
+ const motion = await voting.getMotion(motionId);
+ expect(motion.skillId).to.eq.BN(domain1.skillId);
+ });
+
+ it("cannot create a domain motion with an action in a higher domain", async () => {
+ const key = makeReputationKey(colony.address, domain2.skillId);
+ const value = makeReputationValue(WAD, 6);
+ const [mask, siblings] = await reputationTree.getProof(key);
+
+ // Action in domain 1, motion in domain 2
+ const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]);
+
+ await checkErrorRevert(voting.createMotion(2, UINT256_MAX, ADDRESS_ZERO, action, key, value, mask, siblings), "voting-invalid-domain");
+ });
+
+ 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);
+ const [mask, siblings] = await reputationTree.getProof(key);
+
+ // Action in (2) but proof for (3)
+ const action = await encodeTxData(colony, "makeTask", [1, 0, FAKE, 2, 0, 0]);
+ await checkErrorRevert(voting.createMotion(1, 1, ADDRESS_ZERO, action, key, value, mask, siblings), "voting-invalid-domain");
+ });
+
+ it("can create a motion using the deprecated interfaces", async () => {
+ const rootAction = await encodeTxData(colony, "mintTokens", [WAD]);
+ const domainAction = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]);
+
+ await voting.createRootMotion(ADDRESS_ZERO, rootAction, domain1Key, domain1Value, domain1Mask, domain1Siblings);
+ await voting.createDomainMotion(1, UINT256_MAX, domainAction, domain1Key, domain1Value, domain1Mask, domain1Siblings);
+ });
+
+ it("when creating a motion for moveFundsBetweenPots, permissions are correctly respected", async () => {
+ // Move funds between domain 2 and domain 3 pots using the old deprecated function
+ // This should not be allowed - it doesn't conform to the standard permission proofs, and so can't
+ // be checked
+ let action = await encodeTxData(colony, "moveFundsBetweenPots", [1, 0, 1, domain2.fundingPotId, domain3.fundingPotId, WAD, token.address]);
+ const key = makeReputationKey(colony.address, domain2.skillId);
+ const value = makeReputationValue(WAD, 6);
+ const [mask, siblings] = await reputationTree.getProof(key);
+ checkErrorRevert(voting.createMotion(2, UINT256_MAX, ADDRESS_ZERO, action, key, value, mask, siblings), "voting-bad-function");
+
+ // Now we make an action with the new moveFundsBetweenPots
+ action = await encodeTxData(colony, "moveFundsBetweenPots", [
+ 1,
+ UINT256_MAX,
+ 1,
+ 0,
+ 1,
+ domain2.fundingPotId,
+ domain3.fundingPotId,
+ WAD,
+ token.address,
+ ]);
+
+ // This is not allowed to be created in domain 2
+ await checkErrorRevert(voting.createMotion(2, UINT256_MAX, ADDRESS_ZERO, action, key, value, mask, siblings), "voting-invalid-domain");
+
+ // But is in the root domain
+ await voting.createMotion(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings);
+ });
+ });
+
+ describe("staking on motions", async () => {
+ let motionId;
+
+ beforeEach(async () => {
+ const action = await encodeTxData(colony, "mintTokens", [WAD]);
+ await voting.createMotion(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings);
+ motionId = await voting.getMotionCount();
+ });
+
+ it("can stake on a motion", async () => {
+ 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, YAY, half, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 });
+
+ 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 motion states correctly", async () => {
+ let motionState = await voting.getMotionState(motionId);
+ expect(motionState).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.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 () => {
+ 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.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));
+ });
+
+ it("cannot stake 0", async () => {
+ await checkErrorRevert(
+ voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, 0, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }),
+ "voting-bad-amount"
+ );
+ });
+
+ it("cannot stake a nonexistent side", async () => {
+ await checkErrorRevert(
+ voting.stakeMotion(motionId, 1, UINT256_MAX, 2, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }),
+ "voting-bad-vote"
+ );
+ });
+
+ it("cannot stake less than the minStake, unless there is less than minStake to go", async () => {
+ const minStake = REQUIRED_STAKE.divn(10);
+
+ await checkErrorRevert(
+ voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, minStake.subn(1), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }),
+ "voting-insufficient-stake"
+ );
+
+ 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.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 () => {
+ await colony.makeExpenditure(1, UINT256_MAX, 1);
+ const expenditureId = await colony.getExpenditureCount();
+ await colony.finalizeExpenditure(expenditureId);
+
+ // Set finalizedTimestamp to WAD
+ const action = await encodeTxData(colony, "setExpenditureState", [1, UINT256_MAX, expenditureId, 25, [true], [bn2bytes32(new BN(3))], WAD32]);
+
+ await voting.createMotion(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings);
+ motionId = await voting.getMotionCount();
+
+ 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 });
+
+ expenditure = await colony.getExpenditure(expenditureId);
+ expect(expenditure.globalClaimDelay).to.eq.BN(SECONDS_PER_DAY * 365);
+
+ await checkErrorRevert(colony.claimExpenditurePayout(expenditureId, 0, token.address), "colony-expenditure-cannot-claim");
+ });
+
+ 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();
+ 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.createMotion(1, UINT256_MAX, 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 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();
+ await colony.finalizeExpenditure(expenditureId);
+
+ // 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))],
+ WAD32,
+ ]);
+
+ await voting.createMotion(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings);
+ motionId = await voting.getMotionCount();
+
+ let expenditureSlot;
+ expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0);
+ expect(expenditureSlot.claimDelay).to.be.zero;
+
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+
+ expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0);
+ expect(expenditureSlot.claimDelay).to.eq.BN(SECONDS_PER_DAY * 365);
+
+ 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", [
+ 1,
+ UINT256_MAX,
+ expenditureId,
+ 27,
+ [false, false],
+ ["0x0", bn2bytes32(new BN(token.address.slice(2), 16))],
+ WAD32,
+ ]);
+
+ await voting.createMotion(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings);
+ motionId = await voting.getMotionCount();
+
+ let expenditureSlot;
+ expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0);
+ expect(expenditureSlot.claimDelay).to.be.zero;
+
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+
+ expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0);
+ expect(expenditureSlot.claimDelay).to.eq.BN(SECONDS_PER_DAY * 365);
+
+ 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.createMotion(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.createMotion(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 3
+ // 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.createMotion(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(SECONDS_PER_DAY * 365);
+
+ const expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0);
+ expect(expenditureSlot.claimDelay).to.eq.BN(SECONDS_PER_DAY * 365 * 2);
+
+ 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 () => {
+ // Create a poorly-formed action (no keys)
+ const action = await encodeTxData(colony, "setExpenditureState", [1, UINT256_MAX, 1, 0, [], [], ethers.constants.HashZero]);
+
+ await voting.createMotion(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings);
+ motionId = await voting.getMotionCount();
+
+ await checkErrorRevert(
+ voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }),
+ "voting-lock-failed"
+ );
+ });
+
+ 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();
+
+ // 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))],
+ WAD32,
+ ]);
+
+ // 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))],
+ WAD32,
+ ]);
+
+ await voting.createMotion(1, UINT256_MAX, ADDRESS_ZERO, action1, domain1Key, domain1Value, domain1Mask, domain1Siblings);
+ const motionId1 = await voting.getMotionCount();
+
+ await voting.createMotion(1, UINT256_MAX, ADDRESS_ZERO, action2, domain1Key, domain1Value, domain1Mask, domain1Siblings);
+ const motionId2 = await voting.getMotionCount();
+
+ let expenditureSlot;
+ expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0);
+ expect(expenditureSlot.claimDelay).to.be.zero;
+
+ 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 });
+
+ expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0);
+ expect(expenditureSlot.claimDelay).to.eq.BN(SECONDS_PER_DAY * 365 * 2);
+
+ await forwardTime(STAKE_PERIOD, this);
+ await voting.finalizeMotion(motionId1);
+
+ expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0);
+ expect(expenditureSlot.claimDelay).to.eq.BN(SECONDS_PER_DAY * 365);
+
+ await voting.finalizeMotion(motionId2);
+
+ expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0);
+ expect(expenditureSlot.claimDelay).to.be.zero;
+ });
+
+ it("cannot stake with someone else's reputation", async () => {
+ await checkErrorRevert(
+ voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER1 }),
+ "voting-invalid-user"
+ );
+ });
+
+ 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);
+
+ await checkErrorRevert(
+ voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user2Key, user2Value, user2Mask, user2Siblings, { from: USER2 }),
+ "voting-insufficient-influence"
+ );
+ });
+
+ it("cannot stake once time runs out", async () => {
+ await forwardTime(STAKE_PERIOD, this);
+
+ await checkErrorRevert(
+ voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }),
+ "voting-not-staking"
+ );
+
+ await checkErrorRevert(
+ voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 }),
+ "voting-not-staking"
+ );
+ });
+ });
+
+ describe("voting on motions", async () => {
+ let motionId;
+
+ beforeEach(async () => {
+ const action = await encodeTxData(colony, "mintTokens", [WAD]);
+ await voting.createMotion(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 });
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 });
+ });
+
+ 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);
+
+ const user0LockPre = await tokenLocking.getUserLock(token.address, USER0);
+
+ await voting.revealVote(motionId, SALT, NAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+
+ const user0LockPost = await tokenLocking.getUserLock(token.address, USER0);
+
+ const expectedReward = REQUIRED_STAKE.muln(2).mul(VOTER_REWARD_FRACTION).div(WAD);
+
+ expect(new BN(user0LockPost.balance).sub(new BN(user0LockPre.balance))).to.eq.BN(expectedReward);
+ });
+
+ it("can tally votes from two users", async () => {
+ 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(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.getMotion(motionId);
+ expect(votes[0][0]).to.be.zero;
+ expect(votes[0][1]).to.eq.BN(WAD.muln(3));
+ });
+
+ it("rewards users for voting appropriately", async () => {
+ 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 });
+
+ const user0LockPre = await tokenLocking.getUserLock(token.address, USER0);
+ const user1LockPre = await tokenLocking.getUserLock(token.address, 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 });
+
+ // Two users voted, check reward split appropriately
+ const user0LockPost = await tokenLocking.getUserLock(token.address, USER0);
+ const user1LockPost = await tokenLocking.getUserLock(token.address, USER1);
+ const expectedReward0 = WAD.divn(3).mul(REQUIRED_STAKE).muln(2).mul(VOTER_REWARD_FRACTION).div(WAD).div(WAD);
+ const expectedReward1 = WAD.muln(2).divn(3).mul(REQUIRED_STAKE).muln(2).mul(VOTER_REWARD_FRACTION).div(WAD).div(WAD);
+
+ 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("tells users what their potential reward range is", async () => {
+ const rewardBase = REQUIRED_STAKE.muln(2).mul(VOTER_REWARD_FRACTION).div(WAD);
+
+ let { 0: rewardMin, 1: rewardMax } = await voting.getVoterRewardRange(motionId, USER0, [WAD]);
+ expect(rewardMin).to.eq.BN(WAD.divn(3).mul(rewardBase).div(WAD));
+ expect(rewardMax).to.eq.BN(rewardBase);
+
+ // They vote, expect no change
+ await voting.submitVote(motionId, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+
+ ({ 0: rewardMin, 1: rewardMax } = await voting.getVoterRewardRange(motionId, USER0, [WAD]));
+ expect(rewardMin).to.eq.BN(WAD.divn(3).mul(rewardBase).div(WAD));
+ expect(rewardMax).to.eq.BN(rewardBase);
+
+ // User 1 has no range, as they are the last to vote
+ ({ 0: rewardMin, 1: rewardMax } = await voting.getVoterRewardRange(motionId, USER1, [WAD.muln(2)]));
+ expect(rewardMin).to.eq.BN(WAD.muln(2).divn(3).mul(rewardBase).div(WAD));
+ expect(rewardMax).to.eq.BN(WAD.muln(2).divn(3).mul(rewardBase).div(WAD));
+ });
+
+ it("can update votes, but just the last one counts", async () => {
+ 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(motionId, SALT, NAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }),
+ "voting-secret-no-match"
+ );
+
+ // Revealing second succeeds
+ 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 motion = await voting.getMotion(motionId);
+ expect(motion.totalVotes[0]).to.be.zero;
+
+ await voting.submitVote(motionId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+
+ motion = await voting.getMotion(motionId);
+ expect(motion.totalVotes[0]).to.eq.BN(WAD);
+
+ await voting.submitVote(motionId, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+
+ motion = await voting.getMotion(motionId);
+ expect(motion.totalVotes[0]).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-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 });
+
+ await voting.revealVote(motionId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+
+ await checkErrorRevert(
+ voting.revealVote(motionId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }),
+ "voting-secret-no-match"
+ );
+ });
+
+ 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();
+
+ // Update reputation state
+ const user0Value2 = makeReputationValue(WAD.muln(2), 2);
+ await reputationTree.insert(user0Key, user0Value2);
+
+ 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();
+ 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 forwardTime(SUBMITTER_ONLY_WINDOW + 1, this);
+ await repCycle.confirmNewHash(0);
+
+ // Create new motion with new reputation state
+ const action = await encodeTxData(colony, "mintTokens", [WAD]);
+ await voting.createMotion(1, UINT256_MAX, ADDRESS_ZERO, action, 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(motionId2, soliditySha3(SALT, NAY), user0Key, user0Value2, user0Mask2, user0Siblings2, { from: USER0 });
+
+ await forwardTime(SUBMIT_PERIOD, this);
+
+ 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(motionId, "0x0", user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }),
+ "voting-invalid-secret"
+ );
+ });
+
+ it("cannot submit a vote if voting is closed", async () => {
+ await forwardTime(SUBMIT_PERIOD, this);
+
+ await checkErrorRevert(
+ voting.submitVote(motionId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }),
+ "voting-not-open"
+ );
+ });
+
+ 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-invalid-root-hash"
+ );
+ });
+
+ 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, NAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }),
+ "voting-not-reveal"
+ );
+ });
+
+ 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);
+ await forwardTime(REVEAL_PERIOD, this);
+
+ await checkErrorRevert(
+ voting.revealVote(motionId, SALT, NAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }),
+ "voting-not-reveal"
+ );
+ });
+
+ it("cannot reveal a vote with a bad secret", async () => {
+ await voting.submitVote(motionId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+
+ await forwardTime(SUBMIT_PERIOD, this);
+
+ await checkErrorRevert(
+ voting.revealVote(motionId, SALT, YAY, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }),
+ "voting-secret-no-match"
+ );
+ });
+ });
+
+ describe("executing motions", async () => {
+ let motionId;
+
+ beforeEach(async () => {
+ const action = await encodeTxData(colony, "mintTokens", [WAD]);
+ await voting.createMotion(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings);
+ motionId = await voting.getMotionCount();
+ });
+
+ it("cannot execute a non-existent motion", async () => {
+ await checkErrorRevert(voting.finalizeMotion(0), "voting-not-finalizable");
+ });
+
+ it("motion has no effect if extension does not have permissions", async () => {
+ await colony.setRootRole(voting.address, false);
+
+ 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.finalizeMotion(motionId);
+ expect(logs[0].args.executed).to.be.false;
+
+ // Cleanup
+ await colony.setRootRole(voting.address, true);
+ });
+
+ it("cannot take an action if there is insufficient support", async () => {
+ const smallStake = REQUIRED_STAKE.subn(1);
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, smallStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+
+ await forwardTime(STAKE_PERIOD, this);
+
+ await checkErrorRevert(voting.finalizeMotion(motionId), "voting-not-finalizable");
+ });
+
+ it("can take an action if there is insufficient opposition", async () => {
+ const smallStake = REQUIRED_STAKE.subn(1);
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, smallStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 });
+
+ await forwardTime(STAKE_PERIOD, this);
+
+ 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.createMotion(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 });
+
+ await forwardTime(STAKE_PERIOD, this);
+
+ const { logs } = await voting.finalizeMotion(motionId);
+ expect(logs[0].args.executed).to.be.true;
+ });
+
+ it("can take an action to install an extension", async () => {
+ let installation = await colonyNetwork.getExtensionInstallation(soliditySha3("OneTxPayment"), colony.address);
+ expect(installation).to.be.equal(ADDRESS_ZERO);
+
+ const oneTxPaymentImplementation = await OneTxPayment.new();
+ const resolver = await Resolver.new();
+ await setupEtherRouter("OneTxPayment", { OneTxPayment: oneTxPaymentImplementation.address }, resolver);
+ await metaColony.addExtensionToNetwork(soliditySha3("OneTxPayment"), resolver.address);
+
+ const oneTxPaymentVersion = await oneTxPaymentImplementation.version();
+
+ const action = await encodeTxData(colony, "installExtension", [soliditySha3("OneTxPayment"), oneTxPaymentVersion]);
+ await voting.createMotion(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 });
+
+ await forwardTime(STAKE_PERIOD, this);
+
+ const { logs } = await voting.finalizeMotion(motionId);
+ expect(logs[1].args.executed).to.be.true;
+
+ installation = await colonyNetwork.getExtensionInstallation(soliditySha3("OneTxPayment"), colony.address);
+ expect(installation).to.not.be.equal(ADDRESS_ZERO);
+ });
+
+ 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.createMotion(1, UINT256_MAX, 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 });
+
+ await forwardTime(STAKE_PERIOD, this);
+
+ const balanceBefore = await otherColony.getFundingPotBalance(1, token.address);
+ expect(balanceBefore).to.be.zero;
+
+ await voting.finalizeMotion(motionId);
+
+ const balanceAfter = await otherColony.getFundingPotBalance(1, token.address);
+ expect(balanceAfter).to.eq.BN(WAD);
+ });
+
+ it("can take a nonexistent action", async () => {
+ const action = soliditySha3("foo");
+ await voting.createMotion(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 });
+
+ await forwardTime(STAKE_PERIOD, this);
+
+ 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 motionState;
+ 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 checkErrorRevert(voting.finalizeMotion(motionId), "voting-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-not-finalizable");
+ });
+
+ it("cannot take an action twice", async () => {
+ 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.finalizeMotion(motionId);
+ expect(logs[0].args.executed).to.be.true;
+
+ await checkErrorRevert(voting.finalizeMotion(motionId), "voting-not-finalizable");
+ });
+
+ 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(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 });
+
+ // 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.finalizeMotion(motionId);
+ expect(logs[0].args.executed).to.be.true;
+ });
+
+ 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(motionId, soliditySha3(SALT, NAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+
+ await forwardTime(SUBMIT_PERIOD, this);
+
+ 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.finalizeMotion(motionId);
+ expect(logs[0].args.executed).to.be.false;
+ });
+
+ it("cannot take an action if there is insufficient voting power (state change actions)", async () => {
+ // 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.createMotion(1, UINT256_MAX, ADDRESS_ZERO, 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 });
+ await voting.stakeMotion(motionId1, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 });
+
+ await voting.submitVote(motionId1, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+
+ await forwardTime(SUBMIT_PERIOD, this);
+
+ 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.finalizeMotion(motionId1));
+ expect(logs[0].args.executed).to.be.true;
+
+ // Create another motion for the same variable
+ await voting.createMotion(1, UINT256_MAX, ADDRESS_ZERO, 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 });
+ await voting.stakeMotion(motionId2, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 });
+
+ await voting.submitVote(motionId2, soliditySha3(SALT, YAY), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+
+ await forwardTime(SUBMIT_PERIOD, this);
+
+ 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.finalizeMotion(motionId2));
+ expect(logs[0].args.executed).to.be.false;
+ });
+
+ it("can set vote power correctly after a vote", async () => {
+ 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.createMotion(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 });
+ 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 voting.finalizeMotion(motionId);
+ const slotHash = hashExpenditureSlot(action);
+ 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 () => {
+ 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.createMotion(1, UINT256_MAX, ADDRESS_ZERO, action1, domain1Key, domain1Value, domain1Mask, domain1Siblings);
+ const motionId1 = await voting.getMotionCount();
+
+ await voting.createMotion(1, UINT256_MAX, 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();
+
+ const action = await encodeTxData(colony, "setExpenditureState", [1, UINT256_MAX, expenditureId, 25, [true], ["0x0"], WAD32]);
+
+ await voting.createMotion(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 });
+
+ await forwardTime(STAKE_PERIOD, this);
+
+ await voting.finalizeMotion(motionId);
+ const slotHash = hashExpenditureSlot(action);
+ const pastVote = await voting.getExpenditurePastVote(slotHash);
+ expect(pastVote).to.eq.BN(REQUIRED_STAKE);
+ });
+ });
+
+ describe("claiming rewards", async () => {
+ let motionId;
+
+ beforeEach(async () => {
+ const action = await encodeTxData(colony, "mintTokens", [WAD]);
+ await voting.createMotion(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings);
+ 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-not-claimable");
+ });
+
+ it("returns 0 for staker rewards if no-one staked on a side of a motion", async () => {
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+
+ const yayStakerReward = await voting.getStakerReward(motionId, USER0, YAY);
+ const nayStakerReward = await voting.getStakerReward(motionId, USER0, NAY);
+
+ expect(yayStakerReward[0]).to.eq.BN(REQUIRED_STAKE);
+ expect(nayStakerReward[0]).to.eq.BN(new BN(0));
+ });
+
+ it("can let stakers claim rewards, based on the stake outcome", async () => {
+ const addr = await colonyNetwork.getReputationMiningCycle(false);
+ const repCycle = await IReputationMiningCycle.at(addr);
+ const numEntriesPrev = await repCycle.getReputationUpdateLogLength();
+
+ const nayStake = REQUIRED_STAKE.divn(2);
+ 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.finalizeMotion(motionId);
+
+ 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 voting.claimReward(motionId, 1, UINT256_MAX, USER1, NAY);
+
+ 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)); // 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);
+
+ // 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 () => {
+ const addr = await colonyNetwork.getReputationMiningCycle(false);
+ const repCycle = await IReputationMiningCycle.at(addr);
+ const numEntriesPrev = await repCycle.getReputationUpdateLogLength();
+
+ 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 });
+
+ await forwardTime(ESCALATION_PERIOD, this);
+
+ await voting.finalizeMotion(motionId);
+
+ 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 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 motion = await voting.getMotion(motionId);
+ const loserStake = REQUIRED_STAKE.sub(new BN(motion.paidVoterComp));
+ const expectedReward0 = loserStake.divn(3).muln(2).addn(1); // (stake * .8) * (winPct = 1/3 * 2) + dust
+ 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);
+
+ // 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.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.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.stakeMotion(motionId, 1, UINT256_MAX, NAY, REQUIRED_STAKE.divn(3), user2Key, user2Value, user2Mask, user2Siblings, {
+ from: USER2,
+ });
+
+ 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 });
+
+ await forwardTime(ESCALATION_PERIOD, this);
+
+ 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(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);
+ const user2LockPost = await tokenLocking.getUserLock(token.address, USER2);
+
+ const motion = await voting.getMotion(motionId);
+ const loserStake = REQUIRED_STAKE.sub(new BN(motion.paidVoterComp));
+ const expectedReward0 = loserStake.muln(2).divn(3); // (stake * .8) * (winPct = 1/3 * 2)
+ const expectedReward1 = REQUIRED_STAKE.add(loserStake.divn(3)).muln(2).divn(3); // 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);
+ expect(new BN(user2LockPost.balance).sub(new BN(user2LockPre.balance))).to.eq.BN(expectedReward2);
+ });
+
+ 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.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE.divn(3).muln(2), user0Key, user0Value, user0Mask, user0Siblings, {
+ from: USER0,
+ });
+ 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(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 });
+
+ await forwardTime(ESCALATION_PERIOD, this);
+
+ 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(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);
+ const user2LockPost = await tokenLocking.getUserLock(token.address, USER2);
+
+ const motion = await voting.getMotion(motionId);
+ const loserStake = REQUIRED_STAKE.sub(new BN(motion.paidVoterComp));
+ // User 0 staked 2/3rds of the losing side. 1/3 of the total stake of that side has been
+ // removed due to that side only receiving a third of the vote
+ const expectedReward0 = loserStake.muln(2).divn(3).muln(2).divn(3); // (stake * .8) * (winPct = 1/3 * 2)
+ // User 1 staked all of the winning side, so gets that back plus a third of what is left
+ // on the losing side as a reward (as the winning side got 2/3rds of the vote)
+ const expectedReward1 = REQUIRED_STAKE.add(loserStake.muln(1).divn(3)); // stake + ((stake * .8) * (1 - (winPct = 2/3 * 2))
+ // Same as user 0, but they only staked 1/3 of the losing side.
+ const expectedReward2 = loserStake.muln(2).divn(3).divn(3); // (stake * .8) * (winPct = 1/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);
+ expect(new BN(user2LockPost.balance).sub(new BN(user2LockPre.balance))).to.eq.BN(expectedReward2);
+ });
+
+ it("can let all 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.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE.divn(3).muln(2), user0Key, user0Value, user0Mask, user0Siblings, {
+ from: USER0,
+ });
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE.divn(6), user1Key, user1Value, user1Mask, user1Siblings, {
+ from: USER1,
+ });
+ 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(6), user2Key, user2Value, user2Mask, user2Siblings, {
+ from: USER2,
+ });
+
+ 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 });
+
+ await forwardTime(ESCALATION_PERIOD, this);
+
+ 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(motionId, 1, UINT256_MAX, USER0, YAY);
+ await voting.claimReward(motionId, 1, UINT256_MAX, USER1, YAY);
+ await voting.claimReward(motionId, 1, UINT256_MAX, USER2, 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);
+ const user2LockPost = await tokenLocking.getUserLock(token.address, USER2);
+
+ const motion = await voting.getMotion(motionId);
+ const loserStake = REQUIRED_STAKE.sub(new BN(motion.paidVoterComp));
+ const expectedReward0 = loserStake.muln(2).divn(3).muln(2).divn(3); // (stake * .8) * (winPct = 1/3 * 2)
+ const expectedReward1 = loserStake.muln(2).divn(3).divn(6);
+ const expectedReward2 = loserStake.divn(3).muln(2).divn(6);
+
+ const expectedReward1B = REQUIRED_STAKE.add(loserStake.divn(3));
+
+ 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.add(expectedReward1B));
+ 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 () => {
+ const addr = await colonyNetwork.getReputationMiningCycle(false);
+ const repCycle = await IReputationMiningCycle.at(addr);
+ const numEntriesPrev = await repCycle.getReputationUpdateLogLength();
+
+ 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(motionId, 1, UINT256_MAX, USER0, YAY);
+ await voting.claimReward(motionId, 1, UINT256_MAX, USER1, NAY);
+
+ 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(half);
+ expect(new BN(user1LockPost.balance).sub(new BN(user1LockPre.balance))).to.eq.BN(half);
+ });
+
+ it("cannot claim rewards twice", 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(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);
+
+ await voting.claimReward(motionId, 1, UINT256_MAX, USER0, YAY);
+ await expectEvent(voting.claimReward(motionId, 1, UINT256_MAX, USER1, NAY), "MotionRewardClaimed", [motionId, USER1, NAY, 0]);
+
+ await checkErrorRevert(voting.claimReward(motionId, 1, UINT256_MAX, USER0, YAY), "voting-nothing-to-claim");
+ });
+
+ it("cannot claim rewards before a motion is finalized", async () => {
+ await checkErrorRevert(voting.claimReward(motionId, 1, UINT256_MAX, USER0, YAY), "voting-not-claimable");
+ });
+ });
+
+ describe("escalating motions", async () => {
+ let motionId;
+ let votingPayout;
+
+ beforeEach(async () => {
+ const domain2Key = makeReputationKey(colony.address, domain2.skillId);
+ const domain2Value = makeReputationValue(WAD, 6);
+ const [domain2Mask, domain2Siblings] = await reputationTree.getProof(domain2Key);
+
+ const user0Key2 = makeReputationKey(colony.address, domain2.skillId, USER0);
+ const user0Value2 = makeReputationValue(WAD.divn(3), 9);
+ const [user0Mask2, user0Siblings2] = await reputationTree.getProof(user0Key2);
+
+ 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.createMotion(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.stakeMotion(motionId, 1, 0, NAY, REQUIRED_STAKE_DOMAIN_2, user0Key2, user0Value2, user0Mask2, user0Siblings2, { from: USER0 });
+ await voting.stakeMotion(motionId, 1, 0, YAY, REQUIRED_STAKE_DOMAIN_2, user1Key2, user1Value2, user1Mask2, user1Siblings2, { from: USER1 });
+
+ // Note that this is a passing vote
+ 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(motionId, SALT, NAY, user0Key2, user0Value2, user0Mask2, user0Siblings2, { from: USER0 });
+ await voting.revealVote(motionId, SALT, YAY, user1Key2, user1Value2, user1Mask2, user1Siblings2, { from: USER1 });
+
+ const voter0reward = await voting.getVoterReward(motionId, USER0, [WAD.divn(3)]);
+ const voter1reward = await voting.getVoterReward(motionId, USER1, [WAD.divn(3).muln(2)]);
+ votingPayout = voter0reward.add(voter1reward);
+ });
+
+ it("can internally escalate a domain motion after a vote", async () => {
+ await voting.escalateMotion(motionId, 1, 0, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 });
+ });
+
+ it("cannot internally escalate a domain motion if not in a 'closed' state", async () => {
+ await forwardTime(ESCALATION_PERIOD, this);
+
+ await voting.finalizeMotion(motionId);
+
+ await checkErrorRevert(
+ voting.escalateMotion(motionId, 1, 0, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER2 }),
+ "voting-not-closed"
+ );
+ });
+
+ it("cannot internally escalate a domain motion with an invalid domain proof", async () => {
+ await checkErrorRevert(
+ voting.escalateMotion(motionId, 1, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }),
+ "voting-invalid-domain-proof"
+ );
+ });
+
+ 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-invalid-root-hash");
+ });
+
+ 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(REQUIRED_STAKE_DOMAIN_2);
+ const nayStake = yayStake.add(REQUIRED_STAKE.divn(10));
+
+ 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 motionState = await voting.getMotionState(motionId);
+ expect(motionState).to.eq.BN(SUBMIT);
+ });
+
+ 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(REQUIRED_STAKE_DOMAIN_2);
+
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, yayStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+
+ await forwardTime(STAKE_PERIOD, this);
+
+ const { logs } = await voting.finalizeMotion(motionId);
+ expect(logs[0].args.executed).to.be.true;
+ });
+
+ 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 });
+
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 });
+
+ await forwardTime(STAKE_PERIOD, this);
+
+ 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 () => {
+ 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.finalizeMotion(motionId);
+ expect(logs[0].args.executed).to.be.true;
+ });
+
+ 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(REQUIRED_STAKE_DOMAIN_2);
+ const nayStake = yayStake.add(votingPayout);
+
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, nayStake, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 });
+
+ await forwardTime(STAKE_PERIOD, this);
+
+ const motionState = await voting.getMotionState(motionId);
+ expect(motionState).to.eq.BN(FAILED);
+
+ // Now check that the rewards come out properly
+ const user1LockPre = await tokenLocking.getUserLock(token.address, USER1);
+
+ await checkErrorRevert(voting.claimReward(motionId, 1, UINT256_MAX, USER0, YAY), "voting-nothing-to-claim");
+ await voting.claimReward(motionId, 1, UINT256_MAX, USER1, NAY);
+
+ const user1LockPost = await tokenLocking.getUserLock(token.address, USER1);
+
+ // REQUIRED_STAKE.div(REQUIRED_STAKE.add(votingPayout)) is the fraction of this side they staked
+ // REQUIRED_STAKE.add(REQUIRED_STAKE_DOMAIN_2).muln(2).divn(3) is what's being awarded to the whole of this side.
+ // The product tells us their expected reward.
+
+ const expectedReward1 = nayStake.mul(REQUIRED_STAKE.add(REQUIRED_STAKE_DOMAIN_2.divn(10))).div(REQUIRED_STAKE.add(votingPayout));
+ 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 motion", async () => {
+ await voting.escalateMotion(motionId, 1, 0, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 });
+
+ const yayStake = REQUIRED_STAKE.sub(REQUIRED_STAKE_DOMAIN_2);
+ const nayStake = yayStake.add(votingPayout);
+
+ 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(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 });
+
+ await forwardTime(ESCALATION_PERIOD, this);
+
+ const { logs } = await voting.finalizeMotion(motionId);
+ 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(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);
+
+ const voter0reward2 = await voting.getVoterReward(motionId, USER0, [WAD]);
+ const voter1reward2 = await voting.getVoterReward(motionId, USER1, [WAD.muln(2)]);
+ const votingPayout2 = voter0reward2.add(voter1reward2);
+
+ const loserStake = REQUIRED_STAKE.sub(votingPayout2); // Take out voter comp
+
+ const expectedReward0 = loserStake.muln(2).divn(3).mul(yayStake).div(REQUIRED_STAKE);
+ const expectedReward1 = nayStake.mul(REQUIRED_STAKE.add(loserStake.divn(3))).div(REQUIRED_STAKE.add(votingPayout));
+
+ 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 still claim rewards after a motion has been escalated but failed to stake", async () => {
+ await voting.escalateMotion(motionId, 1, 0, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 });
+
+ await forwardTime(STAKE_PERIOD, this);
+
+ const { logs } = await voting.finalizeMotion(motionId);
+ expect(logs[0].args.executed).to.be.true;
+
+ // Now check that the rewards come out properly
+ // 1st voter reward paid by YAY (user0)
+ const user0LockPre = await tokenLocking.getUserLock(token.address, USER0);
+ const user1LockPre = await tokenLocking.getUserLock(token.address, USER1);
+
+ await voting.claimReward(motionId, 1, UINT256_MAX, USER0, NAY);
+ await voting.claimReward(motionId, 1, UINT256_MAX, USER1, YAY);
+
+ const user0LockPost = await tokenLocking.getUserLock(token.address, USER0);
+ const user1LockPost = await tokenLocking.getUserLock(token.address, USER1);
+
+ const user0Domain2Rep = WAD.divn(3);
+ const user1Domain2Rep = WAD.divn(3).muln(2);
+ const loserStake = REQUIRED_STAKE_DOMAIN_2.sub(votingPayout); // Take out voter comp
+
+ const winFraction = user0Domain2Rep.mul(WAD).div(user0Domain2Rep.add(user1Domain2Rep));
+
+ const expectedReward0 = loserStake.muln(2).mul(winFraction).div(WAD);
+ const expectedReward1 = REQUIRED_STAKE_DOMAIN_2.add(loserStake.mul(winFraction).div(WAD)); // 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);
+ });
+
+ it("can still claim rewards after a motion has been escalated but not enough was staked", async () => {
+ await voting.escalateMotion(motionId, 1, 0, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 });
+
+ const partialStake = REQUIRED_STAKE.sub(REQUIRED_STAKE_DOMAIN_2).divn(2);
+
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, partialStake, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, partialStake.subn(1000), user1Key, user1Value, user1Mask, user1Siblings, {
+ from: USER1,
+ });
+
+ await forwardTime(STAKE_PERIOD, this);
+
+ const { logs } = await voting.finalizeMotion(motionId);
+ expect(logs[0].args.executed).to.be.true;
+
+ // Now check that the rewards come out properly
+ // 1st voter reward paid by YAY (user0)
+ const user0LockPre = await tokenLocking.getUserLock(token.address, USER0);
+ const user1LockPre = await tokenLocking.getUserLock(token.address, USER1);
+
+ await voting.claimReward(motionId, 1, UINT256_MAX, USER0, NAY);
+ await voting.claimReward(motionId, 1, UINT256_MAX, USER1, YAY);
+
+ const user0LockPost = await tokenLocking.getUserLock(token.address, USER0);
+ const user1LockPost = await tokenLocking.getUserLock(token.address, USER1);
+
+ const loserStake = REQUIRED_STAKE_DOMAIN_2.divn(10).muln(8).add(partialStake);
+ const expectedReward0 = loserStake.divn(3).muln(2);
+ const expectedReward1 = REQUIRED_STAKE_DOMAIN_2.add(loserStake.divn(3)).add(partialStake.subn(1000));
+
+ 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 escalate a motion in the root domain", async () => {
+ const action = await encodeTxData(colony, "mintTokens", [WAD]);
+ await voting.createMotion(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 });
+ 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);
+ });
+
+ it("can skip the staking phase if no new stake is required", async () => {
+ voting = await VotingReputation2.new();
+ await voting.install(colony.address);
+ 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.createMotion(3, UINT256_MAX, ADDRESS_ZERO, 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);
+ });
+ });
+});
diff --git a/test/extensions/voting-token.js b/test/extensions/voting-token.js
new file mode 100644
index 0000000000..313877593e
--- /dev/null
+++ b/test/extensions/voting-token.js
@@ -0,0 +1,1290 @@
+/* globals artifacts */
+
+import BN from "bn.js";
+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, SECONDS_PER_DAY, DEFAULT_STAKE } from "../../helpers/constants";
+import { checkErrorRevert, web3GetCode, forwardTime, encodeTxData, bn2bytes32, expectEvent } from "../../helpers/test-helper";
+
+import {
+ setupColonyNetwork,
+ setupMetaColonyWithLockedCLNYToken,
+ setupRandomColony,
+ giveUserCLNYTokensAndStake,
+} from "../../helpers/test-data-generator";
+
+import { setupEtherRouter } from "../../helpers/upgradable-contracts";
+
+const { expect } = chai;
+chai.use(bnChai(web3.utils.BN));
+
+const IReputationMiningCycle = artifacts.require("IReputationMiningCycle");
+const TokenLocking = artifacts.require("TokenLocking");
+const VotingToken = artifacts.require("VotingToken");
+const Resolver = artifacts.require("Resolver");
+
+const VOTING_TOKEN = soliditySha3("VotingToken");
+
+contract("Voting Token", (accounts) => {
+ let colony;
+ let token;
+ let domain1;
+ let metaColony;
+ let colonyNetwork;
+ let tokenLocking;
+
+ let voting;
+ let requiredStake;
+
+ 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(6); // 60 %
+ const VOTER_REWARD_FRACTION = WAD.divn(10); // 10 %
+
+ const STAKE_PERIOD = SECONDS_PER_DAY * 3;
+ const SUBMIT_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];
+ const USER2 = accounts[2];
+ const MINER = accounts[5];
+
+ const SALT = soliditySha3({ type: "string", value: shortid.generate() });
+ const FAKE = soliditySha3({ type: "string", value: shortid.generate() });
+
+ const NAY = 0;
+ const YAY = 1;
+
+ // 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 WAD32 = bn2bytes32(WAD);
+ const HALF = WAD.divn(2);
+ const YEAR = SECONDS_PER_DAY * 365;
+
+ 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);
+
+ const votingImplementation = await VotingToken.new();
+ const resolver = await Resolver.new();
+ await setupEtherRouter("VotingToken", { VotingToken: votingImplementation.address }, resolver);
+ await metaColony.addExtensionToNetwork(VOTING_TOKEN, resolver.address);
+ });
+
+ beforeEach(async () => {
+ ({ colony, token } = await setupRandomColony(colonyNetwork));
+ domain1 = await colony.getDomain(1);
+
+ await colony.installExtension(VOTING_TOKEN, 1);
+ const votingAddress = await colonyNetwork.getExtensionInstallation(VOTING_TOKEN, colony.address);
+ voting = await VotingToken.at(votingAddress);
+
+ await voting.initialise(
+ TOTAL_STAKE_FRACTION,
+ VOTER_REWARD_FRACTION,
+ USER_MIN_STAKE_FRACTION,
+ MAX_VOTE_FRACTION,
+ STAKE_PERIOD,
+ SUBMIT_PERIOD,
+ REVEAL_PERIOD,
+ ESCALATION_PERIOD
+ );
+
+ await colony.setRootRole(voting.address, true);
+ await colony.setArbitrationRole(1, UINT256_MAX, voting.address, 1, true);
+ await colony.setAdministrationRole(1, UINT256_MAX, voting.address, 1, true);
+
+ const user0influence = WAD;
+ const user1influence = WAD.muln(2);
+ const user2influence = WAD;
+
+ await token.mint(USER0, user0influence);
+ await token.mint(USER1, user1influence);
+ await token.mint(USER2, user2influence);
+ await token.approve(tokenLocking.address, user0influence, { from: USER0 });
+ await token.approve(tokenLocking.address, user1influence, { from: USER1 });
+ await token.approve(tokenLocking.address, user2influence, { from: USER2 });
+ await tokenLocking.methods["deposit(address,uint256,bool)"](token.address, user0influence, true, { from: USER0 });
+ await tokenLocking.methods["deposit(address,uint256,bool)"](token.address, user1influence, true, { from: USER1 });
+ await tokenLocking.methods["deposit(address,uint256,bool)"](token.address, user2influence, true, { from: USER2 });
+ await colony.approveStake(voting.address, 1, user0influence, { from: USER0 });
+ await colony.approveStake(voting.address, 1, user1influence, { from: USER1 });
+ await colony.approveStake(voting.address, 1, user2influence, { from: USER2 });
+
+ const totalSupply = await token.totalSupply();
+ requiredStake = totalSupply.divn(1000);
+ });
+
+ 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("managing the extension", async () => {
+ it("can install the extension manually", async () => {
+ voting = await VotingToken.new();
+ await voting.install(colony.address);
+
+ await checkErrorRevert(voting.install(colony.address), "extension-already-installed");
+
+ const identifier = await voting.identifier();
+ const version = await voting.version();
+ expect(identifier).to.equal(VOTING_TOKEN);
+ expect(version).to.eq.BN(1);
+
+ await voting.finishUpgrade();
+ await voting.deprecate(true);
+ await voting.uninstall();
+
+ const code = await web3GetCode(voting.address);
+ expect(code).to.equal("0x");
+ });
+
+ it("can install the extension with the extension manager", async () => {
+ ({ colony } = await setupRandomColony(colonyNetwork));
+ await colony.installExtension(VOTING_TOKEN, 1, { from: USER0 });
+
+ await checkErrorRevert(colony.installExtension(VOTING_TOKEN, 1, { from: USER0 }), "colony-network-extension-already-installed");
+ await checkErrorRevert(colony.uninstallExtension(VOTING_TOKEN, { from: USER1 }), "ds-auth-unauthorized");
+
+ await colony.uninstallExtension(VOTING_TOKEN, { from: USER0 });
+ });
+
+ it("can deprecate the extension if root", async () => {
+ let deprecated = await voting.getDeprecated();
+ expect(deprecated).to.equal(false);
+
+ await checkErrorRevert(colony.deprecateExtension(VOTING_TOKEN, true, { from: USER2 }), "ds-auth-unauthorized");
+ await colony.deprecateExtension(VOTING_TOKEN, true);
+
+ // Cant make new motions!
+ const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]);
+
+ await checkErrorRevert(voting.createMotion(ADDRESS_ZERO, action), "colony-extension-deprecated");
+
+ deprecated = await voting.getDeprecated();
+ expect(deprecated).to.equal(true);
+ });
+
+ it("cannot make a motion before initialised", async () => {
+ voting = await VotingToken.new();
+ await voting.install(colony.address);
+
+ const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]);
+ await checkErrorRevert(voting.createMotion(ADDRESS_ZERO, action), "voting-not-active");
+ });
+
+ it("cannot initialise twice or more if not root", async () => {
+ await checkErrorRevert(voting.initialise(HALF, HALF, WAD, WAD, YEAR, YEAR, YEAR, YEAR), "voting-already-initialised");
+ await checkErrorRevert(voting.initialise(HALF, HALF, WAD, WAD, YEAR, YEAR, YEAR, YEAR, { from: USER2 }), "voting-not-root");
+ });
+
+ it("cannot initialise with invalid values", async () => {
+ voting = await VotingToken.new();
+ await voting.install(colony.address);
+
+ await checkErrorRevert(voting.initialise(HALF.addn(1), HALF, WAD, WAD, YEAR, YEAR, YEAR, YEAR), "voting-invalid-value");
+ await checkErrorRevert(voting.initialise(HALF, HALF.addn(1), WAD, WAD, YEAR, YEAR, YEAR, YEAR), "voting-invalid-value");
+ await checkErrorRevert(voting.initialise(HALF, HALF, WAD.addn(1), WAD, YEAR, YEAR, YEAR, YEAR), "voting-invalid-value");
+ await checkErrorRevert(voting.initialise(HALF, HALF, WAD, WAD.addn(1), YEAR, YEAR, YEAR, YEAR), "voting-invalid-value");
+ await checkErrorRevert(voting.initialise(HALF, HALF, WAD, WAD, YEAR + 1, YEAR, YEAR, YEAR), "voting-invalid-value");
+ await checkErrorRevert(voting.initialise(HALF, HALF, WAD, WAD, YEAR, YEAR + 1, YEAR, YEAR), "voting-invalid-value");
+ await checkErrorRevert(voting.initialise(HALF, HALF, WAD, WAD, YEAR, YEAR, YEAR + 1, YEAR), "voting-invalid-value");
+ await checkErrorRevert(voting.initialise(HALF, HALF, WAD, WAD, YEAR, YEAR, YEAR, YEAR + 1), "voting-invalid-value");
+ });
+
+ it("can initialised with valid values and emit expected event", async () => {
+ voting = await VotingToken.new();
+ await voting.install(colony.address);
+
+ await expectEvent(voting.initialise(HALF, HALF, WAD, WAD, YEAR, YEAR, YEAR, YEAR), "ExtensionInitialised", []);
+ });
+
+ it("can query for initialisation 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);
+ });
+ });
+
+ 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.createMotion(ADDRESS_ZERO, action);
+
+ const motionId = await voting.getMotionCount();
+ const motion = await voting.getMotion(motionId);
+ expect(motion.skillId).to.eq.BN(domain1.skillId);
+ });
+
+ it("does not lock the token when a motion is created", async () => {
+ const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]);
+ await voting.createMotion(ADDRESS_ZERO, action);
+ const motionId = await voting.getMotionCount();
+
+ const lockId = await voting.getLockId(motionId);
+ expect(lockId).to.be.zero;
+ });
+
+ it("can create a motion with an alternative target", async () => {
+ const action = await encodeTxData(colony, "makeTask", [1, 0, FAKE, 2, 0, 0]);
+ await voting.createMotion(voting.address, action);
+ });
+ });
+
+ describe("staking on motions", async () => {
+ let motionId;
+
+ beforeEach(async () => {
+ const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]);
+ await voting.createMotion(ADDRESS_ZERO, action);
+ motionId = await voting.getMotionCount();
+ });
+
+ it("can stake on a motion", async () => {
+ const half = requiredStake.divn(2);
+
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, half, { from: USER0 });
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, half, { from: USER1 });
+
+ const motion = await voting.getMotion(motionId);
+ expect(motion.stakes[0]).to.be.zero;
+ expect(motion.stakes[1]).to.eq.BN(requiredStake);
+
+ 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 motion states correctly", async () => {
+ let motionState = await voting.getMotionState(motionId);
+ expect(motionState).to.eq.BN(STAKING);
+
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, { from: USER0 });
+ motionState = await voting.getMotionState(motionId);
+ expect(motionState).to.eq.BN(STAKING);
+
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, requiredStake, { from: USER1 });
+ motionState = await voting.getMotionState(motionId);
+ expect(motionState).to.eq.BN(SUBMIT);
+ });
+
+ it("cannot stake 0", async () => {
+ await checkErrorRevert(voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, 0, { from: USER0 }), "voting-bad-amount");
+ });
+
+ it("cannot stake a nonexistent side", async () => {
+ await checkErrorRevert(voting.stakeMotion(motionId, 1, UINT256_MAX, 2, requiredStake, { from: USER0 }), "voting-bad-vote");
+ });
+
+ it("cannot stake less than the minStake, unless there is less than minStake to go", async () => {
+ const minStake = requiredStake.divn(10);
+
+ await checkErrorRevert(voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, minStake.subn(1), { from: USER0 }), "voting-insufficient-stake");
+
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, minStake, { from: USER0 });
+
+ // Unless there's less than the minStake to go!
+
+ const stake = requiredStake.sub(minStake.muln(2)).addn(1);
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, stake, { from: USER0 });
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, minStake.subn(1), { 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();
+ await colony.finalizeExpenditure(expenditureId);
+
+ // Set finalizedTimestamp to WAD
+ const action = await encodeTxData(colony, "setExpenditureState", [1, UINT256_MAX, expenditureId, 25, [true], [bn2bytes32(new BN(3))], WAD32]);
+
+ await voting.createMotion(ADDRESS_ZERO, action);
+ motionId = await voting.getMotionCount();
+
+ let expenditure;
+ expenditure = await colony.getExpenditure(expenditureId);
+ expect(expenditure.globalClaimDelay).to.be.zero;
+
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, { from: USER0 });
+
+ expenditure = await colony.getExpenditure(expenditureId);
+ expect(expenditure.globalClaimDelay).to.eq.BN(SECONDS_PER_DAY * 365);
+
+ await checkErrorRevert(colony.claimExpenditurePayout(expenditureId, 0, token.address), "colony-expenditure-cannot-claim");
+ });
+
+ 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();
+ 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.createMotion(otherColony.address, action);
+ motionId = await voting.getMotionCount();
+
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, { from: USER0 });
+
+ 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();
+ await colony.finalizeExpenditure(expenditureId);
+
+ // 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))],
+ WAD32,
+ ]);
+
+ await voting.createMotion(ADDRESS_ZERO, action);
+ motionId = await voting.getMotionCount();
+
+ let expenditureSlot;
+ expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0);
+ expect(expenditureSlot.claimDelay).to.be.zero;
+
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, { from: USER0 });
+
+ expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0);
+ expect(expenditureSlot.claimDelay).to.eq.BN(SECONDS_PER_DAY * 365);
+
+ 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", [
+ 1,
+ UINT256_MAX,
+ expenditureId,
+ 27,
+ [false, false],
+ ["0x0", bn2bytes32(new BN(token.address.slice(2), 16))],
+ WAD32,
+ ]);
+
+ await voting.createMotion(ADDRESS_ZERO, action);
+ motionId = await voting.getMotionCount();
+
+ let expenditureSlot;
+ expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0);
+ expect(expenditureSlot.claimDelay).to.be.zero;
+
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, { from: USER0 });
+
+ expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0);
+ expect(expenditureSlot.claimDelay).to.eq.BN(SECONDS_PER_DAY * 365);
+
+ 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.createMotion(ADDRESS_ZERO, action);
+ motionId = await voting.getMotionCount();
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, { 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.createMotion(ADDRESS_ZERO, action);
+ motionId = await voting.getMotionCount();
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, { from: USER0 });
+
+ // Motion 3
+ // 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.createMotion(ADDRESS_ZERO, action);
+ motionId = await voting.getMotionCount();
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, { from: USER0 });
+
+ const expenditure = await colony.getExpenditure(expenditureId);
+ expect(expenditure.globalClaimDelay).to.eq.BN(SECONDS_PER_DAY * 365);
+
+ const expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0);
+ expect(expenditureSlot.claimDelay).to.eq.BN(SECONDS_PER_DAY * 365 * 2);
+
+ 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 () => {
+ // Create a poorly-formed action (no keys)
+ const action = await encodeTxData(colony, "setExpenditureState", [1, UINT256_MAX, 1, 0, [], [], ethers.constants.HashZero]);
+
+ await voting.createMotion(ADDRESS_ZERO, action);
+ motionId = await voting.getMotionCount();
+
+ await checkErrorRevert(voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, { from: USER0 }), "voting-lock-failed");
+ });
+
+ 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();
+
+ // 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))],
+ WAD32,
+ ]);
+
+ // 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))],
+ WAD32,
+ ]);
+
+ await voting.createMotion(ADDRESS_ZERO, action1);
+ const motionId1 = await voting.getMotionCount();
+
+ await voting.createMotion(ADDRESS_ZERO, action2);
+ const motionId2 = await voting.getMotionCount();
+
+ let expenditureSlot;
+ expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0);
+ expect(expenditureSlot.claimDelay).to.be.zero;
+
+ await voting.stakeMotion(motionId1, 1, UINT256_MAX, YAY, requiredStake, { from: USER0 });
+ await voting.stakeMotion(motionId2, 1, UINT256_MAX, YAY, requiredStake, { from: USER0 });
+
+ expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0);
+ expect(expenditureSlot.claimDelay).to.eq.BN(SECONDS_PER_DAY * 365 * 2);
+
+ await forwardTime(STAKE_PERIOD, this);
+ await voting.finalizeMotion(motionId1);
+
+ expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0);
+ expect(expenditureSlot.claimDelay).to.eq.BN(SECONDS_PER_DAY * 365);
+
+ await voting.finalizeMotion(motionId2);
+
+ expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0);
+ expect(expenditureSlot.claimDelay).to.be.zero;
+ });
+
+ it("cannot stake with insufficient token balance", async () => {
+ const user3 = accounts[3];
+ const user3influence = WAD.divn(1000);
+
+ await token.mint(user3, user3influence);
+ await token.approve(tokenLocking.address, user3influence, { from: user3 });
+ await tokenLocking.methods["deposit(address,uint256,bool)"](token.address, user3influence, true, { from: user3 });
+ await colony.approveStake(voting.address, 1, user3influence, { from: user3 });
+
+ const totalSupply = await token.totalSupply();
+ requiredStake = totalSupply.divn(1000);
+
+ await checkErrorRevert(voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, { from: user3 }), "voting-insufficient-influence");
+ });
+
+ it("cannot stake once time runs out", async () => {
+ await forwardTime(STAKE_PERIOD, this);
+
+ await checkErrorRevert(voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, { from: USER0 }), "voting-not-staking");
+ });
+ });
+
+ describe("voting on motions", async () => {
+ let motionId;
+
+ beforeEach(async () => {
+ const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]);
+ await voting.createMotion(ADDRESS_ZERO, action);
+ motionId = await voting.getMotionCount();
+
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, { from: USER0 });
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, requiredStake, { from: USER1 });
+ });
+
+ it("can rate and reveal for a motion", async () => {
+ await voting.submitVote(motionId, soliditySha3(SALT, NAY), { from: USER0 });
+
+ await forwardTime(SUBMIT_PERIOD, this);
+
+ await voting.revealVote(motionId, SALT, NAY, { from: USER0 });
+ });
+
+ it("locks the token when the first reveal is made", async () => {
+ await voting.submitVote(motionId, soliditySha3(SALT, NAY), { from: USER0 });
+
+ await forwardTime(SUBMIT_PERIOD, this);
+
+ let lockId = await voting.getLockId(motionId);
+ expect(lockId).to.be.zero;
+
+ await voting.revealVote(motionId, SALT, NAY, { from: USER0 });
+
+ lockId = await voting.getLockId(motionId);
+ expect(lockId).to.not.be.zero;
+ });
+
+ it("can unlock the token once revealed", async () => {
+ await voting.submitVote(motionId, soliditySha3(SALT, NAY), { from: USER0 });
+
+ await forwardTime(SUBMIT_PERIOD, this);
+
+ await voting.revealVote(motionId, SALT, NAY, { from: USER0 });
+
+ const lockId = await voting.getLockId(motionId);
+ const { lockCount } = await tokenLocking.getUserLock(token.address, USER0);
+ expect(lockCount).to.eq.BN(lockId);
+ });
+
+ it("can tally votes from two users", async () => {
+ await voting.submitVote(motionId, soliditySha3(SALT, YAY), { from: USER0 });
+ await voting.submitVote(motionId, soliditySha3(SALT, YAY), { from: USER1 });
+
+ await forwardTime(SUBMIT_PERIOD, this);
+
+ await voting.revealVote(motionId, SALT, YAY, { from: USER0 });
+ await voting.revealVote(motionId, SALT, YAY, { from: USER1 });
+
+ // See final counts
+ const { votes } = await voting.getMotion(motionId);
+ expect(votes[0][0]).to.be.zero;
+ expect(votes[0][1]).to.eq.BN(WAD.muln(3));
+ });
+
+ it("can update votes, but just the last one counts", async () => {
+ await voting.submitVote(motionId, soliditySha3(SALT, NAY), { from: USER0 });
+ await voting.submitVote(motionId, soliditySha3(SALT, YAY), { from: USER0 });
+
+ await forwardTime(SUBMIT_PERIOD, this);
+
+ // Revealing first vote fails
+ await checkErrorRevert(voting.revealVote(motionId, SALT, NAY, { from: USER0 }), "voting-secret-no-match");
+
+ // Revealing second succeeds
+ await voting.revealVote(motionId, SALT, YAY, { from: USER0 });
+ });
+
+ it("can update votes, but the totalVotes does not change", async () => {
+ let motion = await voting.getMotion(motionId);
+ expect(motion.totalVotes[0]).to.be.zero;
+
+ await voting.submitVote(motionId, soliditySha3(SALT, NAY), { from: USER0 });
+
+ motion = await voting.getMotion(motionId);
+ expect(motion.totalVotes[0]).to.eq.BN(WAD);
+
+ await voting.submitVote(motionId, soliditySha3(SALT, YAY), { from: USER0 });
+
+ motion = await voting.getMotion(motionId);
+ expect(motion.totalVotes[0]).to.eq.BN(WAD);
+ });
+
+ it("cannot reveal an invalid vote", async () => {
+ await voting.submitVote(motionId, soliditySha3(SALT, 2), { from: USER0 });
+
+ await forwardTime(SUBMIT_PERIOD, this);
+
+ await checkErrorRevert(voting.revealVote(motionId, SALT, 2, { from: USER0 }), "voting-bad-vote");
+ });
+
+ it("cannot reveal a vote twice, and so cannot vote twice", async () => {
+ await voting.submitVote(motionId, soliditySha3(SALT, YAY), { from: USER0 });
+ await voting.submitVote(motionId, soliditySha3(SALT, NAY), { from: USER1 });
+
+ await forwardTime(SUBMIT_PERIOD, this);
+
+ await voting.revealVote(motionId, SALT, YAY, { from: USER0 });
+
+ await checkErrorRevert(voting.revealVote(motionId, SALT, YAY, { from: USER0 }), "voting-secret-no-match");
+ });
+
+ it("can vote in two motions with two different locks", async () => {
+ await voting.submitVote(motionId, soliditySha3(SALT, NAY), { from: USER0 });
+
+ // Create new motion with new reputation state
+ await voting.createMotion(ADDRESS_ZERO, FAKE);
+ const motionId2 = await voting.getMotionCount();
+
+ await voting.stakeMotion(motionId2, 1, UINT256_MAX, YAY, requiredStake, { from: USER0 });
+ await voting.stakeMotion(motionId2, 1, UINT256_MAX, NAY, requiredStake, { from: USER1 });
+
+ await voting.submitVote(motionId2, soliditySha3(SALT, NAY), { from: USER0 });
+
+ await forwardTime(SUBMIT_PERIOD, this);
+
+ await voting.revealVote(motionId, SALT, NAY, { from: USER0 });
+ await voting.revealVote(motionId2, SALT, NAY, { from: USER0 });
+ });
+
+ it("cannot submit a null vote", async () => {
+ await checkErrorRevert(voting.submitVote(motionId, "0x0", { from: USER0 }), "voting-invalid-secret");
+ });
+
+ it("cannot submit a vote if voting is closed", async () => {
+ await forwardTime(SUBMIT_PERIOD, this);
+
+ await checkErrorRevert(voting.submitVote(motionId, soliditySha3(SALT, NAY), { from: USER0 }), "voting-not-open");
+ });
+
+ it("cannot reveal a vote on a non-existent motion", async () => {
+ await forwardTime(SUBMIT_PERIOD, this);
+
+ await checkErrorRevert(voting.revealVote(0, SALT, YAY, { from: USER0 }), "voting-not-reveal");
+ });
+
+ it("cannot reveal a vote during the submit period", async () => {
+ await voting.submitVote(motionId, soliditySha3(SALT, NAY), { from: USER0 });
+ await checkErrorRevert(voting.revealVote(motionId, SALT, YAY, { from: USER0 }), "voting-not-reveal");
+ });
+
+ it("cannot reveal a vote after the reveal period ends", async () => {
+ await voting.submitVote(motionId, soliditySha3(SALT, NAY), { from: USER0 });
+
+ await forwardTime(SUBMIT_PERIOD, this);
+ await forwardTime(REVEAL_PERIOD, this);
+
+ await checkErrorRevert(voting.revealVote(motionId, SALT, NAY, { from: USER0 }), "voting-not-reveal");
+ });
+
+ it("cannot reveal a vote with a bad secret", async () => {
+ await voting.submitVote(motionId, soliditySha3(SALT, NAY), { from: USER0 });
+
+ await forwardTime(SUBMIT_PERIOD, this);
+
+ await checkErrorRevert(voting.revealVote(motionId, SALT, YAY, { from: USER0 }), "voting-secret-no-match");
+ });
+ });
+
+ describe("executing motions", async () => {
+ let motionId;
+
+ beforeEach(async () => {
+ const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]);
+ await voting.createMotion(ADDRESS_ZERO, action);
+ motionId = await voting.getMotionCount();
+ });
+
+ it("cannot execute a non-existent motion", async () => {
+ await checkErrorRevert(voting.finalizeMotion(0), "voting-not-finalizable");
+ });
+
+ it("motion has no effect if extension does not have permissions", async () => {
+ await colony.setAdministrationRole(1, UINT256_MAX, voting.address, 1, false);
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, { from: USER0 });
+
+ await forwardTime(STAKE_PERIOD, this);
+ const tasksBefore = await colony.getTaskCount();
+
+ const { logs } = await voting.finalizeMotion(motionId);
+ expect(logs[0].args.executed).to.be.false;
+
+ const tasksAfter = await colony.getTaskCount();
+ expect(tasksAfter).to.eq.BN(tasksBefore);
+ await colony.setAdministrationRole(1, UINT256_MAX, voting.address, 1, true);
+ });
+
+ it("cannot take an action if there is insufficient support", async () => {
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake.subn(1), { from: USER0 });
+
+ await forwardTime(STAKE_PERIOD, this);
+
+ await checkErrorRevert(voting.finalizeMotion(motionId), "voting-not-finalizable");
+ });
+
+ it("can take an action if there is insufficient opposition", async () => {
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, { from: USER0 });
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, requiredStake.subn(1), { from: USER1 });
+
+ await forwardTime(STAKE_PERIOD, this);
+
+ 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.createMotion(ADDRESS_ZERO, action);
+ motionId = await voting.getMotionCount();
+
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, { from: USER0 });
+
+ await forwardTime(STAKE_PERIOD, this);
+
+ const { logs } = await voting.finalizeMotion(motionId);
+ expect(logs[0].args.executed).to.be.true;
+ });
+
+ it("can take an action with an arbitrary target", async () => {
+ const { colony: otherColony, token: otherToken } = await setupRandomColony(colonyNetwork);
+ await otherToken.mint(otherColony.address, WAD);
+
+ const action = await encodeTxData(colony, "claimColonyFunds", [otherToken.address]);
+ await voting.createMotion(otherColony.address, action);
+ motionId = await voting.getMotionCount();
+
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, { from: USER0 });
+
+ await forwardTime(STAKE_PERIOD, this);
+
+ const balanceBefore = await otherColony.getFundingPotBalance(1, otherToken.address);
+ expect(balanceBefore).to.be.zero;
+
+ await voting.finalizeMotion(motionId);
+
+ const balanceAfter = await otherColony.getFundingPotBalance(1, otherToken.address);
+ expect(balanceAfter).to.eq.BN(WAD);
+ });
+
+ it("can take a nonexistent action", async () => {
+ const action = soliditySha3("foo");
+ await voting.createMotion(ADDRESS_ZERO, action);
+ motionId = await voting.getMotionCount();
+
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, { from: USER0 });
+
+ await forwardTime(STAKE_PERIOD, this);
+
+ 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 motionState;
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, { from: USER0 });
+
+ motionState = await voting.getMotionState(motionId);
+ expect(motionState).to.eq.BN(STAKING);
+ await checkErrorRevert(voting.finalizeMotion(motionId), "voting-not-finalizable");
+
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, requiredStake, { from: USER1 });
+
+ motionState = await voting.getMotionState(motionId);
+ expect(motionState).to.eq.BN(SUBMIT);
+ await checkErrorRevert(voting.finalizeMotion(motionId), "voting-not-finalizable");
+ });
+
+ it("cannot take an action twice", async () => {
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, { from: USER0 });
+
+ await forwardTime(STAKE_PERIOD, this);
+
+ const { logs } = await voting.finalizeMotion(motionId);
+ expect(logs[0].args.executed).to.be.true;
+
+ await checkErrorRevert(voting.finalizeMotion(motionId), "voting-not-finalizable");
+ });
+
+ it("can take an action if the motion passes", async () => {
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, { from: USER0 });
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, requiredStake, { from: USER1 });
+
+ await voting.submitVote(motionId, soliditySha3(SALT, YAY), { from: USER0 });
+
+ await forwardTime(SUBMIT_PERIOD, this);
+
+ await voting.revealVote(motionId, SALT, YAY, { 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.finalizeMotion(motionId);
+ expect(logs[0].args.executed).to.be.true;
+ });
+
+ it("cannot take an action if the motion fails", async () => {
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, { from: USER0 });
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, requiredStake, { from: USER1 });
+
+ await voting.submitVote(motionId, soliditySha3(SALT, NAY), { from: USER0 });
+
+ await forwardTime(SUBMIT_PERIOD, this);
+
+ await voting.revealVote(motionId, SALT, NAY, { from: USER0 });
+
+ await forwardTime(REVEAL_PERIOD, this);
+ await forwardTime(STAKE_PERIOD, this);
+
+ const { logs } = await voting.finalizeMotion(motionId);
+ expect(logs[0].args.executed).to.be.false;
+ });
+
+ it("cannot take an action if there is insufficient voting power (state change actions)", async () => {
+ // Clear the locks
+ await tokenLocking.methods["deposit(address,uint256,bool)"](token.address, 0, true, { from: USER0 });
+ await tokenLocking.methods["deposit(address,uint256,bool)"](token.address, 0, true, { from: USER1 });
+
+ // 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.createMotion(ADDRESS_ZERO, action);
+ const motionId1 = await voting.getMotionCount();
+
+ await voting.stakeMotion(motionId1, 1, UINT256_MAX, YAY, requiredStake, { from: USER0 });
+ await voting.stakeMotion(motionId1, 1, UINT256_MAX, NAY, requiredStake, { from: USER1 });
+
+ await voting.submitVote(motionId1, soliditySha3(SALT, YAY), { from: USER0 });
+
+ await forwardTime(SUBMIT_PERIOD, this);
+
+ await voting.revealVote(motionId1, SALT, YAY, { from: USER0 });
+
+ await forwardTime(REVEAL_PERIOD, this);
+ await forwardTime(STAKE_PERIOD, this);
+
+ let logs;
+ ({ logs } = await voting.finalizeMotion(motionId1));
+ expect(logs[0].args.executed).to.be.true;
+
+ // Create another motion for the same variable
+ await voting.createMotion(ADDRESS_ZERO, action);
+ const motionId2 = await voting.getMotionCount();
+
+ await voting.stakeMotion(motionId2, 1, UINT256_MAX, YAY, requiredStake, { from: USER0 });
+ await voting.stakeMotion(motionId2, 1, UINT256_MAX, NAY, requiredStake, { from: USER1 });
+
+ await voting.submitVote(motionId2, soliditySha3(SALT, YAY), { from: USER0 });
+
+ await forwardTime(SUBMIT_PERIOD, this);
+
+ await voting.revealVote(motionId2, SALT, YAY, { from: USER0 });
+
+ await forwardTime(REVEAL_PERIOD, this);
+ await forwardTime(STAKE_PERIOD, this);
+
+ ({ logs } = await voting.finalizeMotion(motionId2));
+ expect(logs[0].args.executed).to.be.false;
+ });
+
+ it("can set vote power correctly after a vote", async () => {
+ // Clear the locks
+ await tokenLocking.methods["deposit(address,uint256,bool)"](token.address, 0, true, { from: USER0 });
+ await tokenLocking.methods["deposit(address,uint256,bool)"](token.address, 0, true, { from: USER1 });
+
+ 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.createMotion(ADDRESS_ZERO, action);
+ motionId = await voting.getMotionCount();
+
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, { from: USER0 });
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, requiredStake, { from: USER1 });
+
+ await voting.submitVote(motionId, soliditySha3(SALT, YAY), { from: USER0 });
+
+ await forwardTime(SUBMIT_PERIOD, this);
+
+ await voting.revealVote(motionId, SALT, YAY, { from: USER0 });
+
+ await forwardTime(REVEAL_PERIOD, this);
+ await forwardTime(ESCALATION_PERIOD, this);
+
+ await voting.finalizeMotion(motionId);
+ const slotHash = hashExpenditureSlot(action);
+ 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 () => {
+ 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.createMotion(ADDRESS_ZERO, action1);
+ const motionId1 = await voting.getMotionCount();
+
+ await voting.createMotion(ADDRESS_ZERO, action2);
+ const motionId2 = await voting.getMotionCount();
+
+ await voting.stakeMotion(motionId1, 1, UINT256_MAX, YAY, requiredStake, { from: USER0 });
+ await voting.stakeMotion(motionId2, 1, UINT256_MAX, YAY, requiredStake, { 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();
+
+ const action = await encodeTxData(colony, "setExpenditureState", [1, UINT256_MAX, expenditureId, 25, [true], ["0x0"], WAD32]);
+
+ await voting.createMotion(ADDRESS_ZERO, action);
+ motionId = await voting.getMotionCount();
+
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, { from: USER0 });
+
+ await forwardTime(STAKE_PERIOD, this);
+
+ await voting.finalizeMotion(motionId);
+ const slotHash = hashExpenditureSlot(action);
+ const pastVote = await voting.getExpenditurePastVote(slotHash);
+ expect(pastVote).to.eq.BN(requiredStake);
+ });
+ });
+
+ describe("claiming rewards", async () => {
+ let motionId;
+
+ beforeEach(async () => {
+ const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]);
+ await voting.createMotion(ADDRESS_ZERO, action);
+ 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-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);
+ const numEntriesPrev = await repCycle.getReputationUpdateLogLength();
+
+ const nayStake = requiredStake.divn(2);
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, { from: USER0 });
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, nayStake, { from: USER1 });
+
+ await forwardTime(STAKE_PERIOD, this);
+
+ await voting.finalizeMotion(motionId);
+
+ 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 voting.claimReward(motionId, 1, UINT256_MAX, USER1, NAY);
+
+ 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 = requiredStake.add(requiredStake.divn(20)); // 110% of stake
+ const expectedReward1 = requiredStake.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);
+
+ // 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(requiredStake.divn(20).neg());
+ });
+
+ it("can let stakers claim rewards, based on the vote outcome", async () => {
+ const addr = await colonyNetwork.getReputationMiningCycle(false);
+ const repCycle = await IReputationMiningCycle.at(addr);
+ const numEntriesPrev = await repCycle.getReputationUpdateLogLength();
+
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, { from: USER0 });
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, requiredStake, { from: USER1 });
+
+ await voting.submitVote(motionId, soliditySha3(SALT, YAY), { from: USER0 });
+ await voting.submitVote(motionId, soliditySha3(SALT, NAY), { from: USER1 });
+
+ await forwardTime(SUBMIT_PERIOD, this);
+
+ await voting.revealVote(motionId, SALT, YAY, { from: USER0 });
+ await voting.revealVote(motionId, SALT, NAY, { from: USER1 });
+
+ await forwardTime(REVEAL_PERIOD, this);
+
+ await voting.finalizeMotion(motionId);
+
+ 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 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 motion = await voting.getMotion(motionId);
+ const loserStake = requiredStake.sub(new BN(motion.paidVoterComp));
+ const expectedReward0 = loserStake.divn(3).muln(2).subn(1); // (stake * .8) * (winPct = 1/3 * 2) + dust
+ const expectedReward1 = requiredStake.add(loserStake.divn(3)).subn(1); // stake + ((stake * .8) * (1 - (winPct = 2/3 * 2)) + dust
+
+ 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(requiredStake.sub(expectedReward0).neg());
+ });
+
+ it("can let stakers claim rewards, based on the vote outcome, with multiple losing stakers", async () => {
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, { from: USER0 });
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, requiredStake.divn(3).muln(2), { from: USER1 });
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, requiredStake.divn(3).addn(1), { from: USER2 });
+
+ await voting.submitVote(motionId, soliditySha3(SALT, YAY), { from: USER0 });
+ await voting.submitVote(motionId, soliditySha3(SALT, NAY), { from: USER1 });
+
+ await forwardTime(SUBMIT_PERIOD, this);
+
+ await voting.revealVote(motionId, SALT, YAY, { from: USER0 });
+ await voting.revealVote(motionId, SALT, NAY, { from: USER1 });
+
+ await forwardTime(REVEAL_PERIOD, this);
+
+ 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(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);
+ const user2LockPost = await tokenLocking.getUserLock(token.address, USER2);
+
+ const motion = await voting.getMotion(motionId);
+ const loserStake = requiredStake.sub(new BN(motion.paidVoterComp));
+ const expectedReward0 = loserStake.divn(3).muln(2).subn(1); // (stake * .8) * (winPct = 1/3 * 2) + dust
+ const expectedReward1 = requiredStake.add(loserStake.divn(3)).divn(3).muln(2).subn(2); // stake + ((stake * .8) * (1 - (winPct = 2/3 * 2)) + dust
+ const expectedReward2 = requiredStake.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);
+ expect(new BN(user2LockPost.balance).sub(new BN(user2LockPre.balance))).to.eq.BN(expectedReward2);
+ });
+
+ it("can let stakers claim rewards, based on the vote outcome, with multiple winning stakers", async () => {
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake.divn(3).muln(2), { from: USER0 });
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, requiredStake, { from: USER1 });
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake.divn(3).addn(1), { from: USER2 });
+
+ await voting.submitVote(motionId, soliditySha3(SALT, YAY), { from: USER0 });
+ await voting.submitVote(motionId, soliditySha3(SALT, NAY), { from: USER1 });
+
+ await forwardTime(SUBMIT_PERIOD, this);
+
+ await voting.revealVote(motionId, SALT, YAY, { from: USER0 });
+ await voting.revealVote(motionId, SALT, NAY, { from: USER1 });
+
+ await forwardTime(REVEAL_PERIOD, this);
+
+ 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(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);
+ const user2LockPost = await tokenLocking.getUserLock(token.address, USER2);
+
+ const motion = await voting.getMotion(motionId);
+ const loserStake = requiredStake.sub(new BN(motion.paidVoterComp));
+ const expectedReward0 = loserStake.divn(3).muln(2).divn(3).muln(2).subn(1); // (stake * .8) * (winPct = 1/3 * 2) + dust
+ const expectedReward1 = requiredStake.add(loserStake.divn(3)).subn(1); // stake + ((stake * .8) * (1 - (winPct = 2/3 * 2)) + dust
+ 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);
+ 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 () => {
+ const addr = await colonyNetwork.getReputationMiningCycle(false);
+ const repCycle = await IReputationMiningCycle.at(addr);
+ const numEntriesPrev = await repCycle.getReputationUpdateLogLength();
+
+ const half = requiredStake.divn(2);
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, half, { from: USER0 });
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, half, { 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(motionId, 1, UINT256_MAX, USER0, YAY);
+ await voting.claimReward(motionId, 1, UINT256_MAX, USER1, NAY);
+
+ 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(half);
+ expect(new BN(user1LockPost.balance).sub(new BN(user1LockPre.balance))).to.eq.BN(half);
+ });
+
+ it("cannot claim rewards twice", async () => {
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, { from: USER0 });
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, requiredStake, { from: USER1 });
+
+ await voting.submitVote(motionId, soliditySha3(SALT, YAY), { from: USER0 });
+
+ await forwardTime(SUBMIT_PERIOD, this);
+
+ await voting.revealVote(motionId, SALT, YAY, { from: USER0 });
+
+ await forwardTime(REVEAL_PERIOD, this);
+ await forwardTime(ESCALATION_PERIOD, this);
+
+ await voting.finalizeMotion(motionId);
+
+ await voting.claimReward(motionId, 1, UINT256_MAX, USER0, YAY);
+
+ await checkErrorRevert(voting.claimReward(motionId, 1, UINT256_MAX, USER0, YAY), "voting-nothing-to-claim");
+ });
+
+ it("cannot claim rewards before a motion is finalized", async () => {
+ await checkErrorRevert(voting.claimReward(motionId, 1, UINT256_MAX, USER0, YAY), "voting-not-claimable");
+ });
+
+ it("can unlock the token after claiming", async () => {
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, requiredStake, { from: USER0 });
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, requiredStake, { from: USER1 });
+
+ await voting.submitVote(motionId, soliditySha3(SALT, YAY), { from: USER0 });
+ await voting.submitVote(motionId, soliditySha3(SALT, NAY), { from: USER2 });
+
+ await forwardTime(SUBMIT_PERIOD, this);
+
+ await voting.revealVote(motionId, SALT, YAY, { from: USER0 });
+ await voting.revealVote(motionId, SALT, NAY, { from: USER2 });
+
+ await forwardTime(REVEAL_PERIOD, this);
+ await forwardTime(ESCALATION_PERIOD, this);
+
+ await voting.finalizeMotion(motionId);
+
+ let lockCount;
+ const lockId = await voting.getLockId(motionId);
+
+ ({ lockCount } = await tokenLocking.getUserLock(token.address, USER1));
+ expect(lockCount).to.be.zero;
+
+ await voting.claimReward(motionId, 1, UINT256_MAX, USER1, NAY);
+
+ ({ lockCount } = await tokenLocking.getUserLock(token.address, USER1));
+ expect(lockCount).to.eq.BN(lockId);
+ });
+ });
+});