diff --git a/.circleci/config.yml b/.circleci/config.yml
index 01c9335e74..a9aff9d4df 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -177,6 +177,24 @@ jobs:
root: ./
paths:
- coverage-contracts
+ test-upgrade-coverage:
+ <<: *job_common
+ steps:
+ - checkout
+ - <<: *step_restore_cache
+ - setup_remote_docker:
+ version: 19.03.13
+ - <<: *step_pull_solc_docker
+ - <<: *step_setup_global_packages
+ - run:
+ name: "Running upgrade tests with coverage"
+ command: yarn run test:contracts:upgrade:coverage
+ environment:
+ NODE_OPTIONS: --max_old_space_size=4096
+ - persist_to_workspace:
+ root: ./
+ paths:
+ - coverage-upgrade
test-contracts-extensions-coverage:
<<: *job_common
steps:
@@ -311,6 +329,8 @@ workflows:
context: dockerhub-credentials
- test-chainid-coverage:
context: dockerhub-credentials
+ - test-upgrade-coverage:
+ context: dockerhub-credentials
- check-coverage:
context: dockerhub-credentials
requires:
@@ -318,6 +338,7 @@ workflows:
- test-contracts-extensions-coverage
- test-reputation-coverage
- test-chainid-coverage
+ - test-upgrade-coverage
# nightly:
# triggers:
# - schedule:
diff --git a/.solcover.upgrade.js b/.solcover.upgrade.js
new file mode 100644
index 0000000000..3779afe502
--- /dev/null
+++ b/.solcover.upgrade.js
@@ -0,0 +1,39 @@
+const { execSync } = require("child_process");
+const log = console.log;
+
+// Copies pre-built token artifacts to .coverage_artifacts/contracts
+function provisionTokenContracts(config){
+ let output;
+ const provisionColonyToken = `bash ./scripts/provision-token-contracts.sh`;
+
+ log('Provisioning ColonyToken contracts...')
+ output = execSync(provisionColonyToken);
+ log(output.toString())
+}
+
+function getFilesToSkip(){
+ const array = [
+ 'Migrations.sol',
+ 'common/EtherRouter.sol',
+ 'patriciaTree',
+ 'testHelpers',
+ ];
+
+ const output = execSync("ls ./**/*Updated*", {cwd: "./contracts/"});
+
+ return array.concat(output.toString().split('\n').slice(0,-1))
+}
+
+module.exports = {
+ skipFiles: getFilesToSkip(),
+ providerOptions: {
+ port: 8555,
+ network_id: 1999,
+ account_keys_path: "./ganache-accounts.json",
+ vmErrorsOnRPCResponse: false,
+ total_accounts: 18
+ },
+ onCompileComplete: provisionTokenContracts,
+ istanbulFolder: "./coverage-upgrade"
+}
+
diff --git a/contracts/extensions/IColonyExtension.sol b/contracts/extensions/IColonyExtension.sol
new file mode 100644
index 0000000000..94c0d88e0d
--- /dev/null
+++ b/contracts/extensions/IColonyExtension.sol
@@ -0,0 +1,37 @@
+/*
+ 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/IBasicMetaTransaction.sol";
+
+interface IColonyExtension is IBasicMetaTransaction {
+
+ function identifier() external pure returns (bytes32);
+ function version() external pure virtual returns (uint256);
+ function install(address _colony) external virtual;
+ function finishUpgrade() external virtual;
+ function deprecate(bool _deprecated) external virtual;
+ function uninstall() external virtual;
+
+ function getCapabilityRoles(bytes4 _sig) external view virtual returns (bytes32);
+
+ function getDeprecated() external view returns (bool);
+
+ function getColony() external view returns(address);
+
+}
diff --git a/contracts/extensions/votingReputation/IVotingReputation.sol b/contracts/extensions/votingReputation/IVotingReputation.sol
new file mode 100644
index 0000000000..41fe0aeb57
--- /dev/null
+++ b/contracts/extensions/votingReputation/IVotingReputation.sol
@@ -0,0 +1,322 @@
+/*
+ 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 external 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 external License for more details.
+
+ You should have received a copy of the GNU General external License
+ along with The Colony Network. If not, see .
+*/
+
+pragma solidity 0.7.3;
+pragma experimental ABIEncoderV2;
+
+// import "./../colonyNetwork/IColonyNetwork.sol";
+// import "./../colony/ColonyRoles.sol";
+import "./../../common/IBasicMetaTransaction.sol";
+import "./../IColonyExtension.sol";
+import "./VotingReputationDataTypes.sol";
+// import "./../patriciaTree/PatriciaTreeProofs.sol";
+// import "./../tokenLocking/ITokenLocking.sol";
+// import "./ColonyExtension.sol";
+
+
+interface IVotingReputation is IColonyExtension, VotingReputationDataTypes {
+ /// @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
+ )
+ external;
+
+ // external functions (interface)
+
+ /// @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
+ )
+ external;
+
+ /// @notice Create a motion in the root domain (DEPRECATED)
+ /// @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
+ )
+ external;
+
+ /// @notice Create a motion in any domain (DEPRECATED)
+ /// @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
+ )
+ external;
+
+ /// @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
+ )
+ external;
+
+ /// @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
+ )
+ external;
+
+ /// @notice Reveal a vote secret for a motion
+ /// @param _motionId The id of the motion
+ /// @param _salt The salt used to hash the vote
+ /// @param _vote The side being supported (0 = NAY, 1 = YAY)
+ /// @param _key Reputation tree key for the staker/domain
+ /// @param _value Reputation tree value for the staker/domain
+ /// @param _branchMask The branchmask of the proof
+ /// @param _siblings The siblings of the proof
+ function revealVote(
+ uint256 _motionId,
+ bytes32 _salt,
+ uint256 _vote,
+ bytes memory _key,
+ bytes memory _value,
+ uint256 _branchMask,
+ bytes32[] memory _siblings
+ )
+ external;
+
+ /// @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
+ )
+ external;
+
+ /// @notice Finalized a motion, executing its action if appropriate
+ /// @param _motionId The id of the motion to finalize
+ function finalizeMotion(uint256 _motionId) external;
+
+ /// @notice Return whether a motion, assuming it's in the finalizable state,
+ // is allowed to finalize without the call executing successfully.
+ /// @param _motionId The id of the motion
+ /// @dev We are only expecting this to be called from finalize motion in the contracts.
+ /// It is marked as external only so that the frontend can use it.
+ function failingExecutionAllowed(uint256 _motionId) external view returns (bool);
+
+ /// @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
+ )
+ external;
+
+ // external view functions
+
+ /// @notice Get the total stake fraction
+ /// @return The total stake fraction
+ function getTotalStakeFraction() external view returns (uint256);
+
+ /// @notice Get the voter reward fraction
+ /// @return The voter reward fraction
+ function getVoterRewardFraction() external view returns (uint256) ;
+
+ /// @notice Get the user min stake fraction
+ /// @return The user min stake fraction
+ function getUserMinStakeFraction() external view returns (uint256) ;
+
+ /// @notice Get the max vote fraction
+ /// @return The max vote fraction
+ function getMaxVoteFraction() external view returns (uint256);
+
+ /// @notice Get the stake period
+ /// @return The stake period
+ function getStakePeriod() external view returns (uint256);
+
+ /// @notice Get the submit period
+ /// @return The submit period
+ function getSubmitPeriod() external view returns (uint256);
+
+ /// @notice Get the reveal period
+ /// @return The reveal period
+ function getRevealPeriod() external view returns (uint256);
+
+ /// @notice Get the escalation period
+ /// @return The escalation period
+ function getEscalationPeriod() external view returns (uint256);
+
+ /// @notice Get the total motion count
+ /// @return The total motion count
+ function getMotionCount() external view returns (uint256) ;
+
+ /// @notice Get the data for a single motion
+ /// @param _motionId The id of the motion
+ /// @return motion The motion struct
+ function getMotion(uint256 _motionId) external view returns (Motion memory motion);
+
+ /// @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) external view returns (uint256);
+
+ /// @notice Get the number of ongoing motions for a single expenditure / expenditure slot
+ /// @param _structHash The hash of the expenditureId or expenditureId*expenditureSlot
+ /// @return The number of ongoing motions
+ function getExpenditureMotionCount(bytes32 _structHash) external view returns (uint256);
+
+ /// @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) external view returns (uint256);
+
+ /// @notice Get the current state of the motion
+ /// @return The current motion state
+ function getMotionState(uint256 _motionId) external view returns (MotionState) ;
+
+ /// @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 _voterRep The reputation the voter has in the domain
+ /// @return The voter reward
+ function getVoterReward(uint256 _motionId, uint256 _voterRep) external view returns (uint256) ;
+
+ /// @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 _voterRep The reputation the voter has in the domain
+ /// @param _voterAddress The address the user will be voting as
+ /// @return The voter reward
+ function getVoterRewardRange(uint256 _motionId, uint256 _voterRep, address _voterAddress) external view returns (uint256, uint256) ;
+ /// @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) external view returns (uint256, uint256);
+
+ function createClaimDelayAction(bytes memory action, uint256 value)
+ external
+ returns (bytes memory);
+
+ /// @notice Claim the staker's reward from a motion that was created with v4 of the extension, and is
+ /// now missing and cannot be interacted with via the normal claim function.
+ /// @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 claimMisalignedReward(
+ uint256 _motionId,
+ uint256 _permissionDomainId,
+ uint256 _childSkillIndex,
+ address _staker,
+ uint256 _vote
+ )
+ external;
+}
diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/votingReputation/VotingReputation.sol
similarity index 95%
rename from contracts/extensions/VotingReputation.sol
rename to contracts/extensions/votingReputation/VotingReputation.sol
index 955bbaf9fa..41bd70a6b8 100644
--- a/contracts/extensions/VotingReputation.sol
+++ b/contracts/extensions/votingReputation/VotingReputation.sol
@@ -18,28 +18,17 @@
pragma solidity 0.7.3;
pragma experimental ABIEncoderV2;
-import "./../colonyNetwork/IColonyNetwork.sol";
-import "./../colony/ColonyRoles.sol";
-import "./../common/BasicMetaTransaction.sol";
-import "./../common/ERC20Extended.sol";
-import "./../patriciaTree/PatriciaTreeProofs.sol";
-import "./../tokenLocking/ITokenLocking.sol";
-import "./ColonyExtension.sol";
-
-
-contract VotingReputation is ColonyExtension, PatriciaTreeProofs, BasicMetaTransaction {
-
- // 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
+import "./../../colonyNetwork/IColonyNetwork.sol";
+import "./../../colony/ColonyRoles.sol";
+import "./../../common/BasicMetaTransaction.sol";
+import "./../../common/ERC20Extended.sol";
+import "./../../patriciaTree/PatriciaTreeProofs.sol";
+import "./../../tokenLocking/ITokenLocking.sol";
+import "./../ColonyExtension.sol";
+import "./VotingReputationDataTypes.sol";
+
+
+contract VotingReputation is ColonyExtension, PatriciaTreeProofs, BasicMetaTransaction, VotingReputationDataTypes {
uint256 constant UINT128_MAX = 2**128 - 1;
uint256 constant NAY = 0;
@@ -68,8 +57,6 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs, BasicMetaTrans
"moveFundsBetweenPots(uint256,uint256,uint256,uint256,uint256,uint256,address)"
));
- enum ExtensionState { Deployed, Active, Deprecated }
-
// Initialization data
ExtensionState state;
@@ -97,9 +84,21 @@ 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
+
+ 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
+ mapping (bytes32 => uint256) expenditureMotionCounts; // expenditure struct signature => count
+
mapping(address => uint256) metatransactionNonces;
function getMetatransactionNonce(address userAddress) override public view returns (uint256 nonce){
- return metatransactionNonces[userAddress];
+ // This offset is a result of fixing the storage layout, and having to prevent metatransactions being able to be replayed as a result
+ // of the nonce resetting. The broadcaster has made ~3000 transactions in total at time of commit, so we definitely won't have a single
+ // account at 1 million nonce by then.
+ return metatransactionNonces[userAddress] + 1000000;
}
function incrementMetatransactionNonce(address user) override internal {
@@ -123,7 +122,7 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs, BasicMetaTrans
/// @notice Return the version number
/// @return The version number
function version() public pure override returns (uint256) {
- return 5;
+ return 6;
}
/// @notice Install the extension
@@ -189,7 +188,21 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs, BasicMetaTrans
}
/// @notice Called when upgrading the extension
- function finishUpgrade() public override auth {} // solhint-disable-line no-empty-blocks
+ function finishUpgrade() public override auth {
+ // For colonies that have been made since this the previous version's deployment,
+ // or have done the majority of their motions since, let's at least avoid double-emitting events for motions with
+ // the same id where we can, going forward.
+
+ // Load the value from the wrong storage slot in the previous version
+ uint256 wrongSlotValue;
+ assembly {
+ wrongSlotValue := sload(add(motionCount.slot, 1))
+ }
+ // Set the correct storage slot to the larger of the two values.
+ if (wrongSlotValue > motionCount){
+ motionCount = wrongSlotValue;
+ }
+ } // solhint-disable-line no-empty-blocks
/// @notice Called when deprecating (or undeprecating) the extension
function deprecate(bool _deprecated) public override auth {
@@ -201,35 +214,6 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs, BasicMetaTrans
selfdestruct(address(uint160(address(colony))));
}
- // 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 skillRep;
- uint256 repSubmitted;
- uint256 paidVoterComp;
- uint256[2] pastVoterComp; // [nay, yay]
- uint256[2] stakes; // [nay, yay]
- uint256[2] votes; // [nay, yay]
- bool escalated;
- bool finalized;
- address altTarget;
- bytes action;
- }
-
- // Storage
- 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
- mapping (bytes32 => uint256) expenditureMotionCounts; // expenditure struct signature => count
-
// Public functions (interface)
/// @notice Create a motion
@@ -784,8 +768,8 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs, BasicMetaTrans
Motion storage motion = motions[_motionId];
uint256 requiredStake = getRequiredStake(_motionId);
- // Check for valid motion Id
- if (_motionId == 0 || _motionId > motionCount) {
+ // Check for valid motion Id / motion
+ if (_motionId == 0 || _motionId > motionCount || motion.action.length == 0) {
return MotionState.Null;
diff --git a/contracts/extensions/votingReputation/VotingReputationDataTypes.sol b/contracts/extensions/votingReputation/VotingReputationDataTypes.sol
new file mode 100644
index 0000000000..9b7ebc0906
--- /dev/null
+++ b/contracts/extensions/votingReputation/VotingReputationDataTypes.sol
@@ -0,0 +1,54 @@
+/*
+ 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;
+import "./../../colony/ColonyDataTypes.sol";
+
+
+interface VotingReputationDataTypes {
+ // Constants
+ enum ExtensionState { Deployed, Active, Deprecated }
+
+ 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 skillRep;
+ uint256 repSubmitted;
+ uint256 paidVoterComp;
+ uint256[2] pastVoterComp; // [nay, yay]
+ uint256[2] stakes; // [nay, yay]
+ uint256[2] votes; // [nay, yay]
+ bool escalated;
+ bool finalized;
+ address altTarget;
+ bytes action;
+ }
+
+ // 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);
+}
diff --git a/contracts/extensions/votingReputation/VotingReputationMisalignedRecovery.sol b/contracts/extensions/votingReputation/VotingReputationMisalignedRecovery.sol
new file mode 100644
index 0000000000..a7da1a37ee
--- /dev/null
+++ b/contracts/extensions/votingReputation/VotingReputationMisalignedRecovery.sol
@@ -0,0 +1,209 @@
+/*
+ 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 "./../../colonyNetwork/IColonyNetwork.sol";
+import "./../../colony/IColony.sol";
+import "./../../tokenLocking/ITokenLocking.sol";
+import "./../../../lib/dappsys/math.sol";
+import "./../../../lib/dappsys/auth.sol";
+import "./VotingReputationDataTypes.sol";
+
+contract VotingReputationMisalignedRecovery is DSMath, DSAuth, VotingReputationDataTypes {
+
+ // THIS FILE IS DELIBERATELY WRONG. IF YOU'RE EDITING THIS FILE, AND YOU'VE NOT BEEN EXPLICITLY
+ // TOLD TO DO SO, LEAVE NOW. THERE BE DRAGONS HERE.
+
+ // Constants
+ uint256 constant UINT128_MAX = 2**128 - 1;
+
+ uint256 constant NAY = 0;
+ uint256 constant YAY = 1;
+
+ address resolver; // Align storage with EtherRouter
+
+ IColony colony;
+ bool DO_NOT_USE_deprecated;
+
+ ExtensionState DO_NOT_USE_state;
+
+ IColonyNetwork colonyNetwork;
+ ITokenLocking tokenLocking;
+ address token;
+ uint256 totalStakeFraction;
+ uint256 DO_NOT_USE_voterRewardFraction;
+ uint256 DO_NOT_USE_userMinStakeFraction;
+ uint256 DO_NOT_USE_maxVoteFraction;
+ uint256 DO_NOT_USE_stakePeriod;
+ uint256 DO_NOT_USE_submitPeriod;
+ uint256 DO_NOT_USE_revealPeriod;
+ uint256 DO_NOT_USE_escalationPeriod;
+
+ // Here we deliberately recreate the misalignment in the storage slots, so solidity can correctly
+ // find the incorrect data in the mappings.
+ mapping(address => uint256) DO_NOT_USE_metatransactionNonces;
+
+ uint256 DO_NOT_USE_motionCount;
+ mapping (uint256 => Motion) motions;
+ mapping (uint256 => mapping (address => mapping (uint256 => uint256))) stakes;
+ mapping (uint256 => mapping (address => bytes32)) DO_NOT_USE_voteSecrets;
+
+ mapping (bytes32 => uint256) DO_NOT_USE_expenditurePastVotes;
+ mapping (bytes32 => uint256) DO_NOT_USE_expenditureMotionCounts;
+
+ // Public functions (interface)
+
+ /// @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 claimMisalignedReward(
+ uint256 _motionId,
+ uint256 _permissionDomainId,
+ uint256 _childSkillIndex,
+ address _staker,
+ uint256 _vote
+ )
+ public
+ {
+ Motion storage motion = motions[_motionId];
+ // Motions might have been in any point in their lifecycle, so we lose our restirction
+ // on only being able to call this function on finalized/failed motions. These motions
+ // created while misaligned no longer exist, and cannot proceed through their lifecycle.
+ // require(
+ // getMotionState(_motionId) == MotionState.Finalized ||
+ // getMotionState(_motionId) == MotionState.Failed,
+ // "voting-rep-motion-not-claimable"
+ // );
+
+ (uint256 stakerReward, uint256 repPenalty) = getStakerReward(_motionId, _staker, _vote);
+
+ require(stakes[_motionId][_staker][_vote] > 0, "voting-rep-nothing-to-claim");
+ delete stakes[_motionId][_staker][_vote];
+
+ tokenLocking.transfer(token, stakerReward, _staker, true);
+
+ if (repPenalty > 0) {
+ colony.emitDomainReputationPenalty(
+ _permissionDomainId,
+ _childSkillIndex,
+ motion.domainId,
+ _staker,
+ -int256(repPenalty)
+ );
+ }
+
+ emit MotionRewardClaimed(_motionId, _staker, _vote, stakerReward);
+ }
+
+ /// @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) internal 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;
+
+ // If finalized and went to a vote, use vote to determine reward or penalty
+ if (motion.finalized && add(motion.votes[NAY], motion.votes[YAY]) > 0) {
+
+ uint256 loserStake;
+ uint256 winnerStake;
+ if (motion.votes[YAY] > motion.votes[NAY]){
+ loserStake = motion.stakes[NAY];
+ winnerStake = motion.stakes[YAY];
+ } else {
+ loserStake = motion.stakes[YAY];
+ winnerStake = motion.stakes[NAY];
+ }
+
+ loserStake = sub(loserStake, motion.paidVoterComp);
+ uint256 totalVotes = add(motion.votes[NAY], motion.votes[YAY]);
+ uint256 winFraction = wdiv(motion.votes[_vote], totalVotes);
+ uint256 winShare = wmul(winFraction, 2 * WAD); // On a scale of 0-2 WAD
+
+ if (winShare > WAD || (winShare == WAD && _vote == NAY)) {
+ // 50% gets 0% of loser's stake, 100% gets 100% of loser's stake, linear in between
+ stakerReward = wmul(stakeFraction, add(winnerStake, wmul(loserStake, winShare - WAD)));
+ } else {
+ stakerReward = wmul(stakeFraction, wmul(loserStake, winShare));
+ repPenalty = sub(realStake, stakerReward);
+ }
+
+ // Else if finalized, rewards based on stakes alone
+ } else if (motion.finalized) {
+ assert(motion.paidVoterComp == 0);
+ 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;
+
+ }
+ } else {
+ // Motion was never finalized. We just return stakes, exactly as if neither
+ // side fully staked or no votes were revealed.
+ stakerReward = realStake;
+ }
+
+ return (stakerReward, repPenalty);
+ }
+
+ function getRequiredStake(uint256 _motionId) internal view returns (uint256) {
+ return wmul(motions[_motionId].skillRep, totalStakeFraction);
+ }
+
+ function flip(uint256 _vote) internal pure returns (uint256) {
+ return sub(1, _vote);
+ }
+}
diff --git a/contracts/testHelpers/VotingReputationMisaligned.sol b/contracts/testHelpers/VotingReputationMisaligned.sol
new file mode 100644
index 0000000000..27875326ab
--- /dev/null
+++ b/contracts/testHelpers/VotingReputationMisaligned.sol
@@ -0,0 +1,1138 @@
+/*
+ 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 "./../colonyNetwork/IColonyNetwork.sol";
+import "./../colony/ColonyRoles.sol";
+import "./../common/BasicMetaTransaction.sol";
+import "./../common/ERC20Extended.sol";
+import "./../patriciaTree/PatriciaTreeProofs.sol";
+import "./../tokenLocking/ITokenLocking.sol";
+import "./../extensions/ColonyExtension.sol";
+
+
+contract VotingReputationMisaligned is ColonyExtension, PatriciaTreeProofs, BasicMetaTransaction {
+
+ // 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 }
+
+ // Initialization data
+ 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
+ mapping(address => uint256) metatransactionNonces;
+ function getMetatransactionNonce(address userAddress) override public view returns (uint256 nonce){
+ return metatransactionNonces[userAddress];
+ }
+
+ function incrementMetatransactionNonce(address user) override internal {
+ metatransactionNonces[user]++;
+ }
+
+ // Modifiers
+
+ modifier onlyRoot() {
+ require(colony.hasUserRole(msgSender(), 1, ColonyDataTypes.ColonyRole.Root), "voting-rep-caller-not-root");
+ _;
+ }
+
+ // Public
+
+ /// @notice Returns the identifier of the extension
+ function identifier() public override pure returns (bytes32) {
+ return keccak256("VotingReputation");
+ }
+
+ /// @notice Return the version number
+ /// @return The version number
+ function version() public pure override returns (uint256) {
+ return 5;
+ }
+
+ /// @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 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-rep-already-initialised");
+
+ require(_totalStakeFraction <= WAD / 2, "voting-rep-greater-than-half-wad");
+ require(_voterRewardFraction <= WAD / 2, "voting-rep-greater-than-half-wad");
+
+ require(_userMinStakeFraction <= WAD, "voting-rep-greater-than-wad");
+ require(_maxVoteFraction <= WAD, "voting-rep-greater-than-wad");
+
+ require(_stakePeriod <= 365 days, "voting-rep-period-too-long");
+ require(_submitPeriod <= 365 days, "voting-rep-period-too-long");
+ require(_revealPeriod <= 365 days, "voting-rep-period-too-long");
+ require(_escalationPeriod <= 365 days, "voting-rep-period-too-long");
+
+ state = ExtensionState.Active;
+
+ totalStakeFraction = _totalStakeFraction;
+ voterRewardFraction = _voterRewardFraction;
+
+ userMinStakeFraction = _userMinStakeFraction;
+ maxVoteFraction = _maxVoteFraction;
+
+ stakePeriod = _stakePeriod;
+ submitPeriod = _submitPeriod;
+ revealPeriod = _revealPeriod;
+ escalationPeriod = _escalationPeriod;
+
+ emit ExtensionInitialised();
+ }
+
+ /// @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))));
+ }
+
+ // 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 skillRep;
+ uint256 repSubmitted;
+ uint256 paidVoterComp;
+ uint256[2] pastVoterComp; // [nay, yay]
+ uint256[2] stakes; // [nay, yay]
+ uint256[2] votes; // [nay, yay]
+ bool escalated;
+ bool finalized;
+ address altTarget;
+ bytes action;
+ }
+
+ // Storage
+ 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
+ mapping (bytes32 => uint256) expenditureMotionCounts; // expenditure struct signature => count
+
+ // Public functions (interface)
+
+ /// @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
+ {
+ require(state == ExtensionState.Active, "voting-rep-not-active");
+ require(_altTarget != address(colony), "voting-rep-alt-target-cannot-be-base-colony");
+
+ address target = getTarget(_altTarget);
+ bytes4 action = getSig(_action);
+
+ require(action != OLD_MOVE_FUNDS_SIG, "voting-rep-disallowed-function");
+
+ uint256 skillId;
+
+ if (ColonyRoles(target).getCapabilityRoles(action) | ROOT_ROLES == ROOT_ROLES) {
+
+ // A root or unpermissioned function
+ require(_domainId == 1 && _childSkillIndex == UINT256_MAX, "voting-rep-invalid-domain-id");
+ skillId = colony.getDomain(1).skillId;
+
+ } else {
+
+ // A domain permissioned function
+ skillId = colony.getDomain(_domainId).skillId;
+ uint256 actionDomainSkillId = getActionDomainSkillId(_action);
+
+ if (skillId != actionDomainSkillId) {
+ uint256 childSkillId = colonyNetwork.getChildSkillId(skillId, _childSkillIndex);
+ require(childSkillId == actionDomainSkillId, "voting-rep-invalid-domain-id");
+ } else {
+ require(_childSkillIndex == UINT256_MAX, "voting-rep-invalid-domain-id");
+ }
+ }
+
+ 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.skillRep = getReputationFromProof(motionCount, address(0x0), _key, _value, _branchMask, _siblings);
+ motion.altTarget = _altTarget;
+ motion.action = _action;
+
+ emit MotionCreated(motionCount, msgSender(), _domainId);
+ }
+
+ /// @notice Create a motion in the root domain (DEPRECATED)
+ /// @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
+ {
+ createMotion(1, UINT256_MAX, _altTarget, _action, _key, _value, _branchMask, _siblings);
+ }
+
+ /// @notice Create a motion in any domain (DEPRECATED)
+ /// @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
+ {
+ createMotion(_domainId, _childSkillIndex, address(0x0), _action, _key, _value, _branchMask, _siblings);
+ }
+
+ /// @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
+ {
+ Motion storage motion = motions[_motionId];
+ require(_vote <= 1, "voting-rep-bad-vote");
+ require(getMotionState(_motionId) == MotionState.Staking, "voting-rep-motion-not-staking");
+
+ uint256 requiredStake = getRequiredStake(_motionId);
+ uint256 amount = min(_amount, sub(requiredStake, motion.stakes[_vote]));
+ require(amount > 0, "voting-rep-bad-amount");
+
+ uint256 stakerTotalAmount = add(stakes[_motionId][msgSender()][_vote], amount);
+
+ require(
+ stakerTotalAmount <= getReputationFromProof(_motionId, msgSender(), _key, _value, _branchMask, _siblings),
+ "voting-rep-insufficient-rep"
+ );
+ require(
+ stakerTotalAmount >= wmul(requiredStake, userMinStakeFraction) ||
+ add(motion.stakes[_vote], amount) == requiredStake, // To prevent a residual stake from being un-stakable
+ "voting-rep-insufficient-stake"
+ );
+
+ // Update the stake
+ motion.stakes[_vote] = add(motion.stakes[_vote], amount);
+ stakes[_motionId][msgSender()][_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.altTarget == address(0x0)
+ ) {
+ bytes32 structHash = hashExpenditureActionStruct(motion.action);
+ expenditureMotionCounts[structHash] = add(expenditureMotionCounts[structHash], 1);
+ // Set to UINT256_MAX / 3 to avoid overflow (finalizedTimestamp + globalClaimDelay + claimDelay)
+ bytes memory claimDelayAction = createClaimDelayAction(motion.action, UINT256_MAX / 3);
+ require(executeCall(_motionId, claimDelayAction), "voting-rep-expenditure-lock-failed");
+ }
+
+ emit MotionStaked(_motionId, msgSender(), _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
+ delete motion.votes;
+ delete motion.repSubmitted;
+
+ emit MotionEventSet(_motionId, STAKE_END);
+ }
+
+ // Do the external bookkeeping
+ tokenLocking.deposit(token, 0, true); // Faux deposit to clear any locks
+ colony.obligateStake(msgSender(), motion.domainId, amount);
+ colony.transferStake(_permissionDomainId, _childSkillIndex, address(this), msgSender(), 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
+ /// @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
+ {
+ Motion storage motion = motions[_motionId];
+ require(getMotionState(_motionId) == MotionState.Submit, "voting-rep-motion-not-open");
+ require(_voteSecret != bytes32(0), "voting-rep-invalid-secret");
+
+ uint256 userRep = getReputationFromProof(_motionId, msgSender(), _key, _value, _branchMask, _siblings);
+
+ // Count reputation if first submission
+ if (voteSecrets[_motionId][msgSender()] == bytes32(0)) {
+ motion.repSubmitted = add(motion.repSubmitted, userRep);
+ }
+
+ voteSecrets[_motionId][msgSender()] = _voteSecret;
+
+ emit MotionVoteSubmitted(_motionId, msgSender());
+
+ if (motion.repSubmitted >= wmul(motion.skillRep, maxVoteFraction)) {
+ motion.events[SUBMIT_END] = uint64(block.timestamp);
+ motion.events[REVEAL_END] = uint64(block.timestamp + revealPeriod);
+
+ emit MotionEventSet(_motionId, SUBMIT_END);
+ }
+ }
+
+ /// @notice Reveal a vote secret for a motion
+ /// @param _motionId The id of the motion
+ /// @param _salt The salt used to hash the vote
+ /// @param _vote The side being supported (0 = NAY, 1 = YAY)
+ /// @param _key Reputation tree key for the staker/domain
+ /// @param _value Reputation tree value for the staker/domain
+ /// @param _branchMask The branchmask of the proof
+ /// @param _siblings The siblings of the proof
+ function revealVote(
+ uint256 _motionId,
+ bytes32 _salt,
+ uint256 _vote,
+ bytes memory _key,
+ bytes memory _value,
+ uint256 _branchMask,
+ bytes32[] memory _siblings
+ )
+ public
+ {
+ Motion storage motion = motions[_motionId];
+ require(getMotionState(_motionId) == MotionState.Reveal, "voting-rep-motion-not-reveal");
+ require(_vote <= 1, "voting-rep-bad-vote");
+
+ uint256 userRep = getReputationFromProof(_motionId, msgSender(), _key, _value, _branchMask, _siblings);
+ motion.votes[_vote] = add(motion.votes[_vote], userRep);
+
+ bytes32 voteSecret = voteSecrets[_motionId][msgSender()];
+ require(voteSecret == getVoteSecret(_salt, _vote), "voting-rep-secret-no-match");
+ delete voteSecrets[_motionId][msgSender()];
+
+ uint256 voterReward = getVoterReward(_motionId, userRep);
+ motion.paidVoterComp = add(motion.paidVoterComp, voterReward);
+
+ emit MotionVoteRevealed(_motionId, msgSender(), _vote);
+
+ // See if reputation revealed matches reputation submitted
+ if (add(motion.votes[NAY], motion.votes[YAY]) == motion.repSubmitted) {
+ motion.events[REVEAL_END] = uint64(block.timestamp);
+
+ emit MotionEventSet(_motionId, REVEAL_END);
+ }
+
+ tokenLocking.transfer(token, voterReward, msgSender(), true);
+ }
+
+ /// @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-rep-motion-not-closed");
+
+ uint256 newDomainSkillId = colony.getDomain(_newDomainId).skillId;
+ uint256 childSkillId = colonyNetwork.getChildSkillId(newDomainSkillId, _childSkillIndex);
+ require(childSkillId == motion.skillId, "voting-rep-invalid-domain-proof");
+
+ uint256 domainId = motion.domainId;
+ motion.domainId = _newDomainId;
+ motion.skillId = newDomainSkillId;
+ motion.skillRep = getReputationFromProof(_motionId, address(0x0), _key, _value, _branchMask, _siblings);
+
+ uint256 loser = (motion.votes[NAY] < motion.votes[YAY]) ? NAY : YAY;
+ motion.stakes[loser] = sub(motion.stakes[loser], motion.paidVoterComp);
+ motion.pastVoterComp[loser] = add(motion.pastVoterComp[loser], motion.paidVoterComp);
+ delete motion.paidVoterComp;
+
+ 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, msgSender(), domainId, _newDomainId);
+
+ if (motion.events[STAKE_END] <= uint64(block.timestamp)) {
+ emit MotionEventSet(_motionId, STAKE_END);
+ }
+ }
+
+ function finalizeMotion(uint256 _motionId) public {
+ Motion storage motion = motions[_motionId];
+ require(getMotionState(_motionId) == MotionState.Finalizable, "voting-rep-motion-not-finalizable");
+
+ assert(
+ motion.stakes[YAY] == getRequiredStake(_motionId) ||
+ add(motion.votes[NAY], motion.votes[YAY]) > 0
+ );
+
+ motion.finalized = true;
+
+ bool canExecute = (
+ motion.stakes[NAY] < motion.stakes[YAY] ||
+ motion.votes[NAY] < motion.votes[YAY]
+ );
+
+ if (
+ getSig(motion.action) == CHANGE_FUNCTION_SIG &&
+ getTarget(motion.altTarget) == address(colony)
+ ) {
+ bytes32 structHash = hashExpenditureActionStruct(motion.action);
+ expenditureMotionCounts[structHash] = sub(expenditureMotionCounts[structHash], 1);
+
+ // Release the claimDelay if this is the last active motion
+ if (expenditureMotionCounts[structHash] == 0) {
+ bytes memory claimDelayAction = createClaimDelayAction(motion.action, 0);
+ // No require this time, since we don't want stakes to be permanently locked
+ executeCall(_motionId, claimDelayAction);
+ }
+
+ bytes32 actionHash = hashExpenditureAction(motion.action);
+ uint256 votePower = (add(motion.votes[NAY], motion.votes[YAY]) > 0) ?
+ motion.votes[YAY] : motion.stakes[YAY];
+
+ if (expenditurePastVotes[actionHash] < votePower) {
+ expenditurePastVotes[actionHash] = votePower;
+ } else {
+ canExecute = false;
+ }
+ }
+
+ bool executed;
+
+ if (canExecute) {
+ executed = executeCall(_motionId, motion.action);
+ require(executed || failingExecutionAllowed(_motionId), "voting-execution-failed-not-one-week");
+ }
+
+ emit MotionFinalized(_motionId, motion.action, executed);
+ }
+
+
+ /// @notice Return whether a motion, assuming it's in the finalizable state,
+ // is allowed to finalize without the call executing successfully.
+ /// @param _motionId The id of the motion
+ /// @dev We are only expecting this to be called from finalize motion in the contracts.
+ /// It is marked as public only so that the frontend can use it.
+ function failingExecutionAllowed(uint256 _motionId) public view returns (bool) {
+ Motion storage motion = motions[_motionId];
+ uint256 requiredStake = getRequiredStake(_motionId);
+
+ // Failing execution is allowed if we didn't fully stake, and it's been a week since staking ended
+ if (motion.stakes[YAY] < requiredStake || motion.stakes[NAY] < requiredStake) {
+ return block.timestamp >= motion.events[STAKE_END] + 7 days;
+ } else {
+ // It was fully staked, and went to a vote.
+ // Failing execution is also allowed if it's been a week since reveal ended
+ return block.timestamp >= motion.events[REVEAL_END] + 7 days;
+ }
+ }
+
+ /// @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
+ {
+ Motion storage motion = motions[_motionId];
+ require(
+ getMotionState(_motionId) == MotionState.Finalized ||
+ getMotionState(_motionId) == MotionState.Failed,
+ "voting-rep-motion-not-claimable"
+ );
+
+ (uint256 stakerReward, uint256 repPenalty) = getStakerReward(_motionId, _staker, _vote);
+
+ require(stakes[_motionId][_staker][_vote] > 0, "voting-rep-nothing-to-claim");
+ delete stakes[_motionId][_staker][_vote];
+
+ tokenLocking.transfer(token, stakerReward, _staker, true);
+
+ if (repPenalty > 0) {
+ colony.emitDomainReputationPenalty(
+ _permissionDomainId,
+ _childSkillIndex,
+ motion.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 number of ongoing motions for a single expenditure / expenditure slot
+ /// @param _structHash The hash of the expenditureId or expenditureId*expenditureSlot
+ /// @return The number of ongoing motions
+ function getExpenditureMotionCount(bytes32 _structHash) public view returns (uint256) {
+ return expenditureMotionCounts[_structHash];
+ }
+
+ /// @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 vote we can fall back on?
+ } else if (add(motion.votes[NAY], motion.votes[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 (
+ block.timestamp < motion.events[REVEAL_END] + escalationPeriod &&
+ motion.domainId > 1
+ ) {
+ 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 _voterRep The reputation the voter has in the domain
+ /// @return The voter reward
+ function getVoterReward(uint256 _motionId, uint256 _voterRep) public view returns (uint256) {
+ Motion storage motion = motions[_motionId];
+ uint256 fractionUserReputation = wdiv(_voterRep, motion.repSubmitted);
+ uint256 totalStake = add(motion.stakes[YAY], motion.stakes[NAY]);
+ return wmul(wmul(fractionUserReputation, totalStake), 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 _voterRep The reputation the voter has in the domain
+ /// @param _voterAddress The address the user will be voting as
+ /// @return The voter reward
+ function getVoterRewardRange(uint256 _motionId, uint256 _voterRep, address _voterAddress) public view returns (uint256, uint256) {
+ Motion storage motion = motions[_motionId];
+ // The minimum reward is when everyone has voted, with a total weight of motion.skillRep
+ uint256 minFractionUserReputation = wdiv(_voterRep, motion.skillRep);
+
+ // 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
+ uint256 voteTotal = motion.repSubmitted;
+ // Has the user already voted?
+ if (voteSecrets[_motionId][_voterAddress] == bytes32(0)) {
+ // They have not, so add their rep
+ voteTotal = add(voteTotal, _voterRep);
+ }
+ uint256 maxFractionUserReputation = wdiv(_voterRep, voteTotal);
+
+ uint256 totalStake = add(motion.stakes[YAY], motion.stakes[NAY]);
+ return (
+ wmul(wmul(minFractionUserReputation, totalStake), voterRewardFraction),
+ wmul(wmul(maxFractionUserReputation, totalStake), voterRewardFraction)
+ );
+ }
+
+ /// @notice Get the staker reward
+ /// @param _motionId The id of the motion
+ /// @param _staker The staker's address
+ /// @param _vote The vote (0 = NAY, 1 = YAY)
+ /// @return The staker reward and the reputation penalty (if any)
+ function getStakerReward(uint256 _motionId, address _staker, uint256 _vote) public view returns (uint256, uint256) {
+ Motion storage motion = motions[_motionId];
+
+ uint256 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;
+
+ // Went to a vote, use vote to determine reward or penalty
+ if (add(motion.votes[NAY], motion.votes[YAY]) > 0) {
+
+ uint256 loserStake;
+ uint256 winnerStake;
+ if (motion.votes[YAY] > motion.votes[NAY]){
+ loserStake = motion.stakes[NAY];
+ winnerStake = motion.stakes[YAY];
+ } else {
+ loserStake = motion.stakes[YAY];
+ winnerStake = motion.stakes[NAY];
+ }
+
+ loserStake = sub(loserStake, motion.paidVoterComp);
+ uint256 totalVotes = add(motion.votes[NAY], motion.votes[YAY]);
+ uint256 winFraction = wdiv(motion.votes[_vote], totalVotes);
+ uint256 winShare = wmul(winFraction, 2 * WAD); // On a scale of 0-2 WAD
+
+ if (winShare > WAD || (winShare == WAD && _vote == NAY)) {
+ // 50% gets 0% of loser's stake, 100% gets 100% of loser's stake, linear in between
+ stakerReward = wmul(stakeFraction, add(winnerStake, wmul(loserStake, winShare - WAD)));
+ } else {
+ stakerReward = wmul(stakeFraction, wmul(loserStake, winShare));
+ repPenalty = sub(realStake, stakerReward);
+ }
+
+ // Determine rewards based on stakes alone
+ } else {
+ assert(motion.paidVoterComp == 0);
+ 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);
+ }
+
+ // Internal functions
+
+ function getVoteSecret(bytes32 _salt, uint256 _vote) internal pure returns (bytes32) {
+ return keccak256(abi.encodePacked(_salt, _vote));
+ }
+
+ function getRequiredStake(uint256 _motionId) internal view returns (uint256) {
+ return wmul(motions[_motionId].skillRep, totalStakeFraction);
+ }
+
+ function getTarget(address _target) internal view returns (address) {
+ return (_target == address(0x0)) ? address(colony) : _target;
+ }
+
+ function flip(uint256 _vote) internal pure returns (uint256) {
+ return sub(1, _vote);
+ }
+
+ 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-rep-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-rep-invalid-colony-address");
+ require(keySkill == motions[_motionId].skillId, "voting-rep-invalid-skill-id");
+ require(keyUserAddress == _who, "voting-rep-invalid-user-address");
+
+ return reputationValue;
+ }
+
+ function getActionDomainSkillId(bytes memory _action) internal view returns (uint256) {
+ uint256 permissionDomainId;
+ uint256 childSkillIndex;
+
+ assembly {
+ permissionDomainId := mload(add(_action, 0x24))
+ childSkillIndex := mload(add(_action, 0x44))
+ }
+
+ uint256 permissionSkillId = colony.getDomain(permissionDomainId).skillId;
+ return colonyNetwork.getChildSkillId(permissionSkillId, childSkillIndex);
+ }
+
+ function executeCall(uint256 motionId, bytes memory action) internal returns (bool success) {
+ address to = getTarget(motions[motionId].altTarget);
+
+ assembly {
+ // call contract at address a with input mem[in…(in+insize))
+ // providing g gas and v wei and output area mem[out…(out+outsize))
+ // returning 0 on error (eg. out of gas) and 1 on success
+
+ // call(g, a, v, in, insize, out, outsize)
+ success := call(gas(), to, 0, add(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 hashExpenditureActionStruct(bytes memory action) internal returns (bytes32 hash) {
+ assert(getSig(action) == CHANGE_FUNCTION_SIG);
+
+ uint256 expenditureId;
+ uint256 storageSlot;
+ uint256 expenditureSlot;
+
+ assembly {
+ expenditureId := mload(add(action, 0x64))
+ storageSlot := mload(add(action, 0x84))
+ expenditureSlot := mload(add(action, 0x184))
+ }
+
+ if (storageSlot == 25) {
+ hash = keccak256(abi.encodePacked(expenditureId));
+ } else {
+ hash = keccak256(abi.encodePacked(expenditureId, expenditureSlot));
+ }
+ }
+
+ function createClaimDelayAction(bytes memory action, uint256 value)
+ public
+ returns (bytes memory)
+ {
+ // 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) {
+
+ 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), value)
+ 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 {
+
+ bytes memory slotClaimDelayAction = new bytes(4 + 32 * 13); // 420 bytes
+ uint256 expenditureSlot;
+
+ assembly {
+ expenditureSlot := mload(add(action, 0x184))
+
+ 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), value)
+ 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;
+
+ }
+ }
+}
diff --git a/migrations/9_setup_extensions.js b/migrations/9_setup_extensions.js
index fc14f05482..f376ece83a 100644
--- a/migrations/9_setup_extensions.js
+++ b/migrations/9_setup_extensions.js
@@ -10,8 +10,9 @@ const StakedExpenditure = artifacts.require("./StakedExpenditure");
const FundingQueue = artifacts.require("./FundingQueue");
const OneTxPayment = artifacts.require("./OneTxPayment");
const StreamingPayments = artifacts.require("./StreamingPayments");
-const TokenSupplier = artifacts.require("./TokenSupplier");
const VotingReputation = artifacts.require("./VotingReputation");
+const VotingReputationMisalignedRecovery = artifacts.require("./VotingReputationMisalignedRecovery");
+const TokenSupplier = artifacts.require("./TokenSupplier");
const Whitelist = artifacts.require("./Whitelist");
const Resolver = artifacts.require("./Resolver");
@@ -19,17 +20,21 @@ const EtherRouter = artifacts.require("./EtherRouter");
const IColonyNetwork = artifacts.require("./IColonyNetwork");
const IMetaColony = artifacts.require("./IMetaColony");
-async function addExtension(colonyNetwork, name, implementation) {
+async function addExtension(colonyNetwork, interfaceName, extensionName, implementations) {
const metaColonyAddress = await colonyNetwork.getMetaColony();
const metaColony = await IMetaColony.at(metaColonyAddress);
- const NAME_HASH = soliditySha3(name);
- const deployment = await implementation.new();
+ const NAME_HASH = soliditySha3(extensionName);
+ const deployments = await Promise.all(implementations.map((x) => x.new()));
const resolver = await Resolver.new();
- // Computed property names! Fancy!
- await setupEtherRouter(name, { [name]: deployment.address }, resolver);
+
+ const deployedImplementations = {};
+ for (let idx = 0; idx < implementations.length; idx += 1) {
+ deployedImplementations[implementations[idx].contractName] = deployments[idx].address;
+ }
+ await setupEtherRouter(interfaceName, deployedImplementations, resolver);
await metaColony.addExtensionToNetwork(NAME_HASH, resolver.address);
- console.log(`### ${name} extension installed`);
+ console.log(`### ${extensionName} extension installed`);
}
// eslint-disable-next-line no-unused-vars
@@ -37,13 +42,13 @@ module.exports = async function (deployer, network, accounts) {
const etherRouterDeployed = await EtherRouter.deployed();
const colonyNetwork = await IColonyNetwork.at(etherRouterDeployed.address);
- await addExtension(colonyNetwork, "CoinMachine", CoinMachine);
- await addExtension(colonyNetwork, "EvaluatedExpenditure", EvaluatedExpenditure);
- await addExtension(colonyNetwork, "StakedExpenditure", StakedExpenditure);
- await addExtension(colonyNetwork, "FundingQueue", FundingQueue);
- await addExtension(colonyNetwork, "OneTxPayment", OneTxPayment);
- await addExtension(colonyNetwork, "StreamingPayments", StreamingPayments);
- await addExtension(colonyNetwork, "TokenSupplier", TokenSupplier);
- await addExtension(colonyNetwork, "VotingReputation", VotingReputation);
- await addExtension(colonyNetwork, "Whitelist", Whitelist);
+ await addExtension(colonyNetwork, "CoinMachine", "CoinMachine", [CoinMachine]);
+ await addExtension(colonyNetwork, "EvaluatedExpenditure", "EvaluatedExpenditure", [EvaluatedExpenditure]);
+ await addExtension(colonyNetwork, "StakedExpenditure", "StakedExpenditure", [StakedExpenditure]);
+ await addExtension(colonyNetwork, "FundingQueue", "FundingQueue", [FundingQueue]);
+ await addExtension(colonyNetwork, "OneTxPayment", "OneTxPayment", [OneTxPayment]);
+ await addExtension(colonyNetwork, "StreamingPayments", "StreamingPayments", [StreamingPayments]);
+ await addExtension(colonyNetwork, "TokenSupplier", "TokenSupplier", [TokenSupplier]);
+ await addExtension(colonyNetwork, "IVotingReputation", "VotingReputation", [VotingReputation, VotingReputationMisalignedRecovery]);
+ await addExtension(colonyNetwork, "Whitelist", "Whitelist", [Whitelist]);
};
diff --git a/package.json b/package.json
index fe66dfd5e0..55210da1d4 100644
--- a/package.json
+++ b/package.json
@@ -35,6 +35,7 @@
"test:contracts:chainid:coverage": "SOLIDITY_COVERAGE=1 truffle run coverage --solcoverjs ./.solcover.chainid.js --network coverage --temp build --file='./test-chainid/**/*'",
"test:contracts:upgrade:parity": "npm run start:blockchain:client parity & npm run generate:test:contracts && truffle migrate --reset --compile-all && truffle test ./test-upgrade/* --network integration",
"test:contracts:upgrade:ganache": "npm run start:blockchain:client & npm run generate:test:contracts && truffle migrate --reset --compile-all && truffle test ./test-upgrade/* --network development",
+ "test:contracts:upgrade:coverage": "npm run generate:test:contracts && SOLIDITY_COVERAGE=1 truffle run coverage --solcoverjs ./.solcover.upgrade.js --network coverage --temp build --file='./test-upgrade/*'",
"test:contracts:gasCosts": "npm run start:blockchain:client & truffle migrate --reset --compile-all && truffle test test-gas-costs/gasCosts.js --network development",
"test:contracts:patricia": "npm run start:blockchain:client parity & truffle migrate --reset --compile-all && truffle test packages/reputation-miner/patricia-test.js --network development",
"test:contracts:coverage": "SOLIDITY_COVERAGE=1 truffle run coverage --network coverage --temp build --file='./test/contracts-network/*'",
diff --git a/scripts/check-recovery.js b/scripts/check-recovery.js
index 5ffffa4ce4..430453f46b 100755
--- a/scripts/check-recovery.js
+++ b/scripts/check-recovery.js
@@ -44,6 +44,7 @@ walkSync("./contracts/").forEach((contractName) => {
"contracts/common/TokenAuthority.sol", // Imported from colonyToken repo
"contracts/ens/ENS.sol",
"contracts/ens/ENSRegistry.sol",
+ "contracts/extensions/IColonyExtension.sol",
"contracts/extensions/CoinMachine.sol",
"contracts/extensions/ColonyExtension.sol",
"contracts/extensions/ColonyExtensionMeta.sol",
@@ -53,7 +54,9 @@ walkSync("./contracts/").forEach((contractName) => {
"contracts/extensions/OneTxPayment.sol",
"contracts/extensions/StreamingPayments.sol",
"contracts/extensions/TokenSupplier.sol",
- "contracts/extensions/VotingReputation.sol",
+ "contracts/extensions/votingReputation/VotingReputation.sol",
+ "contracts/extensions/votingReputation/VotingReputationMisalignedRecovery.sol",
+ "contracts/extensions/votingReputation/IVotingReputation.sol",
"contracts/extensions/Whitelist.sol",
"contracts/gnosis/MultiSigWallet.sol",
"contracts/patriciaTree/Bits.sol",
@@ -78,6 +81,7 @@ walkSync("./contracts/").forEach((contractName) => {
"contracts/testHelpers/TestExtensions.sol",
"contracts/testHelpers/TransferTest.sol",
"contracts/testHelpers/RequireExecuteCall.sol",
+ "contracts/testHelpers/VotingReputationMisaligned.sol",
"contracts/tokenLocking/ITokenLocking.sol",
"contracts/tokenLocking/TokenLocking.sol",
"contracts/tokenLocking/TokenLockingStorage.sol",
diff --git a/scripts/check-storage.js b/scripts/check-storage.js
index 583b0e6b55..99fa0bd7f7 100755
--- a/scripts/check-storage.js
+++ b/scripts/check-storage.js
@@ -36,7 +36,8 @@ walkSync("./contracts/").forEach((contractName) => {
"contracts/extensions/OneTxPayment.sol",
"contracts/extensions/StreamingPayments.sol",
"contracts/extensions/TokenSupplier.sol",
- "contracts/extensions/VotingReputation.sol",
+ "contracts/extensions/votingReputation/VotingReputationMisalignedRecovery.sol",
+ "contracts/extensions/votingReputation/VotingReputation.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
@@ -44,6 +45,7 @@ walkSync("./contracts/").forEach((contractName) => {
"contracts/testHelpers/ToggleableToken.sol",
"contracts/testHelpers/TestExtensions.sol",
"contracts/testHelpers/GasGuzzler.sol",
+ "contracts/testHelpers/VotingReputationMisaligned.sol",
"contracts/tokenLocking/TokenLockingStorage.sol",
"contracts/Migrations.sol",
"contracts/Token.sol", // Imported from colonyToken repo
diff --git a/scripts/generate-test-contracts.sh b/scripts/generate-test-contracts.sh
index a47a74352c..9bc5490b94 100644
--- a/scripts/generate-test-contracts.sh
+++ b/scripts/generate-test-contracts.sh
@@ -39,3 +39,10 @@ sed -i.bak "s| is ReputationMiningCycleCommon {| is ReputationMiningCycleCommon
# Modify IReputationMiningCycle contract
sed -i.bak "s/interface IReputationMiningCycle/interface IUpdatedReputationMiningCycle/g" ./contracts/reputationMiningCycle/IUpdatedReputationMiningCycle.sol
sed -i.bak "s/function resetWindow() public;/function resetWindow() public; function isUpdated() public pure returns(bool);/g" ./contracts/reputationMiningCycle/IUpdatedReputationMiningCycle.sol
+# Modify VotingReputationMisaligned to have the correct version
+
+votingVersion="$(grep 'return [0-9]*;' ./contracts/extensions/votingReputation/VotingReputation.sol | sed 's/ return //' | sed 's/;//')"
+echo "Current Voting contract version is $votingVersion"
+previous_version=$(($votingVersion - 1))
+echo "Updating test contract to $previous_version"
+sed -i.bak "s/return 4/return $previous_version/g" ./contracts/testHelpers/VotingReputationMisaligned.sol
diff --git a/scripts/versioningCheck.sh b/scripts/versioningCheck.sh
index 692531f9a8..597fd8b9c0 100644
--- a/scripts/versioningCheck.sh
+++ b/scripts/versioningCheck.sh
@@ -42,10 +42,26 @@ do
continue
fi
+ if [ $file = "contracts/extensions/IColonyExtension.sol" ]; then
+ continue
+ fi
+
if [ $file = "contracts/extensions/ColonyExtensionMeta.sol" ]; then
continue
fi
+ if [ $file = "contracts/extensions/votingReputation/VotingReputationDataTypes.sol" ]; then
+ continue
+ fi
+
+ if [ $file = "contracts/extensions/votingReputation/VotingReputationMisalignedRecovery.sol" ]; then
+ continue
+ fi
+
+ if [ $file = "contracts/extensions/votingReputation/IVotingReputation.sol" ]; then
+ continue
+ fi
+
if git show $LATEST_RELEASE:$file > /dev/null 2>&1 ; then
oldVersion="$(version_from_commit_extensions $LATEST_RELEASE $file)"
diff --git a/test-gas-costs/gasCosts.js b/test-gas-costs/gasCosts.js
index 6a02ebf7ea..07db097d0a 100644
--- a/test-gas-costs/gasCosts.js
+++ b/test-gas-costs/gasCosts.js
@@ -267,6 +267,7 @@ contract("All", function (accounts) {
client2: { respondToChallenge: "colony-reputation-mining-increased-reputation-value-incorrect" },
});
const repCycle = await getActiveRepCycle(colonyNetwork);
+
await forwardTime(CHALLENGE_RESPONSE_WINDOW_DURATION + 1, this);
await repCycle.confirmNewHash(2, { from: STAKER1 });
diff --git a/test-smoke/colony-storage-consistent.js b/test-smoke/colony-storage-consistent.js
index 8617c00ff6..76369a53c4 100644
--- a/test-smoke/colony-storage-consistent.js
+++ b/test-smoke/colony-storage-consistent.js
@@ -149,8 +149,8 @@ contract("Contract Storage", (accounts) => {
console.log("miningCycleStateHash:", miningCycleStateHash);
console.log("tokenLockingStateHash:", tokenLockingStateHash);
- expect(colonyNetworkStateHash).to.equal("0x225612c3bf05b92dddf480ee2d44e2a71400b4eab1fa2ddf5c99245ee3952988");
- expect(colonyStateHash).to.equal("0x3461a9d484a0f29eed70f61c5d5b8e7b0805dbb2c785876196f7e09c3f6241c0");
+ expect(colonyNetworkStateHash).to.equal("0xd274dba9b9c02954cf11a9f6b968560bf03bb7f6a3c2b6844409a9dbf8fb93c2");
+ expect(colonyStateHash).to.equal("0xa49d332bbdd1951f062b1ffc40bbeb7b3a0a16fd2cd1879fca8348eda7b5b587");
expect(metaColonyStateHash).to.equal("0xff23657f917385e6a94f328907443fef625f08b8b3224e065a53b690f91be0bb");
expect(miningCycleStateHash).to.equal("0x264d4a83e21fef92f687f9fabacae9370966b0b30ebc15307653c4c3d33a0035");
expect(tokenLockingStateHash).to.equal("0x983a56a52582ce548e98659e15a9baa5387886fcb0ac1185dbd746dfabf00338");
diff --git a/test-upgrade/voting-reputation-misalignment-upgrade.js b/test-upgrade/voting-reputation-misalignment-upgrade.js
new file mode 100644
index 0000000000..47a2a92a16
--- /dev/null
+++ b/test-upgrade/voting-reputation-misalignment-upgrade.js
@@ -0,0 +1,441 @@
+/* globals artifacts */
+
+const chai = require("chai");
+const bnChai = require("bn-chai");
+const ethers = require("ethers");
+const shortid = require("shortid");
+const { soliditySha3 } = require("web3-utils");
+const { makeReputationKey, makeReputationValue, getActiveRepCycle, forwardTime, encodeTxData, checkErrorRevert } = require("../helpers/test-helper");
+const { UINT256_MAX, WAD, MINING_CYCLE_DURATION, SECONDS_PER_DAY, CHALLENGE_RESPONSE_WINDOW_DURATION } = require("../helpers/constants");
+const { setupRandomColony } = require("../helpers/test-data-generator");
+
+const IColonyNetwork = artifacts.require("IColonyNetwork");
+const IMetaColony = artifacts.require("IMetaColony");
+const EtherRouter = artifacts.require("EtherRouter");
+const Resolver = artifacts.require("Resolver");
+const VotingReputationMisaligned = artifacts.require("VotingReputationMisaligned");
+const { setupEtherRouter } = require("../helpers/upgradable-contracts");
+const PatriciaTree = require("../packages/reputation-miner/patricia");
+
+const { expect } = chai;
+chai.use(bnChai(web3.utils.BN));
+
+const TokenLocking = artifacts.require("TokenLocking");
+const IVotingReputation = artifacts.require("IVotingReputation");
+
+const VOTING_REPUTATION = soliditySha3("VotingReputation");
+
+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 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);
+contract("Voting Reputation Misalignment upgrade", (accounts) => {
+ const USER0 = accounts[0];
+ const USER1 = accounts[1];
+ const USER2 = accounts[2];
+ const MINER = accounts[5];
+
+ let colony;
+ let token;
+ let domain1;
+ let domain2;
+ let domain3;
+ let metaColony;
+ let colonyNetwork;
+ let tokenLocking;
+
+ 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;
+ let badVersion;
+ const NAME_HASH = soliditySha3("VotingReputation");
+
+ const SALT = soliditySha3({ type: "string", value: shortid.generate() });
+
+ before(async function () {
+ const etherRouterColonyNetwork = await EtherRouter.deployed();
+ colonyNetwork = await IColonyNetwork.at(etherRouterColonyNetwork.address);
+ const metaColonyAddress = await colonyNetwork.getMetaColony();
+ metaColony = await IMetaColony.at(metaColonyAddress);
+
+ const badImplementation = await VotingReputationMisaligned.new();
+
+ badVersion = await badImplementation.version();
+ const badResolver = await Resolver.new();
+
+ await setupEtherRouter("VotingReputationMisaligned", { VotingReputationMisaligned: badImplementation.address }, badResolver);
+ await metaColony.addExtensionToNetwork(NAME_HASH, badResolver.address);
+
+ const tokenLockingAddress = await colonyNetwork.getTokenLocking();
+ tokenLocking = await TokenLocking.at(tokenLockingAddress);
+ });
+
+ beforeEach(async function () {
+ // Install previous in a new colony
+ ({ colony, token } = await setupRandomColony(colonyNetwork));
+ await colony.installExtension(NAME_HASH, badVersion);
+
+ // 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);
+
+ const votingAddress = await colonyNetwork.getExtensionInstallation(VOTING_REPUTATION, colony.address);
+ voting = await IVotingReputation.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(CHALLENGE_RESPONSE_WINDOW_DURATION + 1, this);
+ await repCycle.confirmNewHash(0, { from: MINER });
+ });
+
+ describe("when upgrading voting reputation contract", function () {
+ it("can reclaim stake from ghost motion", async function () {
+ 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 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);
+
+ let count = await voting.getMotionCount();
+ expect(count).to.eq.BN(1);
+ // Upgrade
+
+ await colony.upgradeExtension(VOTING_REPUTATION, badVersion.toNumber() + 1);
+
+ count = await voting.getMotionCount();
+ expect(count).to.eq.BN(1);
+ // Reclaim ghost stakes.
+
+ await voting.claimMisalignedReward(1, 1, UINT256_MAX, USER0, YAY);
+ await voting.claimMisalignedReward(1, 1, UINT256_MAX, USER1, YAY);
+ });
+
+ it("can reclaim stake from ghost motion that was finalized after going to a vote", async function () {
+ 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();
+
+ 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(STAKE_PERIOD, this);
+
+ const { logs } = await voting.finalizeMotion(motionId);
+ expect(logs[0].args.executed).to.be.true;
+
+ let count = await voting.getMotionCount();
+ expect(count).to.eq.BN(1);
+ // Upgrade
+
+ await colony.upgradeExtension(VOTING_REPUTATION, badVersion.toNumber() + 1);
+
+ count = await voting.getMotionCount();
+ expect(count).to.eq.BN(1);
+ // Reclaim ghost stakes.
+
+ await voting.claimMisalignedReward(1, 1, UINT256_MAX, USER0, YAY);
+ await voting.claimMisalignedReward(1, 1, UINT256_MAX, USER1, NAY);
+ });
+
+ it("can reclaim stake from ghost motion that was finalized, but didn't execute due to vote decision after going to a vote", async function () {
+ 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();
+
+ 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;
+
+ let count = await voting.getMotionCount();
+ expect(count).to.eq.BN(1);
+ // Upgrade
+
+ await colony.upgradeExtension(VOTING_REPUTATION, badVersion.toNumber() + 1);
+
+ count = await voting.getMotionCount();
+ expect(count).to.eq.BN(1);
+ // Reclaim ghost stakes.
+
+ await voting.claimMisalignedReward(1, 1, UINT256_MAX, USER0, YAY);
+ await voting.claimMisalignedReward(1, 1, UINT256_MAX, USER1, NAY);
+ });
+
+ it("can reclaim stake from ghost motion that was finalized after being staked but not voted on", async function () {
+ 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 half = 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, half, 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;
+
+ let count = await voting.getMotionCount();
+ expect(count).to.eq.BN(1);
+ // Upgrade
+
+ await colony.upgradeExtension(VOTING_REPUTATION, badVersion.toNumber() + 1);
+
+ count = await voting.getMotionCount();
+ expect(count).to.eq.BN(1);
+ // Reclaim ghost stakes.
+
+ await voting.claimMisalignedReward(1, 1, UINT256_MAX, USER0, YAY);
+ await voting.claimMisalignedReward(1, 1, UINT256_MAX, USER1, NAY);
+ });
+
+ it("can reclaim stake from ghost motion that could have been finalized, but wasn't", async function () {
+ 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();
+
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+
+ await forwardTime(STAKE_PERIOD, this);
+
+ let count = await voting.getMotionCount();
+ expect(count).to.eq.BN(1);
+ // Upgrade
+
+ await colony.upgradeExtension(VOTING_REPUTATION, badVersion.toNumber() + 1);
+
+ count = await voting.getMotionCount();
+ expect(count).to.eq.BN(1);
+ // Reclaim ghost stakes.
+
+ await voting.claimMisalignedReward(1, 1, UINT256_MAX, USER0, YAY);
+
+ // Can't reclaim twice
+ await checkErrorRevert(voting.claimMisalignedReward(1, 1, UINT256_MAX, USER0, YAY), "voting-rep-nothing-to-claim");
+ });
+
+ it("can reclaim stake from ghost motion that never was staked fully", async function () {
+ 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 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);
+
+ let count = await voting.getMotionCount();
+ expect(count).to.eq.BN(1);
+ // Upgrade
+
+ await colony.upgradeExtension(VOTING_REPUTATION, badVersion.toNumber() + 1);
+
+ count = await voting.getMotionCount();
+ expect(count).to.eq.BN(1);
+ // Reclaim ghost stakes.
+
+ await voting.claimMisalignedReward(1, 1, UINT256_MAX, USER0, YAY);
+ await voting.claimMisalignedReward(1, 1, UINT256_MAX, USER1, NAY);
+ });
+
+ it("can reclaim stake from ghost motion that was staked fully, no-one voted on, and was then finalized", async function () {
+ 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();
+
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 });
+ await voting.stakeMotion(motionId, 1, UINT256_MAX, NAY, REQUIRED_STAKE, user1Key, user1Value, user1Mask, user1Siblings, { from: USER1 });
+
+ await forwardTime(STAKE_PERIOD, this);
+ await forwardTime(SUBMIT_PERIOD, this);
+ await forwardTime(REVEAL_PERIOD, this);
+
+ const { logs } = await voting.finalizeMotion(motionId);
+ expect(logs[0].args.executed).to.be.false;
+
+ let count = await voting.getMotionCount();
+ expect(count).to.eq.BN(1);
+ // Upgrade
+
+ await colony.upgradeExtension(VOTING_REPUTATION, badVersion.toNumber() + 1);
+
+ count = await voting.getMotionCount();
+ expect(count).to.eq.BN(1);
+ // Reclaim ghost stakes.
+
+ await voting.claimMisalignedReward(1, 1, UINT256_MAX, USER0, YAY);
+ await voting.claimMisalignedReward(1, 1, UINT256_MAX, USER1, NAY);
+ });
+
+ it("metatransaction nonces skip a million", async function () {
+ let nonce = await voting.getMetatransactionNonce(USER0);
+ expect(nonce).to.eq.BN(0);
+ await colony.upgradeExtension(VOTING_REPUTATION, badVersion.toNumber() + 1);
+ nonce = await voting.getMetatransactionNonce(USER0);
+ expect(nonce).to.eq.BN(1000000);
+ });
+ });
+});
diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js
index 841199aaac..ec86de6c51 100644
--- a/test/extensions/voting-rep.js
+++ b/test/extensions/voting-rep.js
@@ -40,6 +40,7 @@ const EtherRouter = artifacts.require("EtherRouter");
const IReputationMiningCycle = artifacts.require("IReputationMiningCycle");
const Token = artifacts.require("Token");
const TokenLocking = artifacts.require("TokenLocking");
+const IVotingReputation = artifacts.require("VotingReputation");
const VotingReputation = artifacts.require("VotingReputation");
const OneTxPayment = artifacts.require("OneTxPayment");
@@ -140,7 +141,7 @@ contract("Voting Reputation", (accounts) => {
await colony.installExtension(VOTING_REPUTATION, version);
const votingAddress = await colonyNetwork.getExtensionInstallation(VOTING_REPUTATION, colony.address);
- voting = await VotingReputation.at(votingAddress);
+ voting = await IVotingReputation.at(votingAddress);
await voting.initialise(
TOTAL_STAKE_FRACTION,