diff --git a/contracts/colony/Colony.sol b/contracts/colony/Colony.sol index f8d5a43785..9659b1dade 100755 --- a/contracts/colony/Colony.sol +++ b/contracts/colony/Colony.sol @@ -309,14 +309,11 @@ contract Colony is BasicMetaTransaction, ColonyStorage, PatriciaTreeProofs { // v9 to v10 function finishUpgrade() public always { - // Leaving in as an example of what this function usually does. - - // ColonyAuthority colonyAuthority = ColonyAuthority(address(authority)); - // bytes4 sig; - - // sig = bytes4(keccak256("addLocalSkill()")); - // colonyAuthority.setRoleCapability(uint8(ColonyRole.Root), address(this), sig, true); + ColonyAuthority colonyAuthority = ColonyAuthority(address(authority)); + bytes4 sig; + sig = bytes4(keccak256("setExpenditurePayout(uint256,uint256,uint256,uint256,address,uint256)")); + colonyAuthority.setRoleCapability(uint8(ColonyRole.Arbitration), address(this), sig, true); } function getMetatransactionNonce(address _user) override public view returns (uint256 nonce){ diff --git a/contracts/colony/ColonyAuthority.sol b/contracts/colony/ColonyAuthority.sol index 8e8282ff20..e62d3b2cf0 100644 --- a/contracts/colony/ColonyAuthority.sol +++ b/contracts/colony/ColonyAuthority.sol @@ -119,11 +119,14 @@ contract ColonyAuthority is CommonAuthority { addRoleCapability(ROOT_ROLE, "setDefaultGlobalClaimDelay(uint256)"); addRoleCapability(ARBITRATION_ROLE, "setExpenditureMetadata(uint256,uint256,uint256,string)"); - // Added in colony v9 (f-lwss) + // Added in colony v9 (fuschia-lwss) addRoleCapability(ROOT_ROLE, "addLocalSkill()"); addRoleCapability(ROOT_ROLE, "deprecateLocalSkill(uint256,bool)"); addRoleCapability(ARCHITECTURE_ROLE, "deprecateDomain(uint256,uint256,uint256,bool)"); addRoleCapability(ROOT_ROLE, "editColonyByDelta(string)"); + + // Added in colony v10 (ginger-lwss) + addRoleCapability(ARBITRATION_ROLE, "setExpenditurePayout(uint256,uint256,uint256,uint256,address,uint256)"); } function addRoleCapability(uint8 role, bytes memory sig) private { diff --git a/contracts/colony/ColonyDataTypes.sol b/contracts/colony/ColonyDataTypes.sol index 3921f453eb..c12dfc0919 100755 --- a/contracts/colony/ColonyDataTypes.sol +++ b/contracts/colony/ColonyDataTypes.sol @@ -157,6 +157,22 @@ interface ColonyDataTypes { /// @param payoutModifier The payout modifier for the slot event ExpenditurePayoutModifierSet(address agent, uint256 indexed expenditureId, uint256 indexed slot, int256 payoutModifier); + /// @notice Event logged when an expenditure slot payout modifier changes + /// @param agent The address that is responsible for triggering this event + /// @param expenditureId Id of the expenditure + /// @param storageSlot Initial storage slot being set (expenditures or expenditureSlots) + /// @param mask Mask indicating whether we are making mapping or array operations + /// @param keys Values used to construct final slot via mapping or array operations + /// @param value Value being set in the slot + event ExpenditureStateChanged( + address agent, + uint256 indexed expenditureId, + uint256 indexed storageSlot, + bool[] mask, + bytes32[] keys, + bytes32 value + ); + /// @notice Event logged when a new payment is added /// @param agent The address that is responsible for triggering this event /// @param paymentId The newly added payment id diff --git a/contracts/colony/ColonyExpenditure.sol b/contracts/colony/ColonyExpenditure.sol index 2dc5bc5c10..fa1ca15fab 100644 --- a/contracts/colony/ColonyExpenditure.sol +++ b/contracts/colony/ColonyExpenditure.sol @@ -68,7 +68,6 @@ contract ColonyExpenditure is ColonyStorage { function transferExpenditure(uint256 _id, address _newOwner) public stoppable - expenditureExists(_id) expenditureDraftOrLocked(_id) expenditureOnlyOwner(_id) { @@ -86,7 +85,6 @@ contract ColonyExpenditure is ColonyStorage { ) public stoppable - expenditureExists(_id) expenditureDraftOrLocked(_id) authDomain(_permissionDomainId, _childSkillIndex, expenditures[_id].domainId) { @@ -98,7 +96,6 @@ contract ColonyExpenditure is ColonyStorage { function cancelExpenditure(uint256 _id) public stoppable - expenditureExists(_id) expenditureDraft(_id) expenditureOnlyOwner(_id) { @@ -110,7 +107,6 @@ contract ColonyExpenditure is ColonyStorage { function lockExpenditure(uint256 _id) public stoppable - expenditureExists(_id) expenditureDraft(_id) expenditureOnlyOwner(_id) { @@ -122,7 +118,6 @@ contract ColonyExpenditure is ColonyStorage { function finalizeExpenditure(uint256 _id) public stoppable - expenditureExists(_id) expenditureDraftOrLocked(_id) expenditureOnlyOwner(_id) { @@ -138,7 +133,6 @@ contract ColonyExpenditure is ColonyStorage { function setExpenditureMetadata(uint256 _id, string memory _metadata) public stoppable - expenditureExists(_id) expenditureDraft(_id) expenditureOnlyOwner(_id) { @@ -153,7 +147,7 @@ contract ColonyExpenditure is ColonyStorage { ) public stoppable - expenditureExists(_id) + validExpenditure(_id) authDomain(_permissionDomainId, _childSkillIndex, expenditures[_id].domainId) { emit ExpenditureMetadataSet(msgSender(), _id, _metadata); @@ -162,7 +156,6 @@ contract ColonyExpenditure is ColonyStorage { function setExpenditureRecipients(uint256 _id, uint256[] memory _slots, address payable[] memory _recipients) public stoppable - expenditureExists(_id) expenditureDraft(_id) expenditureOnlyOwner(_id) { @@ -178,7 +171,6 @@ contract ColonyExpenditure is ColonyStorage { function setExpenditureSkills(uint256 _id, uint256[] memory _slots, uint256[] memory _skillIds) public stoppable - expenditureExists(_id) expenditureDraft(_id) expenditureOnlyOwner(_id) { @@ -201,7 +193,6 @@ contract ColonyExpenditure is ColonyStorage { function setExpenditureClaimDelays(uint256 _id, uint256[] memory _slots, uint256[] memory _claimDelays) public stoppable - expenditureExists(_id) expenditureDraft(_id) expenditureOnlyOwner(_id) { @@ -217,7 +208,6 @@ contract ColonyExpenditure is ColonyStorage { function setExpenditurePayoutModifiers(uint256 _id, uint256[] memory _slots, int256[] memory _payoutModifiers) public stoppable - expenditureExists(_id) expenditureDraft(_id) expenditureOnlyOwner(_id) { @@ -230,6 +220,32 @@ contract ColonyExpenditure is ColonyStorage { } } + function setExpenditureValues( + uint256 _id, + uint256[] memory _recipientSlots, + address payable[] memory _recipients, + uint256[] memory _skillIdSlots, + uint256[] memory _skillIds, + uint256[] memory _claimDelaySlots, + uint256[] memory _claimDelays, + uint256[] memory _payoutModifierSlots, + int256[] memory _payoutModifiers, + address[] memory _payoutTokens, + uint256[][] memory _payoutSlots, + uint256[][] memory _payoutValues + ) + public + stoppable + expenditureDraft(_id) + expenditureOnlyOwner(_id) + { + if (_recipients.length > 0) { setExpenditureRecipients(_id, _recipientSlots, _recipients); } + if (_skillIds.length > 0) { setExpenditureSkills(_id, _skillIdSlots, _skillIds); } + if (_claimDelays.length > 0) { setExpenditureClaimDelays(_id, _claimDelaySlots, _claimDelays); } + if (_payoutModifiers.length > 0) { setExpenditurePayoutModifiers(_id, _payoutModifierSlots, _payoutModifiers); } + if (_payoutTokens.length > 0) { setExpenditurePayouts(_id, _payoutTokens, _payoutSlots, _payoutValues); } + } + // Deprecated function setExpenditureRecipient(uint256 _id, uint256 _slot, address payable _recipient) public @@ -268,7 +284,6 @@ contract ColonyExpenditure is ColonyStorage { uint256 constant EXPENDITURES_SLOT = 25; uint256 constant EXPENDITURESLOTS_SLOT = 26; - uint256 constant EXPENDITURESLOTPAYOUTS_SLOT = 27; function setExpenditureState( uint256 _permissionDomainId, @@ -281,10 +296,11 @@ contract ColonyExpenditure is ColonyStorage { ) public stoppable - expenditureExists(_id) + validExpenditure(_id) authDomain(_permissionDomainId, _childSkillIndex, expenditures[_id].domainId) { - // Only allow editing expenditure status, owner, and finalizedTimestamp + // Only allow editing expenditure status, owner, finalizedTimestamp, and globalClaimDelay + // Do not allow editing of fundingPotId or domainId // Note that status + owner occupy one slot if (_storageSlot == EXPENDITURES_SLOT) { require(_keys.length == 1, "colony-expenditure-bad-keys"); @@ -306,15 +322,13 @@ contract ColonyExpenditure is ColonyStorage { ); } - // Should always be two mappings - } else if (_storageSlot == EXPENDITURESLOTPAYOUTS_SLOT) { - require(_keys.length == 2, "colony-expenditure-bad-keys"); - } else { require(false, "colony-expenditure-bad-slot"); } executeStateChange(keccak256(abi.encode(_id, _storageSlot)), _mask, _keys, _value); + + emit ExpenditureStateChanged(msgSender(), _id, _storageSlot, _mask, _keys, _value); } // Public view functions @@ -337,6 +351,28 @@ contract ColonyExpenditure is ColonyStorage { // Internal functions + // Used to avoid stack error in setExpenditureValues + function setExpenditurePayouts( + uint256 _id, + address[] memory _tokens, + uint256[][] memory _slots, + uint256[][] memory _values + ) + internal + { + for (uint256 i; i < _tokens.length; i++) { + (bool success, bytes memory returndata) = address(this).delegatecall( + abi.encodeWithSignature("setExpenditurePayouts(uint256,uint256[],address,uint256[])", _id, _slots[i], _tokens[i], _values[i]) + ); + if (!success) { + if (returndata.length == 0) revert(); + assembly { + revert(add(32, returndata), mload(returndata)) + } + } + } + } + bool constant MAPPING = false; bool constant ARRAY = true; uint256 constant MAX_ARRAY = 1024; // Prevent writing arbitrary slots diff --git a/contracts/colony/ColonyFunding.sol b/contracts/colony/ColonyFunding.sol index 5ddbb5face..3dc5d506b0 100755 --- a/contracts/colony/ColonyFunding.sol +++ b/contracts/colony/ColonyFunding.sol @@ -26,7 +26,7 @@ contract ColonyFunding is ColonyStorage { // ignore-swc-123 // Public - function moveFundsBetweenPots( + function moveFundsBetweenPots( uint256 _permissionDomainId, uint256 _childSkillIndex, uint256 _domainId, @@ -43,8 +43,8 @@ contract ColonyFunding is ColonyStorage { // ignore-swc-123 authDomain(_permissionDomainId, _childSkillIndex, _domainId) validFundingTransfer(_fromPot, _toPot) { - require(validateDomainInheritance(_domainId, _fromChildSkillIndex, getDomainFromFundingPot(_fromPot)), "colony-invalid-domain-inheritence"); - require(validateDomainInheritance(_domainId, _toChildSkillIndex, getDomainFromFundingPot(_toPot)), "colony-invalid-domain-inheritence"); + require(validateDomainInheritance(_domainId, _fromChildSkillIndex, getDomainFromFundingPot(_fromPot)), "colony-invalid-domain-inheritance"); + require(validateDomainInheritance(_domainId, _toChildSkillIndex, getDomainFromFundingPot(_toPot)), "colony-invalid-domain-inheritance"); moveFundsBetweenPotsFunctionality(_fromPot, _toPot, _amount, _token); } @@ -138,25 +138,54 @@ contract ColonyFunding is ColonyStorage { // ignore-swc-123 } } - function setExpenditurePayouts(uint256 _id, uint256[] memory _slots, address _token, uint256[] memory _amounts) + /// @notice For owners to update payouts with one token and many slots + function setExpenditurePayouts( + uint256 _id, + uint256[] memory _slots, + address _token, + uint256[] memory _amounts + ) public stoppable - expenditureExists(_id) expenditureDraft(_id) expenditureOnlyOwner(_id) { setExpenditurePayoutsInternal(_id, _slots, _token, _amounts); } + /// @notice For arbitrators to update payouts with one token and one slot + function setExpenditurePayout( + uint256 _permissionDomainId, + uint256 _childSkillIndex, + uint256 _id, + uint256 _slot, + address _token, + uint256 _amount + ) + public + stoppable + validExpenditure(_id) + authDomain(_permissionDomainId, _childSkillIndex, expenditures[_id].domainId) + { + uint256[] memory slots = new uint256[](1); + slots[0] = _slot; + uint256[] memory amounts = new uint256[](1); + amounts[0] = _amount; + setExpenditurePayoutsInternal(_id, slots, _token, amounts); + } + + /// @notice For owners to update payouts with one token and one slot function setExpenditurePayout(uint256 _id, uint256 _slot, address _token, uint256 _amount) public stoppable + expenditureDraft(_id) + expenditureOnlyOwner(_id) { uint256[] memory slots = new uint256[](1); slots[0] = _slot; uint256[] memory amounts = new uint256[](1); amounts[0] = _amount; - setExpenditurePayouts(_id, slots, _token, amounts); + setExpenditurePayoutsInternal(_id, slots, _token, amounts); } int256 constant MAX_PAYOUT_MODIFIER = int256(WAD); @@ -164,7 +193,6 @@ contract ColonyFunding is ColonyStorage { // ignore-swc-123 function claimExpenditurePayout(uint256 _id, uint256 _slot, address _token) public stoppable - expenditureExists(_id) expenditureFinalized(_id) { Expenditure storage expenditure = expenditures[_id]; @@ -188,12 +216,9 @@ contract ColonyFunding is ColonyStorage { // ignore-swc-123 uint256 tokenPayout = min(initialPayout, repPayout); uint256 tokenSurplus = sub(initialPayout, tokenPayout); - // Send any surplus back to the domain (for payoutScalars < 1) + // Deduct any surplus from the outstanding payouts (for payoutScalars < 1) if (tokenSurplus > 0) { fundingPot.payouts[_token] = sub(fundingPot.payouts[_token], tokenSurplus); - fundingPot.balance[_token] = sub(fundingPot.balance[_token], tokenSurplus); - FundingPot storage domainFundingPot = fundingPots[domains[expenditure.domainId].fundingPotId]; - domainFundingPot.balance[_token] = add(domainFundingPot.balance[_token], tokenSurplus); } // Process reputation updates if internal token @@ -420,6 +445,8 @@ contract ColonyFunding is ColonyStorage { // ignore-swc-123 } function processPayout(uint256 _fundingPotId, address _token, uint256 _payout, address payable _user) private { + refundDomain(_fundingPotId, _token); + IColonyNetwork colonyNetworkContract = IColonyNetwork(colonyNetworkAddress); address payable metaColonyAddress = colonyNetworkContract.getMetaColony(); @@ -450,4 +477,13 @@ contract ColonyFunding is ColonyStorage { // ignore-swc-123 emit PayoutClaimed(msgSender(), _fundingPotId, _token, remainder); } -} \ No newline at end of file + + function refundDomain(uint256 _fundingPotId, address _token) private { + FundingPot storage fundingPot = fundingPots[_fundingPotId]; + if (fundingPot.payouts[_token] < fundingPot.balance[_token]) { + uint256 domainId = getDomainFromFundingPot(_fundingPotId); + uint256 surplus = sub(fundingPot.balance[_token], fundingPot.payouts[_token]); + moveFundsBetweenPotsFunctionality(_fundingPotId, domains[domainId].fundingPotId, surplus, _token); + } + } +} diff --git a/contracts/colony/ColonyRewards.sol b/contracts/colony/ColonyRewards.sol index 37b738ec1b..8afd38d9a6 100644 --- a/contracts/colony/ColonyRewards.sol +++ b/contracts/colony/ColonyRewards.sol @@ -205,4 +205,4 @@ contract ColonyRewards is ColonyStorage, PatriciaTreeProofs { // ignore-swc-123 return (payout.tokenAddress, reward); } -} \ No newline at end of file +} diff --git a/contracts/colony/ColonyStorage.sol b/contracts/colony/ColonyStorage.sol index 266aaf9a37..db6f8540f3 100755 --- a/contracts/colony/ColonyStorage.sol +++ b/contracts/colony/ColonyStorage.sol @@ -169,17 +169,19 @@ contract ColonyStorage is ColonyDataTypes, ColonyNetworkDataTypes, DSMath, Commo _; } - modifier expenditureExists(uint256 _id) { - require(_id > 0 && _id <= expenditureCount, "colony-expenditure-does-not-exist"); + modifier validExpenditure(uint256 _id) { + require(expenditureExists(_id), "colony-expenditure-does-not-exist"); _; } modifier expenditureDraft(uint256 _id) { + require(expenditureExists(_id), "colony-expenditure-does-not-exist"); require(expenditures[_id].status == ExpenditureStatus.Draft, "colony-expenditure-not-draft"); _; } modifier expenditureDraftOrLocked(uint256 _id) { + require(expenditureExists(_id), "colony-expenditure-does-not-exist"); require( expenditures[_id].status == ExpenditureStatus.Draft || expenditures[_id].status == ExpenditureStatus.Locked, @@ -189,6 +191,7 @@ contract ColonyStorage is ColonyDataTypes, ColonyNetworkDataTypes, DSMath, Commo } modifier expenditureFinalized(uint256 _id) { + require(expenditureExists(_id), "colony-expenditure-does-not-exist"); require(expenditures[_id].status == ExpenditureStatus.Finalized, "colony-expenditure-not-finalized"); _; } @@ -230,7 +233,7 @@ contract ColonyStorage is ColonyDataTypes, ColonyNetworkDataTypes, DSMath, Commo // Note that these require messages currently cannot propogate up because of the `executeTaskRoleAssignment` logic modifier isAdmin(uint256 _permissionDomainId, uint256 _childSkillIndex, uint256 _id, address _user) { require(ColonyAuthority(address(authority)).hasUserRole(_user, _permissionDomainId, uint8(ColonyRole.Administration)), "colony-not-admin"); - require(validateDomainInheritance(_permissionDomainId, _childSkillIndex, tasks[_id].domainId), "ds-auth-invalid-domain-inheritence"); + require(validateDomainInheritance(_permissionDomainId, _childSkillIndex, tasks[_id].domainId), "ds-auth-invalid-domain-inheritance"); _; } @@ -254,7 +257,7 @@ contract ColonyStorage is ColonyDataTypes, ColonyNetworkDataTypes, DSMath, Commo require(domainExists(_permissionDomainId), "ds-auth-permission-domain-does-not-exist"); require(domainExists(_childDomainId), "ds-auth-child-domain-does-not-exist"); require(isAuthorized(msgSender(), _permissionDomainId, msg.sig), "ds-auth-unauthorized"); - require(validateDomainInheritance(_permissionDomainId, _childSkillIndex, _childDomainId), "ds-auth-invalid-domain-inheritence"); + require(validateDomainInheritance(_permissionDomainId, _childSkillIndex, _childDomainId), "ds-auth-invalid-domain-inheritance"); _; } @@ -330,6 +333,10 @@ contract ColonyStorage is ColonyDataTypes, ColonyNetworkDataTypes, DSMath, Commo return domainId > 0 && domainId <= domainCount; } + function expenditureExists(uint256 expenditureId) internal view returns (bool) { + return expenditureId > 0 && expenditureId <= expenditureCount; + } + function calculateNetworkFeeForPayout(uint256 _payout) internal view returns (uint256 fee) { uint256 feeInverse = IColonyNetwork(colonyNetworkAddress).getFeeInverse(); diff --git a/contracts/colony/IColony.sol b/contracts/colony/IColony.sol index c35e65d403..e3a9eaa163 100644 --- a/contracts/colony/IColony.sol +++ b/contracts/colony/IColony.sol @@ -154,7 +154,7 @@ interface IColony is ColonyDataTypes, IRecovery, IBasicMetaTransaction { function hasUserRole(address _user, uint256 _domainId, ColonyRole _role) external view returns (bool hasRole); /// @notice Check whether a given user has a given role for the colony, in a child domain. - /// Calls the function of the same name on the colony's authority contract and an internal inheritence validator function + /// Calls the function of the same name on the colony's authority contract and an internal inheritance validator function /// @param _user The user whose role we want to check /// @param _domainId Domain in which the caller has the role /// @param _role The role we want to check for @@ -400,6 +400,7 @@ interface IColony is ColonyDataTypes, IRecovery, IBasicMetaTransaction { function finalizeExpenditure(uint256 _id) external; /// @notice Sets the metadata for an expenditure. Can only be called by expenditure owner. + /// @dev Can only be called while expenditure is in draft state. /// @param _id Id of the expenditure /// @param _metadata IPFS hash of the metadata function setExpenditureMetadata(uint256 _id, string memory _metadata) external; @@ -413,12 +414,14 @@ interface IColony is ColonyDataTypes, IRecovery, IBasicMetaTransaction { /// @notice @deprecated /// @notice Sets the recipient on an expenditure slot. Can only be called by expenditure owner. + /// @dev Can only be called while expenditure is in draft state. /// @param _id Id of the expenditure /// @param _slot Slot for the recipient address /// @param _recipient Address of the recipient function setExpenditureRecipient(uint256 _id, uint256 _slot, address payable _recipient) external; /// @notice Sets the recipients in given expenditure slots. Can only be called by expenditure owner. + /// @dev Can only be called while expenditure is in draft state. /// @param _id Id of the expenditure /// @param _slots Array of slots to set recipients /// @param _recipients Addresses of the recipients @@ -426,6 +429,7 @@ interface IColony is ColonyDataTypes, IRecovery, IBasicMetaTransaction { /// @notice @deprecated /// @notice Set the token payout on an expenditure slot. Can only be called by expenditure owner. + /// @dev Can only be called while expenditure is in draft state. /// @param _id Id of the expenditure /// @param _slot Number of the slot /// @param _token Address of the token, `0x0` value indicates Ether @@ -433,12 +437,29 @@ interface IColony is ColonyDataTypes, IRecovery, IBasicMetaTransaction { function setExpenditurePayout(uint256 _id, uint256 _slot, address _token, uint256 _amount) external; /// @notice Set the token payouts in given expenditure slots. Can only be called by expenditure owner. + /// @dev Can only be called while expenditure is in draft state. /// @param _id Id of the expenditure /// @param _slots Array of slots to set payouts /// @param _token Address of the token, `0x0` value indicates Ether /// @param _amounts Payout amounts function setExpenditurePayouts(uint256 _id, uint256[] memory _slots, address _token, uint256[] memory _amounts) external; + /// @notice Set the token payout in a given expenditure slot. Can only be called by an Arbitration user. + /// @param _permissionDomainId The domainId in which I have the permission to take this action + /// @param _childSkillIndex The index that the `_domainId` is relative to `_permissionDomainId` + /// @param _id Id of the expenditure + /// @param _slot The slot to set the payout + /// @param _token Address of the token, `0x0` value indicates Ether + /// @param _amount Payout amount + function setExpenditurePayout( + uint256 _permissionDomainId, + uint256 _childSkillIndex, + uint256 _id, + uint256 _slot, + address _token, + uint256 _amount + ) external; + /// @notice @deprecated /// @notice Sets the skill on an expenditure slot. Can only be called by expenditure owner. /// @param _id Expenditure identifier @@ -471,6 +492,33 @@ interface IColony is ColonyDataTypes, IRecovery, IBasicMetaTransaction { /// @param _payoutModifiers Values (between +/- WAD) to modify the payout & reputation bonus function setExpenditurePayoutModifiers(uint256 _id, uint256[] memory _slots, int256[] memory _payoutModifiers) external; + /// @notice Set many values of an expenditure simultaneously. Can only be called by expenditure owner. + /// @param _recipientSlots Array of slots to set recipients + /// @param _recipients Addresses of the recipients + /// @param _skillIdSlots Array of slots to set skills + /// @param _skillIds Ids of the new skills to set + /// @param _claimDelaySlots Array of slots to set claim delays + /// @param _claimDelays Durations of time (in seconds) to delay + /// @param _payoutModifierSlots Array of slots to set payout modifiers + /// @param _payoutModifiers Values (between +/- WAD) to modify the payout & reputation bonus + /// @param _payoutSlots 2-dimensional array of slots to set payouts + /// @param _payoutTokens Addresses of the tokens, `0x0` value indicates Ether + /// @param _payoutValues 2-dimensional array of the payout amounts + function setExpenditureValues( + uint256 _id, + uint256[] memory _recipientSlots, + address payable[] memory _recipients, + uint256[] memory _skillIdSlots, + uint256[] memory _skillIds, + uint256[] memory _claimDelaySlots, + uint256[] memory _claimDelays, + uint256[] memory _payoutModifierSlots, + int256[] memory _payoutModifiers, + address[] memory _payoutTokens, + uint256[][] memory _payoutSlots, + uint256[][] memory _payoutValues + ) external; + /// @notice Set arbitrary state on an expenditure slot. Can only be called by Arbitration role. /// @param _permissionDomainId The domainId in which I have the permission to take this action /// @param _childSkillIndex The index that the `_domainId` is relative to `_permissionDomainId`, @@ -934,7 +982,8 @@ interface IColony is ColonyDataTypes, IRecovery, IBasicMetaTransaction { /// @param _permissionDomainId The domainId in which I have the permission to take this action /// @param _childSkillIndex The child index in _permissionDomainId where I will be taking this action /// @param _domainId The domain where I am taking this action, pointed to by _permissionDomainId and _childSkillIndex - /// @param _fromChildSkillIndex In the array of child skills for the skill associated with the domain pointed to by _permissionDomainId + _childSkillIndex, the index of the skill associated with the domain that contains _fromPot + /// @param _fromChildSkillIndex In the array of child skills for the skill associated with the domain pointed to by _permissionDomainId + _childSkillIndex, + /// the index of the skill associated with the domain that contains _fromPot /// @param _toChildSkillIndex The same, but for the _toPot which the funds are being moved to /// @param _fromPot Funding pot id providing the funds /// @param _toPot Funding pot id receiving the funds diff --git a/contracts/extensions/StakedExpenditure.sol b/contracts/extensions/StakedExpenditure.sol new file mode 100644 index 0000000000..cdb998fe8e --- /dev/null +++ b/contracts/extensions/StakedExpenditure.sol @@ -0,0 +1,339 @@ +/* + This file is part of The Colony Network. + + The Colony Network is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + The Colony Network is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with The Colony Network. If not, see . +*/ + +pragma solidity 0.7.3; +pragma experimental ABIEncoderV2; + +import "./../colony/ColonyDataTypes.sol"; +import "./../colonyNetwork/IColonyNetwork.sol"; +import "./../patriciaTree/PatriciaTreeProofs.sol"; +import "./ColonyExtensionMeta.sol"; + +// ignore-file-swc-108 + + +contract StakedExpenditure is ColonyExtensionMeta, PatriciaTreeProofs { + + // Events + + event ExpenditureMadeViaStake(address indexed creator, uint256 expenditureId, uint256 stake); + event ExpenditureCancelled(uint256 expenditureId); + event StakeReclaimed(uint256 expenditureId); + event StakeFractionSet(uint256 stakeFraction); + + // Datatypes + + struct Stake { + address creator; + uint256 amount; + } + + // Storage + + uint256 stakeFraction; + + mapping (uint256 => Stake) stakes; + + // Modifiers + + modifier onlyRoot() { + require(colony.hasUserRole(msgSender(), 1, ColonyDataTypes.ColonyRole.Root), "staked-expenditure-caller-not-root"); + _; + } + + // Overrides + + /// @notice Returns the identifier of the extension + function identifier() public override pure returns (bytes32) { + return keccak256("StakedExpenditure"); + } + + /// @notice Returns the version of the extension + function version() public override pure returns (uint256) { + return 1; + } + + /// @notice Configures the extension + /// @param _colony The colony in which the extension holds permissions + function install(address _colony) public override auth { + require(address(colony) == address(0x0), "extension-already-installed"); + + colony = IColony(_colony); + } + + /// @notice Called when upgrading the extension + function finishUpgrade() public override auth {} + + /// @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)))); + } + + // Public + + /// @notice Sets the stake fraction + /// @param _stakeFraction WAD-denominated fraction, used to determine stake as fraction of rep in domain + function setStakeFraction(uint256 _stakeFraction) public onlyRoot { + require(_stakeFraction <= WAD, "staked-expenditure-value-too-large"); + stakeFraction = _stakeFraction; + + emit StakeFractionSet(_stakeFraction); + } + + /// @notice Make an expenditure by putting up a stake + /// @param _permissionDomainId The domainId in which the extension has the administration permission + /// @param _childSkillIndex The index that the `_domainId` is relative to `_permissionDomainId`, + /// @param _domainId The domain where the expenditure belongs + /// @param _key A reputation hash tree key, of the total reputation in _domainId + /// @param _value Reputation value indicating the total reputation in _domainId + /// @param _branchMask The branchmask of the proof + /// @param _siblings The siblings of the proof + function makeExpenditureWithStake( + uint256 _permissionDomainId, + uint256 _childSkillIndex, + uint256 _domainId, + bytes memory _key, + bytes memory _value, + uint256 _branchMask, + bytes32[] memory _siblings + ) + public + notDeprecated + { + uint256 domainRep = getReputationFromProof(_domainId, _key, _value, _branchMask, _siblings); + uint256 stakeAmount = wmul(domainRep, stakeFraction); + + colony.obligateStake(msgSender(), _domainId, stakeAmount); + uint256 expenditureId = colony.makeExpenditure(_permissionDomainId, _childSkillIndex, _domainId); + + stakes[expenditureId] = Stake({ creator: msgSender(), amount: stakeAmount }); + colony.transferExpenditure(expenditureId, msgSender()); + + emit ExpenditureMadeViaStake(msgSender(), expenditureId, stakeAmount); + } + + /// @notice Reclaims the stake if the expenditure is finalized or cancelled + /// @param _expenditureId The id of the expenditure + function reclaimStake(uint256 _expenditureId) public { + Stake storage stake = stakes[_expenditureId]; + require(stake.creator != address(0x0), "staked-expenditure-nothing-to-claim"); + + uint256 stakeAmount = stake.amount; + address stakeCreator = stake.creator; + delete stakes[_expenditureId]; + + ColonyDataTypes.Expenditure memory expenditure = colony.getExpenditure(_expenditureId); + require( + expenditure.status == ColonyDataTypes.ExpenditureStatus.Cancelled || + expenditure.status == ColonyDataTypes.ExpenditureStatus.Finalized, + "staked-expenditure-expenditure-invalid-state" + ); + + colony.deobligateStake(stakeCreator, expenditure.domainId, stakeAmount); + + emit StakeReclaimed(_expenditureId); + } + + /// @notice Cancel the expenditure and reclaim the stake in one transaction + /// @notice Can only be called by expenditure owner while expenditure is in draft state + /// @param _permissionDomainId The domainId in which the extension has the arbitration permission + /// @param _childSkillIndex The index that the `_domainId` is relative to `_permissionDomainId` + /// @param _expenditureId The id of the expenditure + function cancelAndReclaimStake( + uint256 _permissionDomainId, + uint256 _childSkillIndex, + uint256 _expenditureId + ) + public + { + Stake storage stake = stakes[_expenditureId]; + ColonyDataTypes.Expenditure memory expenditure = colony.getExpenditure(_expenditureId); + + require(expenditure.owner == msgSender(), "staked-expenditure-must-be-owner"); + + require( + expenditure.status == ColonyDataTypes.ExpenditureStatus.Draft, + "staked-expenditure-expenditure-not-draft" + ); + + cancelExpenditure(_permissionDomainId, _childSkillIndex, _expenditureId, expenditure.owner); + + // slither-disable-next-line reentrancy-no-eth + reclaimStake(_expenditureId); + } + + /// @notice Cancel the expenditure and punish the staker + /// @notice Can only be called by an arbitration user + /// @param _permissionDomainId The domainId in which the extension has the arbitration permission + /// @param _childSkillIndex The index that the `_domainId` is relative to `_permissionDomainId` + /// @param _callerPermissionDomainId The domainId in which the caller has the arbitration permission + /// @param _callerChildSkillIndex The index that the `_domainId` is relative to `_callerPermissionDomainId` + /// @param _expenditureId The id of the expenditure + /// @param _punish Whether the staker should be punished by losing an amount of reputation equal to the stake + function cancelAndPunish( + uint256 _permissionDomainId, + uint256 _childSkillIndex, + uint256 _callerPermissionDomainId, + uint256 _callerChildSkillIndex, + uint256 _expenditureId, + bool _punish + ) + public + { + ColonyDataTypes.Expenditure memory expenditure = colony.getExpenditure(_expenditureId); + + require( + colony.hasInheritedUserRole( + msgSender(), + _callerPermissionDomainId, + ColonyDataTypes.ColonyRole.Arbitration, + _callerChildSkillIndex, + expenditure.domainId + ), + "staked-expenditure-caller-not-arbitration" + ); + require( + expenditure.status != ColonyDataTypes.ExpenditureStatus.Cancelled, + "staked-expenditure-expenditure-already-cancelled" + ); + + require( + expenditure.status != ColonyDataTypes.ExpenditureStatus.Draft, + "staked-expenditure-expenditure-still-draft" + ); + + if (_punish) { + uint256 stakeAmount = stakes[_expenditureId].amount; + address stakeCreator = stakes[_expenditureId].creator; + delete stakes[_expenditureId]; + + if (stakeAmount > 0) { + colony.transferStake( + _permissionDomainId, + _childSkillIndex, + address(this), + stakeCreator, + expenditure.domainId, + stakeAmount, + address(0x0) + ); + + colony.emitDomainReputationPenalty( + _permissionDomainId, + _childSkillIndex, + expenditure.domainId, + stakeCreator, + -int256(stakeAmount) + ); + } + } + + cancelExpenditure(_permissionDomainId, _childSkillIndex, _expenditureId, expenditure.owner); + } + + // View + + /// @notice Get the stake fraction + /// @return stakeFraction The stake fraction + function getStakeFraction() public view returns (uint256) { + return stakeFraction; + } + + /// @notice Get the stake for an expenditure + /// @param stake The stake, a struct holding the staker's address and the stake amount + function getStake(uint256 _expenditureId) public view returns (Stake memory stake) { + return stakes[_expenditureId]; + } + + // Internal + + uint256 constant EXPENDITURE_SLOT = 25; + bool constant ARRAY = true; + + function cancelExpenditure( + uint256 _permissionDomainId, + uint256 _childSkillIndex, + uint256 _expenditureId, + address _expenditureOwner + ) + internal + { + // Get the slot storing 0x{owner}{state} + bool[] memory mask = new bool[](1); + mask[0] = ARRAY; + bytes32[] memory keys = new bytes32[](1); + keys[0] = bytes32(uint256(0)); + + // Prepare the new 0x000...{owner}{state} value + bytes32 value = ( + bytes32(bytes20(_expenditureOwner)) >> 0x58 | // Shift the address to the right, except for one byte + bytes32(uint256(ColonyDataTypes.ExpenditureStatus.Cancelled)) // Put this value in that rightmost byte + ); + + colony.setExpenditureState( + _permissionDomainId, + _childSkillIndex, + _expenditureId, + EXPENDITURE_SLOT, + mask, + keys, + value + ); + + emit ExpenditureCancelled(_expenditureId); + } + + function getReputationFromProof( + uint256 _domainId, + bytes memory _key, + bytes memory _value, + uint256 _branchMask, + bytes32[] memory _siblings + ) + internal + view + returns (uint256) + { + bytes32 rootHash = IColonyNetwork(colony.getColonyNetwork()).getReputationRootHash(); + bytes32 impliedRoot = getImpliedRootHashKey(_key, _value, _branchMask, _siblings); + require(rootHash == impliedRoot, "staked-expenditure-invalid-root-hash"); + + + uint256 reputationValue; + address keyColonyAddress; + uint256 keySkillId; + address keyUserAddress; + + assembly { + reputationValue := mload(add(_value, 32)) + keyColonyAddress := mload(add(_key, 20)) + keySkillId := mload(add(_key, 52)) + keyUserAddress := mload(add(_key, 72)) + } + + require(keyColonyAddress == address(colony), "staked-expenditure-invalid-colony-address"); + require(keySkillId == colony.getDomain(_domainId).skillId, "staked-expenditure-invalid-skill-id"); + require(keyUserAddress == address(0x0), "staked-expenditure-invalid-user-address"); + + return reputationValue; + } +} diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index 3292525a3e..955bbaf9fa 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -56,10 +56,14 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs, BasicMetaTrans bytes32(uint256(1)) << uint8(ColonyDataTypes.ColonyRole.Root) ); - bytes4 constant CHANGE_FUNCTION_SIG = bytes4(keccak256( + bytes4 constant SET_EXPENDITURE_STATE = bytes4(keccak256( "setExpenditureState(uint256,uint256,uint256,uint256,bool[],bytes32[],bytes32)" )); + bytes4 constant SET_EXPENDITURE_PAYOUT = bytes4(keccak256( + "setExpenditurePayout(uint256,uint256,uint256,uint256,address,uint256)" + )); + bytes4 constant OLD_MOVE_FUNDS_SIG = bytes4(keccak256( "moveFundsBetweenPots(uint256,uint256,uint256,uint256,uint256,uint256,address)" )); @@ -393,9 +397,10 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs, BasicMetaTrans if ( _vote == YAY && !motion.escalated && - motion.stakes[YAY] == requiredStake && - getSig(motion.action) == CHANGE_FUNCTION_SIG && - motion.altTarget == address(0x0) + motion.stakes[YAY] == requiredStake && ( + getSig(motion.action) == SET_EXPENDITURE_STATE || + getSig(motion.action) == SET_EXPENDITURE_PAYOUT + ) && motion.altTarget == address(0x0) ) { bytes32 structHash = hashExpenditureActionStruct(motion.action); expenditureMotionCounts[structHash] = add(expenditureMotionCounts[structHash], 1); @@ -589,9 +594,10 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs, BasicMetaTrans motion.votes[NAY] < motion.votes[YAY] ); - if ( - getSig(motion.action) == CHANGE_FUNCTION_SIG && - getTarget(motion.altTarget) == address(colony) + if (( + getSig(motion.action) == SET_EXPENDITURE_STATE || + getSig(motion.action) == SET_EXPENDITURE_PAYOUT + ) && getTarget(motion.altTarget) == address(colony) ) { bytes32 structHash = hashExpenditureActionStruct(motion.action); expenditureMotionCounts[structHash] = sub(expenditureMotionCounts[structHash], 1); @@ -1038,34 +1044,44 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs, BasicMetaTrans } function hashExpenditureAction(bytes memory action) internal returns (bytes32 hash) { + bytes4 sig = getSig(action); + assert(sig == SET_EXPENDITURE_STATE || sig == SET_EXPENDITURE_PAYOUT); + + uint256 valueLoc = (sig == SET_EXPENDITURE_STATE) ? 0xe4 : 0xc4; + + // Hash all but the domain proof and action value, so actions for the + // same storage slot return the same hash. + // Recall: mload(action) gives the length of the bytes array + // So skip past the three bytes32 (length + domain proof), + // plus 4 bytes for the sig (0x64). Subtract the same from the end, less + // the length bytes32 (0x44). And zero out the value. + 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) + mstore(add(action, valueLoc), 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); + bytes4 sig = getSig(action); + assert(sig == SET_EXPENDITURE_STATE || sig == SET_EXPENDITURE_PAYOUT); uint256 expenditureId; - uint256 storageSlot; + uint256 storageSlot; // This value is only used if (sig == SET_EXPENDITURE_STATE) uint256 expenditureSlot; assembly { expenditureId := mload(add(action, 0x64)) storageSlot := mload(add(action, 0x84)) - expenditureSlot := mload(add(action, 0x184)) } - if (storageSlot == 25) { + if (sig == SET_EXPENDITURE_STATE && storageSlot == 25) { hash = keccak256(abi.encodePacked(expenditureId)); } else { + uint256 expenditureSlotLoc = (sig == SET_EXPENDITURE_STATE) ? 0x184 : 0x84; + assembly { + expenditureSlot := mload(add(action, expenditureSlotLoc)) + } hash = keccak256(abi.encodePacked(expenditureId, expenditureSlot)); } } @@ -1082,14 +1098,18 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs, BasicMetaTrans // of 0x20 represents advancing one byte, 4 is the function signature. // So: 0x[length][sig][args...] - bytes32 functionSignature; + bytes4 sig = getSig(action); + assert(sig == SET_EXPENDITURE_STATE || sig == SET_EXPENDITURE_PAYOUT); + + bytes4 functionSignature = SET_EXPENDITURE_STATE; + uint256 permissionDomainId; uint256 childSkillIndex; uint256 expenditureId; - uint256 storageSlot; + uint256 storageSlot; // This value is only used if (sig == SET_EXPENDITURE_STATE) + uint256 expenditureSlot; assembly { - functionSignature := mload(add(action, 0x20)) permissionDomainId := mload(add(action, 0x24)) childSkillIndex := mload(add(action, 0x44)) expenditureId := mload(add(action, 0x64)) @@ -1097,49 +1117,50 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs, BasicMetaTrans } // If we are editing the main expenditure struct - if (storageSlot == 25) { - + if (sig == SET_EXPENDITURE_STATE && 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 + 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; + uint256 expenditureSlotLoc = (sig == SET_EXPENDITURE_STATE) ? 0x184 : 0x84; 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 + expenditureSlot := mload(add(action, expenditureSlotLoc)) + + 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/docs/_Interface_IColony.md b/docs/_Interface_IColony.md index 2b6c125577..8a87e4027a 100644 --- a/docs/_Interface_IColony.md +++ b/docs/_Interface_IColony.md @@ -4,7 +4,7 @@ section: Interface order: 3 --- - + ## Interface Methods ### `addDomain` @@ -1033,7 +1033,7 @@ Gets the bytes32 representation of the roles for a user in a given domain ### `hasInheritedUserRole` -Check whether a given user has a given role for the colony, in a child domain. Calls the function of the same name on the colony's authority contract and an internal inheritence validator function +Check whether a given user has a given role for the colony, in a child domain. Calls the function of the same name on the colony's authority contract and an internal inheritance validator function **Parameters** @@ -2085,4 +2085,4 @@ Get the Colony contract version. Starts from 1 and is incremented with every dep |Name|Type|Description| |---|---|---| -|colonyVersion|uint256|Version number \ No newline at end of file +|colonyVersion|uint256|Version number diff --git a/migrations/9_setup_extensions.js b/migrations/9_setup_extensions.js index 3b6337437a..fc14f05482 100644 --- a/migrations/9_setup_extensions.js +++ b/migrations/9_setup_extensions.js @@ -6,6 +6,7 @@ const { setupEtherRouter } = require("../helpers/upgradable-contracts"); const CoinMachine = artifacts.require("./CoinMachine"); const EvaluatedExpenditure = artifacts.require("./EvaluatedExpenditure"); +const StakedExpenditure = artifacts.require("./StakedExpenditure"); const FundingQueue = artifacts.require("./FundingQueue"); const OneTxPayment = artifacts.require("./OneTxPayment"); const StreamingPayments = artifacts.require("./StreamingPayments"); @@ -38,6 +39,7 @@ module.exports = async function (deployer, network, accounts) { 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); diff --git a/scripts/check-recovery.js b/scripts/check-recovery.js index 815de9a6e6..5ffffa4ce4 100755 --- a/scripts/check-recovery.js +++ b/scripts/check-recovery.js @@ -48,6 +48,7 @@ walkSync("./contracts/").forEach((contractName) => { "contracts/extensions/ColonyExtension.sol", "contracts/extensions/ColonyExtensionMeta.sol", "contracts/extensions/EvaluatedExpenditure.sol", + "contracts/extensions/StakedExpenditure.sol", "contracts/extensions/FundingQueue.sol", "contracts/extensions/OneTxPayment.sol", "contracts/extensions/StreamingPayments.sol", diff --git a/scripts/check-storage.js b/scripts/check-storage.js index 3eef3b5395..583b0e6b55 100755 --- a/scripts/check-storage.js +++ b/scripts/check-storage.js @@ -29,6 +29,7 @@ walkSync("./contracts/").forEach((contractName) => { "contracts/ens/ENSRegistry.sol", // Not directly used by any colony contracts "contracts/extensions/CoinMachine.sol", "contracts/extensions/EvaluatedExpenditure.sol", + "contracts/extensions/StakedExpenditure.sol", "contracts/extensions/FundingQueue.sol", "contracts/extensions/ColonyExtension.sol", "contracts/extensions/ColonyExtensionMeta.sol", diff --git a/test-smoke/colony-storage-consistent.js b/test-smoke/colony-storage-consistent.js index 0e5a980f39..8617c00ff6 100644 --- a/test-smoke/colony-storage-consistent.js +++ b/test-smoke/colony-storage-consistent.js @@ -149,11 +149,11 @@ contract("Contract Storage", (accounts) => { console.log("miningCycleStateHash:", miningCycleStateHash); console.log("tokenLockingStateHash:", tokenLockingStateHash); - expect(colonyNetworkStateHash).to.equal("0x90e0bd3dedfd8216fd5abd8813df607219aa3f83ebac1a7e22844b68b8205b99"); - expect(colonyStateHash).to.equal("0x8802a96ed09142c3ac65c9908a2d439a9dd0ac6ae8c337e0d0571567580aa450"); - expect(metaColonyStateHash).to.equal("0x4b66ebf5d7b2bc392a92cd3259bbee53555c3cab7b10f6a5ecdd9f3398cf32ef"); - expect(miningCycleStateHash).to.equal("0x9e541d338d0ad0dd5be97a9332d20b4c654f2a33b53f3ba082c0692cb38d2947"); - expect(tokenLockingStateHash).to.equal("0xec1709226e3f22aaeed11ab675789292ac762af0605f2feab596afc86eaf5ca8"); + expect(colonyNetworkStateHash).to.equal("0x225612c3bf05b92dddf480ee2d44e2a71400b4eab1fa2ddf5c99245ee3952988"); + expect(colonyStateHash).to.equal("0x3461a9d484a0f29eed70f61c5d5b8e7b0805dbb2c785876196f7e09c3f6241c0"); + expect(metaColonyStateHash).to.equal("0xff23657f917385e6a94f328907443fef625f08b8b3224e065a53b690f91be0bb"); + expect(miningCycleStateHash).to.equal("0x264d4a83e21fef92f687f9fabacae9370966b0b30ebc15307653c4c3d33a0035"); + expect(tokenLockingStateHash).to.equal("0x983a56a52582ce548e98659e15a9baa5387886fcb0ac1185dbd746dfabf00338"); }); }); }); diff --git a/test/contracts-network/colony-expenditure.js b/test/contracts-network/colony-expenditure.js index b52c662bb2..9cfe2de1b8 100644 --- a/test/contracts-network/colony-expenditure.js +++ b/test/contracts-network/colony-expenditure.js @@ -52,7 +52,6 @@ contract("Colony Expenditure", (accounts) => { const EXPENDITURES_SLOT = 25; const EXPENDITURESLOTS_SLOT = 26; - const EXPENDITURESLOTPAYOUTS_SLOT = 27; let colony; let token; @@ -179,10 +178,10 @@ contract("Colony Expenditure", (accounts) => { }); it("should error if the expenditure does not exist", async () => { - await checkErrorRevert(colony.setExpenditureSkill(100, SLOT0, GLOBAL_SKILL_ID, { from: ADMIN }), "colony-expenditure-does-not-exist"); + await checkErrorRevert(colony.setExpenditureSkill(100, SLOT0, GLOBAL_SKILL_ID), "colony-expenditure-does-not-exist"); await checkErrorRevert(colony.transferExpenditure(100, USER), "colony-expenditure-does-not-exist"); await checkErrorRevert( - colony.transferExpenditureViaArbitration(0, UINT256_MAX, 100, USER, { from: ADMIN }), + colony.transferExpenditureViaArbitration(0, UINT256_MAX, 100, USER, { from: ARBITRATOR }), "colony-expenditure-does-not-exist" ); await checkErrorRevert(colony.cancelExpenditure(100), "colony-expenditure-does-not-exist"); @@ -443,6 +442,175 @@ contract("Colony Expenditure", (accounts) => { totalPayout = await colony.getFundingPotPayout(expenditure.fundingPotId, token.address); expect(totalPayout).to.eq.BN(WAD); }); + + it("should not allow owners to set a payout when out of draft state", async () => { + await colony.finalizeExpenditure(expenditureId, { from: ADMIN }); + + await checkErrorRevert(colony.setExpenditurePayout(expenditureId, SLOT0, token.address, WAD, { from: ADMIN }), "colony-expenditure-not-draft"); + }); + + it("should not allow non-owners to set a payout", async () => { + await checkErrorRevert(colony.setExpenditurePayout(expenditureId, SLOT0, token.address, WAD, { from: USER }), "colony-expenditure-not-owner"); + }); + + it("should allow owners to update many values simultaneously", async () => { + await colony.setExpenditureValues( + expenditureId, + [SLOT0, SLOT1, SLOT2], + [RECIPIENT, USER, ADMIN], + [SLOT1, SLOT2], + [GLOBAL_SKILL_ID, GLOBAL_SKILL_ID], + [SLOT0, SLOT1], + [10, 20], + [SLOT0, SLOT2], + [WAD.divn(3), WAD.divn(2)], + [token.address, otherToken.address], + [ + [SLOT0, SLOT1], + [SLOT1, SLOT2], + ], + [ + [WAD.muln(10), WAD.muln(20)], + [WAD.muln(30), WAD.muln(40)], + ], + { from: ADMIN } + ); + + let slot; + slot = await colony.getExpenditureSlot(expenditureId, SLOT0); + expect(slot.recipient).to.equal(RECIPIENT); + expect(slot.skills[0]).to.be.zero; + expect(slot.claimDelay).to.eq.BN(10); + expect(slot.payoutModifier).to.eq.BN(WAD.divn(3)); + + slot = await colony.getExpenditureSlot(expenditureId, SLOT1); + expect(slot.recipient).to.equal(USER); + expect(slot.skills[0]).to.eq.BN(GLOBAL_SKILL_ID); + expect(slot.claimDelay).to.eq.BN(20); + expect(slot.payoutModifier).to.be.zero; + + slot = await colony.getExpenditureSlot(expenditureId, SLOT2); + expect(slot.recipient).to.equal(ADMIN); + expect(slot.skills[0]).to.eq.BN(GLOBAL_SKILL_ID); + expect(slot.claimDelay).to.be.zero; + expect(slot.payoutModifier).to.eq.BN(WAD.divn(2)); + + let payout; + payout = await colony.getExpenditureSlotPayout(expenditureId, SLOT0, token.address); + expect(payout).to.eq.BN(WAD.muln(10)); + payout = await colony.getExpenditureSlotPayout(expenditureId, SLOT1, token.address); + expect(payout).to.eq.BN(WAD.muln(20)); + payout = await colony.getExpenditureSlotPayout(expenditureId, SLOT2, token.address); + expect(payout).to.be.zero; + + payout = await colony.getExpenditureSlotPayout(expenditureId, SLOT0, otherToken.address); + expect(payout).to.be.zero; + payout = await colony.getExpenditureSlotPayout(expenditureId, SLOT1, otherToken.address); + expect(payout).to.eq.BN(WAD.muln(30)); + payout = await colony.getExpenditureSlotPayout(expenditureId, SLOT2, otherToken.address); + expect(payout).to.eq.BN(WAD.muln(40)); + }); + + it("should revert with an error even if the delegatecall in setExpenditurePayouts is used and fails", async () => { + await checkErrorRevert( + colony.setExpenditureValues( + expenditureId, + [SLOT0, SLOT1, SLOT2], + [RECIPIENT, USER, ADMIN], + [SLOT1, SLOT2], + [GLOBAL_SKILL_ID, GLOBAL_SKILL_ID], + [SLOT0, SLOT1], + [10, 20], + [SLOT0, SLOT2], + [WAD.divn(3), WAD.divn(2)], + [token.address, otherToken.address], + [[SLOT0], [SLOT1, SLOT2]], + [ + [WAD.muln(10), WAD.muln(20)], + [WAD.muln(30), WAD.muln(40)], + ], + { from: ADMIN } + ), + "colony-expenditure-bad-slots" + ); + }); + + it("should not allow owners to update many values simultaneously if not owner", async () => { + await checkErrorRevert( + colony.setExpenditureValues(expenditureId, [], [], [], [], [], [], [], [], [], [[], []], [[], []], { from: USER }), + "colony-expenditure-not-owner" + ); + }); + + it("should not allow owners to update many values simultaneously if not in draft", async () => { + await colony.cancelExpenditure(expenditureId, { from: ADMIN }); + + await checkErrorRevert( + colony.setExpenditureValues(expenditureId, [], [], [], [], [], [], [], [], [], [[], []], [[], []], { from: ADMIN }), + "colony-expenditure-not-draft" + ); + }); + + it("will not update values if empty arrays are passed", async () => { + await colony.setExpenditureValues( + expenditureId, + [SLOT0, SLOT1, SLOT2], + [RECIPIENT, USER, ADMIN], + [SLOT1, SLOT2], + [GLOBAL_SKILL_ID, GLOBAL_SKILL_ID], + [SLOT0, SLOT1], + [10, 20], + [SLOT0, SLOT2], + [WAD.divn(3), WAD.divn(2)], + [token.address, otherToken.address], + [ + [SLOT0, SLOT1], + [SLOT1, SLOT2], + ], + [ + [WAD.muln(10), WAD.muln(20)], + [WAD.muln(30), WAD.muln(40)], + ], + { from: ADMIN } + ); + + // This call has no effect + await colony.setExpenditureValues(expenditureId, [], [], [], [], [], [], [], [], [], [[], []], [[], []], { from: ADMIN }); + + let slot; + slot = await colony.getExpenditureSlot(expenditureId, SLOT0); + expect(slot.recipient).to.equal(RECIPIENT); + expect(slot.skills[0]).to.be.zero; + expect(slot.claimDelay).to.eq.BN(10); + expect(slot.payoutModifier).to.eq.BN(WAD.divn(3)); + + slot = await colony.getExpenditureSlot(expenditureId, SLOT1); + expect(slot.recipient).to.equal(USER); + expect(slot.skills[0]).to.eq.BN(GLOBAL_SKILL_ID); + expect(slot.claimDelay).to.eq.BN(20); + expect(slot.payoutModifier).to.be.zero; + + slot = await colony.getExpenditureSlot(expenditureId, SLOT2); + expect(slot.recipient).to.equal(ADMIN); + expect(slot.skills[0]).to.eq.BN(GLOBAL_SKILL_ID); + expect(slot.claimDelay).to.be.zero; + expect(slot.payoutModifier).to.eq.BN(WAD.divn(2)); + + let payout; + payout = await colony.getExpenditureSlotPayout(expenditureId, SLOT0, token.address); + expect(payout).to.eq.BN(WAD.muln(10)); + payout = await colony.getExpenditureSlotPayout(expenditureId, SLOT1, token.address); + expect(payout).to.eq.BN(WAD.muln(20)); + payout = await colony.getExpenditureSlotPayout(expenditureId, SLOT2, token.address); + expect(payout).to.be.zero; + + payout = await colony.getExpenditureSlotPayout(expenditureId, SLOT0, otherToken.address); + expect(payout).to.be.zero; + payout = await colony.getExpenditureSlotPayout(expenditureId, SLOT1, otherToken.address); + expect(payout).to.eq.BN(WAD.muln(30)); + payout = await colony.getExpenditureSlotPayout(expenditureId, SLOT2, otherToken.address); + expect(payout).to.eq.BN(WAD.muln(40)); + }); }); describe("when locking expenditures", () => { @@ -742,6 +910,35 @@ contract("Colony Expenditure", (accounts) => { expect(potPayout).to.be.zero; }); + it("should automatically reclaim funds if there is excess funding for a token", async () => { + await colony.setExpenditureRecipient(expenditureId, SLOT0, RECIPIENT, { from: ADMIN }); + await colony.setExpenditurePayout(expenditureId, SLOT0, token.address, WAD, { from: ADMIN }); + + const expenditure = await colony.getExpenditure(expenditureId); + await colony.moveFundsBetweenPots( + 1, + UINT256_MAX, + 1, + UINT256_MAX, + UINT256_MAX, + domain1.fundingPotId, + expenditure.fundingPotId, + WAD.muln(2), + token.address + ); + await colony.finalizeExpenditure(expenditureId, { from: ADMIN }); + + const balanceBefore = await colony.getFundingPotBalance(domain1.fundingPotId, token.address); + await colony.claimExpenditurePayout(expenditureId, SLOT0, token.address); + const balanceAfter = await colony.getFundingPotBalance(domain1.fundingPotId, token.address); + expect(balanceAfter.sub(balanceBefore)).to.eq.BN(WAD); + + const potBalance = await colony.getFundingPotBalance(expenditure.fundingPotId, token.address); + const potPayout = await colony.getFundingPotPayout(expenditure.fundingPotId, token.address); + expect(potBalance).to.be.zero; + expect(potPayout).to.be.zero; + }); + it("if skill is set, should emit two reputation updates", async () => { await colony.setExpenditureRecipient(expenditureId, SLOT0, RECIPIENT, { from: ADMIN }); await colony.setExpenditurePayout(expenditureId, SLOT0, token.address, WAD, { from: ADMIN }); @@ -1108,15 +1305,12 @@ contract("Colony Expenditure", (accounts) => { expect(expenditureSlot.skills[0]).to.eq.BN(GLOBAL_SKILL_ID); }); - it("should allow arbitration users to update expenditure slot payouts", async () => { - const mask = [MAPPING, MAPPING]; - const keys = ["0x0", bn2bytes32(new BN(token.address.slice(2), 16))]; - const value = bn2bytes32(new BN(100)); + it("should allow arbitrators to update a payout in one slot", async () => { + const setExpenditurePayout = colony.methods["setExpenditurePayout(uint256,uint256,uint256,uint256,address,uint256)"]; + await setExpenditurePayout(1, UINT256_MAX, expenditureId, SLOT0, token.address, 10, { from: ARBITRATOR }); - await colony.setExpenditureState(1, UINT256_MAX, expenditureId, EXPENDITURESLOTPAYOUTS_SLOT, mask, keys, value, { from: ARBITRATOR }); - - const expenditureSlotPayout = await colony.getExpenditureSlotPayout(expenditureId, 0, token.address); - expect(expenditureSlotPayout).to.eq.BN(100); + const payout = await colony.getExpenditureSlotPayout(expenditureId, SLOT0, token.address); + expect(payout).to.eq.BN(10); }); it("should not allow arbitration users to pass invalid slots", async () => { @@ -1174,11 +1368,6 @@ contract("Colony Expenditure", (accounts) => { "colony-expenditure-bad-keys" ); - await checkErrorRevert( - colony.setExpenditureState(1, UINT256_MAX, expenditureId, EXPENDITURESLOTPAYOUTS_SLOT, mask, keys, HASHZERO, { from: ARBITRATOR }), - "colony-expenditure-bad-keys" - ); - await checkErrorRevert( colony.setExpenditureState(1, UINT256_MAX, expenditureId, 1000000, mask, keys, HASHZERO, { from: ARBITRATOR }), "colony-expenditure-bad-slot" diff --git a/test/contracts-network/colony-funding.js b/test/contracts-network/colony-funding.js index 7ba146902d..fc47e13651 100755 --- a/test/contracts-network/colony-funding.js +++ b/test/contracts-network/colony-funding.js @@ -509,19 +509,22 @@ contract("Colony Funding", (accounts) => { expect(colonyPotBalance).to.eq.BN(MANAGER_PAYOUT.add(EVALUATOR_PAYOUT).add(WORKER_PAYOUT)); }); - it("should allow funds to be removed from a task if there are no more payouts of that token to be claimed", async () => { - await fundColonyWithTokens(colony, otherToken, WAD.muln(363)); + it("should automatically return surplus funds to the domain", async () => { + await fundColonyWithTokens(colony, otherToken, WAD.muln(500)); const taskId = await setupFinalizedTask({ colonyNetwork, colony, token: otherToken }); const task = await colony.getTask(taskId); - await colony.moveFundsBetweenPots(1, UINT256_MAX, 1, UINT256_MAX, UINT256_MAX, 1, task.fundingPotId, 10, otherToken.address); + + // Add an extra WAD of funding + await colony.moveFundsBetweenPots(1, UINT256_MAX, 1, UINT256_MAX, UINT256_MAX, 1, task.fundingPotId, WAD, otherToken.address); + await colony.claimTaskPayout(taskId, MANAGER_ROLE, otherToken.address); await colony.claimTaskPayout(taskId, WORKER_ROLE, otherToken.address); await colony.claimTaskPayout(taskId, EVALUATOR_ROLE, otherToken.address); - await colony.moveFundsBetweenPots(1, UINT256_MAX, 1, UINT256_MAX, UINT256_MAX, task.fundingPotId, 1, 10, otherToken.address); - const colonyPotBalance = await colony.getFundingPotBalance(2, otherToken.address); - expect(colonyPotBalance).to.be.zero; + // WAD is gone + const taskPotBalance = await colony.getFundingPotBalance(task.fundingPotId, otherToken.address); + expect(taskPotBalance).to.be.zero; }); it("should not allow user to claim payout if rating is 1", async () => { diff --git a/test/contracts-network/colony-permissions.js b/test/contracts-network/colony-permissions.js index aa6982a5e5..2786586c34 100644 --- a/test/contracts-network/colony-permissions.js +++ b/test/contracts-network/colony-permissions.js @@ -477,7 +477,7 @@ contract("ColonyPermissions", (accounts) => { token.address, { from: USER2 } ), - "ds-auth-invalid-domain-inheritence" + "ds-auth-invalid-domain-inheritance" ); await checkErrorRevert( colony.methods["moveFundsBetweenPots(uint256,uint256,uint256,uint256,uint256,uint256,address)"]( @@ -490,7 +490,7 @@ contract("ColonyPermissions", (accounts) => { token.address, { from: USER2 } ), - "ds-auth-invalid-domain-inheritence" + "ds-auth-invalid-domain-inheritance" ); // The newest version @@ -507,7 +507,7 @@ contract("ColonyPermissions", (accounts) => { token.address, { from: USER2 } ), - "colony-invalid-domain-inheritence" + "colony-invalid-domain-inheritance" ); await checkErrorRevert( @@ -523,7 +523,7 @@ contract("ColonyPermissions", (accounts) => { token.address, { from: USER2 } ), - "colony-invalid-domain-inheritence" + "colony-invalid-domain-inheritance" ); }); diff --git a/test/contracts-network/colony-recovery.js b/test/contracts-network/colony-recovery.js index 2130ed5045..06f6fa351b 100644 --- a/test/contracts-network/colony-recovery.js +++ b/test/contracts-network/colony-recovery.js @@ -189,6 +189,7 @@ contract("Colony Recovery", (accounts) => { await checkErrorRevert(metaColony.setExpenditureSkill(0, 0, 0), "colony-in-recovery-mode"); await checkErrorRevert(metaColony.setExpenditureClaimDelay(0, 0, 0), "colony-in-recovery-mode"); await checkErrorRevert(metaColony.setExpenditureState(0, 0, 0, 0, [], [], HASHZERO), "colony-in-recovery-mode"); + await checkErrorRevert(metaColony.setExpenditureValues(0, [], [], [], [], [], [], [], [], [], [[]], [[]]), "colony-in-recovery-mode"); await checkErrorRevert(metaColony.setArbitrationRole(0, 0, ADDRESS_ZERO, 0, true), "colony-in-recovery-mode"); await checkErrorRevert(metaColony.setArchitectureRole(0, 0, ADDRESS_ZERO, 0, true), "colony-in-recovery-mode"); await checkErrorRevert(metaColony.setFundingRole(0, 0, ADDRESS_ZERO, 0, true), "colony-in-recovery-mode"); @@ -227,6 +228,7 @@ contract("Colony Recovery", (accounts) => { await checkErrorRevert(metaColony.setRewardInverse(0), "colony-in-recovery-mode"); await checkErrorRevert(metaColony.setExpenditurePayouts(0, [], ADDRESS_ZERO, []), "colony-in-recovery-mode"); await checkErrorRevert(metaColony.setExpenditurePayout(0, 0, ADDRESS_ZERO, 0), "colony-in-recovery-mode"); + await checkErrorRevert(metaColony.setExpenditurePayout(1, UINT256_MAX, 0, 0, ADDRESS_ZERO, 0), "colony-in-recovery-mode"); await checkErrorRevert(metaColony.enterRecoveryMode(), "colony-in-recovery-mode"); await checkErrorRevert(metaColony.burnTokens(ADDRESS_ZERO, 0), "colony-in-recovery-mode"); await checkErrorRevert(metaColony.registerColonyLabel("", ""), "colony-in-recovery-mode"); diff --git a/test/contracts-network/colony-staking.js b/test/contracts-network/colony-staking.js index a92eeefe2b..8c1f00f27e 100644 --- a/test/contracts-network/colony-staking.js +++ b/test/contracts-network/colony-staking.js @@ -3,13 +3,14 @@ const chai = require("chai"); const bnChai = require("bn-chai"); const { ethers } = require("ethers"); -const { UINT256_MAX, WAD, INITIAL_FUNDING } = require("../../helpers/constants"); +const { UINT256_MAX, WAD, INITIAL_FUNDING, ADDRESS_ZERO } = require("../../helpers/constants"); const { fundColonyWithTokens, setupRandomColony, setupColony } = require("../../helpers/test-data-generator"); const { checkErrorRevert, expectEvent } = require("../../helpers/test-helper"); const { expect } = chai; chai.use(bnChai(web3.utils.BN)); +const ToggleableToken = artifacts.require("ToggleableToken"); const EtherRouter = artifacts.require("EtherRouter"); const IColonyNetwork = artifacts.require("IColonyNetwork"); const ITokenLocking = artifacts.require("ITokenLocking"); @@ -236,5 +237,38 @@ contract("Colony Staking", (accounts) => { const { balance } = await tokenLocking.getUserLock(token.address, USER2); expect(balance).to.eq.BN(WAD); }); + + it.skip("should burn the stake if sent to address(0x0) and the token supports burning", async () => { + const supplyBefore = await token.totalSupply(); + + await colony.approveStake(USER0, 1, WAD, { from: USER1 }); + await colony.obligateStake(USER1, 1, WAD, { from: USER0 }); + await colony.transferStake(1, UINT256_MAX, USER0, USER1, 1, WAD, ADDRESS_ZERO, { from: USER2 }); + + const supplyAfter = await token.totalSupply(); + expect(supplyBefore.sub(supplyAfter)).to.eq.BN(WAD); + }); + + it.skip("should send the stake to address(0x0) if the token does not support burning", async () => { + // This token does not support burning + token = await ToggleableToken.new(WAD, { from: USER1 }); + colony = await setupColony(colonyNetwork, token.address); + await colony.setArbitrationRole(1, UINT256_MAX, USER2, 1, true); + + await token.approve(tokenLocking.address, WAD, { from: USER1 }); + await tokenLocking.deposit(token.address, WAD, true, { from: USER1 }); + + const supplyBefore = await token.balanceOf(tokenLocking.address); + + await colony.approveStake(USER0, 1, WAD, { from: USER1 }); + await colony.obligateStake(USER1, 1, WAD, { from: USER0 }); + await colony.transferStake(1, UINT256_MAX, USER0, USER1, 1, WAD, ADDRESS_ZERO, { from: USER2 }); + + const supplyAfter = await token.balanceOf(tokenLocking.address); + expect(supplyBefore.sub(supplyAfter)).to.eq.BN(WAD); + + const nullBalance = await token.balanceOf(ADDRESS_ZERO); + expect(nullBalance).to.eq.BN(WAD); + }); }); }); diff --git a/test/extensions/evaluated-expenditures.js b/test/extensions/evaluated-expenditure.js similarity index 100% rename from test/extensions/evaluated-expenditures.js rename to test/extensions/evaluated-expenditure.js diff --git a/test/extensions/staked-expenditure.js b/test/extensions/staked-expenditure.js new file mode 100644 index 0000000000..ba57f92138 --- /dev/null +++ b/test/extensions/staked-expenditure.js @@ -0,0 +1,401 @@ +/* globals artifacts */ + +const chai = require("chai"); +const bnChai = require("bn-chai"); +const { ethers } = require("ethers"); +const { soliditySha3 } = require("web3-utils"); + +const { UINT256_MAX, WAD, MINING_CYCLE_DURATION, CHALLENGE_RESPONSE_WINDOW_DURATION, ADDRESS_ZERO } = require("../../helpers/constants"); +const { setupRandomColony } = require("../../helpers/test-data-generator"); +const { + checkErrorRevert, + web3GetCode, + makeReputationKey, + makeReputationValue, + getActiveRepCycle, + forwardTime, +} = require("../../helpers/test-helper"); + +const PatriciaTree = require("../../packages/reputation-miner/patricia"); + +const { expect } = chai; +chai.use(bnChai(web3.utils.BN)); + +const IColonyNetwork = artifacts.require("IColonyNetwork"); +const ITokenLocking = artifacts.require("ITokenLocking"); +const EtherRouter = artifacts.require("EtherRouter"); +const StakedExpenditure = artifacts.require("StakedExpenditure"); +const IReputationMiningCycle = artifacts.require("IReputationMiningCycle"); + +const STAKED_EXPENDITURE = soliditySha3("StakedExpenditure"); + +contract("StakedExpenditure", (accounts) => { + let colonyNetwork; + let colony; + let token; + let tokenLocking; + let stakedExpenditure; + let version; + + let reputationTree; + let domain1Key; + let domain1Value; + let domain1Mask; + let domain1Siblings; + + let requiredStake; + + const USER0 = accounts[0]; + const USER1 = accounts[1]; + const MINER = accounts[5]; + + const CANCELLED = 1; + + before(async () => { + const etherRouter = await EtherRouter.deployed(); + colonyNetwork = await IColonyNetwork.at(etherRouter.address); + + const tokenLockingAddress = await colonyNetwork.getTokenLocking(); + tokenLocking = await ITokenLocking.at(tokenLockingAddress); + + const extension = await StakedExpenditure.new(); + version = await extension.version(); + }); + + beforeEach(async () => { + ({ colony, token } = await setupRandomColony(colonyNetwork)); + + await colony.installExtension(STAKED_EXPENDITURE, version); + + const stakedExpenditureAddress = await colonyNetwork.getExtensionInstallation(STAKED_EXPENDITURE, colony.address); + stakedExpenditure = await StakedExpenditure.at(stakedExpenditureAddress); + + await colony.setArbitrationRole(1, UINT256_MAX, stakedExpenditure.address, 1, true); + await colony.setAdministrationRole(1, UINT256_MAX, stakedExpenditure.address, 1, true); + + const domain1 = await colony.getDomain(1); + + reputationTree = new PatriciaTree(); + await reputationTree.insert( + makeReputationKey(colony.address, domain1.skillId), // Colony total + makeReputationValue(WAD.muln(3), 1) + ); + + // Used to create invalid proofs + await reputationTree.insert( + makeReputationKey(ADDRESS_ZERO, domain1.skillId), // Bad colony + makeReputationValue(WAD, 2) + ); + await reputationTree.insert( + makeReputationKey(colony.address, 100), // Bad skill + makeReputationValue(WAD, 3) + ); + await reputationTree.insert( + makeReputationKey(colony.address, domain1.skillId, USER0), // Bad user + makeReputationValue(WAD, 4) + ); + + domain1Key = makeReputationKey(colony.address, domain1.skillId); + domain1Value = makeReputationValue(WAD.muln(3), 1); + [domain1Mask, domain1Siblings] = await reputationTree.getProof(domain1Key); + + 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("managing the extension", async () => { + it("can install the extension manually", async () => { + stakedExpenditure = await StakedExpenditure.new(); + await stakedExpenditure.install(colony.address); + + await checkErrorRevert(stakedExpenditure.install(colony.address), "extension-already-installed"); + + const identifier = await stakedExpenditure.identifier(); + expect(identifier).to.equal(STAKED_EXPENDITURE); + + const capabilityRoles = await stakedExpenditure.getCapabilityRoles("0x0"); + expect(capabilityRoles).to.equal(ethers.constants.HashZero); + + await stakedExpenditure.finishUpgrade(); + await stakedExpenditure.deprecate(true); + await stakedExpenditure.uninstall(); + + const code = await web3GetCode(stakedExpenditure.address); + expect(code).to.equal("0x"); + }); + + it("can't use the network-level functions if installed via ColonyNetwork", async () => { + await checkErrorRevert(stakedExpenditure.install(ADDRESS_ZERO, { from: USER1 }), "ds-auth-unauthorized"); + await checkErrorRevert(stakedExpenditure.finishUpgrade({ from: USER1 }), "ds-auth-unauthorized"); + await checkErrorRevert(stakedExpenditure.deprecate(true, { from: USER1 }), "ds-auth-unauthorized"); + await checkErrorRevert(stakedExpenditure.uninstall({ from: USER1 }), "ds-auth-unauthorized"); + }); + + it("can install the extension with the extension manager", async () => { + ({ colony } = await setupRandomColony(colonyNetwork)); + await colony.installExtension(STAKED_EXPENDITURE, version, { from: USER0 }); + + await checkErrorRevert(colony.installExtension(STAKED_EXPENDITURE, version, { from: USER0 }), "colony-network-extension-already-installed"); + await checkErrorRevert(colony.uninstallExtension(STAKED_EXPENDITURE, { from: USER1 }), "ds-auth-unauthorized"); + + await colony.uninstallExtension(STAKED_EXPENDITURE, { from: USER0 }); + }); + }); + + describe("using stakes to manage expenditures", async () => { + beforeEach(async () => { + await stakedExpenditure.setStakeFraction(WAD.divn(10)); // Stake of .3 WADs + requiredStake = WAD.muln(3).divn(10); + + await token.mint(USER0, WAD); + await token.approve(tokenLocking.address, WAD, { from: USER0 }); + await tokenLocking.deposit(token.address, WAD, false, { from: USER0 }); + await colony.approveStake(stakedExpenditure.address, 1, WAD, { from: USER0 }); + + const userLock = await tokenLocking.getUserLock(token.address, USER0); + expect(userLock.balance).to.eq.BN(WAD); + }); + + it("can set the stake fraction", async () => { + await stakedExpenditure.setStakeFraction(WAD, { from: USER0 }); + + const stakeFraction = await stakedExpenditure.getStakeFraction(); + expect(stakeFraction).to.eq.BN(WAD); + + // But not if not root! + await checkErrorRevert(stakedExpenditure.setStakeFraction(WAD, { from: USER1 }), "staked-expenditure-caller-not-root"); + + // Also not greater than WAD! + await checkErrorRevert(stakedExpenditure.setStakeFraction(WAD.addn(1), { from: USER0 }), "staked-expenditure-value-too-large"); + }); + + it("can create an expenditure by submitting a stake", async () => { + await stakedExpenditure.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); + const expenditureId = await colony.getExpenditureCount(); + + const { owner } = await colony.getExpenditure(expenditureId); + expect(owner).to.equal(USER0); + + const obligation = await tokenLocking.getObligation(USER0, token.address, colony.address); + expect(obligation).to.eq.BN(requiredStake); + + const stake = await stakedExpenditure.getStake(expenditureId); + expect(stake.creator).to.equal(USER0); + expect(stake.amount).to.eq.BN(requiredStake); + }); + + it("cannot create an expenditure with an invalid proof", async () => { + const domain1 = await colony.getDomain(1); + + let key; + let value; + let mask; + let siblings; + + key = makeReputationKey(colony.address, domain1.skillId); + value = makeReputationValue(WAD, 10); + [mask, siblings] = await reputationTree.getProof(key); + await checkErrorRevert( + stakedExpenditure.makeExpenditureWithStake(1, UINT256_MAX, 1, key, value, mask, siblings), + "staked-expenditure-invalid-root-hash" + ); + + key = makeReputationKey(ADDRESS_ZERO, domain1.skillId); + value = makeReputationValue(WAD, 2); + [mask, siblings] = await reputationTree.getProof(key); + await checkErrorRevert( + stakedExpenditure.makeExpenditureWithStake(1, UINT256_MAX, 1, key, value, mask, siblings), + "staked-expenditure-invalid-colony-address" + ); + + key = makeReputationKey(colony.address, 100); + value = makeReputationValue(WAD, 3); + [mask, siblings] = await reputationTree.getProof(key); + await checkErrorRevert( + stakedExpenditure.makeExpenditureWithStake(1, UINT256_MAX, 1, key, value, mask, siblings), + "staked-expenditure-invalid-skill-id" + ); + + key = makeReputationKey(colony.address, domain1.skillId, USER0); + value = makeReputationValue(WAD, 4); + [mask, siblings] = await reputationTree.getProof(key); + await checkErrorRevert( + stakedExpenditure.makeExpenditureWithStake(1, UINT256_MAX, 1, key, value, mask, siblings), + "staked-expenditure-invalid-user-address" + ); + }); + + it("cannot create an expenditure if the extension is deprecated", async () => { + await colony.deprecateExtension(STAKED_EXPENDITURE, true); + + await checkErrorRevert( + stakedExpenditure.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }), + "colony-extension-deprecated" + ); + }); + + it("can slash the stake with the arbitration permission", async () => { + await stakedExpenditure.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); + const expenditureId = await colony.getExpenditureCount(); + await colony.lockExpenditure(expenditureId); + + await stakedExpenditure.cancelAndPunish(1, UINT256_MAX, 1, UINT256_MAX, expenditureId, true); + + const obligation = await tokenLocking.getObligation(USER0, token.address, colony.address); + expect(obligation).to.be.zero; + + const userLock = await tokenLocking.getUserLock(token.address, USER0); + expect(userLock.balance).to.eq.BN(WAD.sub(requiredStake)); + + // Creator gets a reputation penalty + const addr = await colonyNetwork.getReputationMiningCycle(false); + const repCycle = await IReputationMiningCycle.at(addr); + const numUpdates = await repCycle.getReputationUpdateLogLength(); + const repUpdate = await repCycle.getReputationUpdateLogEntry(numUpdates.subn(1)); + const domain1 = await colony.getDomain(1); + + expect(repUpdate.user).to.equal(USER0); + expect(repUpdate.amount).to.eq.BN(requiredStake.neg()); + expect(repUpdate.skillId).to.eq.BN(domain1.skillId); + + // And the expenditure is automatically cancelled + const expenditure = await colony.getExpenditure(expenditureId); + expect(expenditure.status).to.eq.BN(CANCELLED); + }); + + it("cannot slash the stake without the arbitration permission", async () => { + await stakedExpenditure.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); + const expenditureId = await colony.getExpenditureCount(); + await colony.lockExpenditure(expenditureId); + + await checkErrorRevert( + stakedExpenditure.cancelAndPunish(1, UINT256_MAX, 1, UINT256_MAX, expenditureId, true, { from: USER1 }), + "staked-expenditure-caller-not-arbitration" + ); + }); + + it("can cancel the expenditure without penalty", async () => { + await stakedExpenditure.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); + const expenditureId = await colony.getExpenditureCount(); + await colony.lockExpenditure(expenditureId); + + await stakedExpenditure.cancelAndPunish(1, UINT256_MAX, 1, UINT256_MAX, expenditureId, false); + + let obligation; + let userLock; + + obligation = await tokenLocking.getObligation(USER0, token.address, colony.address); + expect(obligation).to.eq.BN(requiredStake); + + userLock = await tokenLocking.getUserLock(token.address, USER0); + expect(userLock.balance).to.eq.BN(WAD); + + const expenditure = await colony.getExpenditure(expenditureId); + expect(expenditure.status).to.eq.BN(CANCELLED); + + // User can reclaim the stake + await stakedExpenditure.reclaimStake(expenditureId); + + obligation = await tokenLocking.getObligation(USER0, token.address, colony.address); + expect(obligation).to.be.zero; + + userLock = await tokenLocking.getUserLock(token.address, USER0); + expect(userLock.balance).to.eq.BN(WAD); + }); + + it("if ownership is transferred, the original owner is still slashed", async () => { + await stakedExpenditure.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); + const expenditureId = await colony.getExpenditureCount(); + await colony.lockExpenditure(expenditureId); + + await colony.transferExpenditure(expenditureId, USER1, { from: USER0 }); + + await stakedExpenditure.cancelAndPunish(1, UINT256_MAX, 1, UINT256_MAX, expenditureId, true); + + const obligation = await tokenLocking.getObligation(USER0, token.address, colony.address); + expect(obligation).to.be.zero; + + const userLock = await tokenLocking.getUserLock(token.address, USER0); + expect(userLock.balance).to.eq.BN(WAD.sub(requiredStake)); + }); + + it("cannot slash a nonexistent stake", async () => { + await colony.makeExpenditure(1, UINT256_MAX, 1); + const expenditureId = await colony.getExpenditureCount(); + await colony.lockExpenditure(expenditureId); + + await checkErrorRevert( + stakedExpenditure.cancelAndPunish(1, UINT256_MAX, 1, UINT256_MAX, expenditureId, true), + "staked-expenditure-nothing-to-slash" + ); + }); + + it("can reclaim the stake by cancelling the expenditure", async () => { + await stakedExpenditure.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); + const expenditureId = await colony.getExpenditureCount(); + + await colony.cancelExpenditure(expenditureId); + + await stakedExpenditure.reclaimStake(expenditureId); + + const obligation = await tokenLocking.getObligation(USER0, token.address, colony.address); + expect(obligation).to.be.zero; + + const userLock = await tokenLocking.getUserLock(token.address, USER0); + expect(userLock.balance).to.eq.BN(WAD); + }); + + it("can cancel and reclaim the stake in one transaction", async () => { + await stakedExpenditure.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); + const expenditureId = await colony.getExpenditureCount(); + + await stakedExpenditure.cancelAndReclaimStake(1, UINT256_MAX, expenditureId); + + const obligation = await tokenLocking.getObligation(USER0, token.address, colony.address); + expect(obligation).to.be.zero; + + const userLock = await tokenLocking.getUserLock(token.address, USER0); + expect(userLock.balance).to.eq.BN(WAD); + }); + + it("cannot cancel and reclaim the stake in one transaction if not owner", async () => { + await stakedExpenditure.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); + const expenditureId = await colony.getExpenditureCount(); + + await checkErrorRevert( + stakedExpenditure.cancelAndReclaimStake(1, UINT256_MAX, expenditureId, { from: USER1 }), + "staked-expenditure-must-be-owner" + ); + }); + + it("can reclaim the stake by finalizing the expenditure", async () => { + await stakedExpenditure.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); + const expenditureId = await colony.getExpenditureCount(); + + await colony.finalizeExpenditure(expenditureId); + + await stakedExpenditure.reclaimStake(expenditureId); + + const obligation = await tokenLocking.getObligation(USER0, token.address, colony.address); + expect(obligation).to.be.zero; + + const userLock = await tokenLocking.getUserLock(token.address, USER0); + expect(userLock.balance).to.eq.BN(WAD); + }); + + it("cannot reclaim the stake while the expenditure is in progress", async () => { + await stakedExpenditure.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); + const expenditureId = await colony.getExpenditureCount(); + + await checkErrorRevert(stakedExpenditure.reclaimStake(expenditureId), "staked-expenditure-expenditure-invalid-state"); + }); + + it("cannot reclaim a nonexistent stake", async () => { + await checkErrorRevert(stakedExpenditure.reclaimStake(0), "staked-expenditure-nothing-to-claim"); + }); + }); +}); diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index f3e488e3e6..4360d64f7a 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -19,6 +19,7 @@ const { encodeTxData, bn2bytes32, expectEvent, + getTokenArgs, } = require("../../helpers/test-helper"); const { setupRandomColony, getMetaTransactionParameters } = require("../../helpers/test-data-generator"); @@ -32,6 +33,7 @@ const IColonyNetwork = artifacts.require("IColonyNetwork"); const IMetaColony = artifacts.require("IMetaColony"); const EtherRouter = artifacts.require("EtherRouter"); const IReputationMiningCycle = artifacts.require("IReputationMiningCycle"); +const Token = artifacts.require("Token"); const TokenLocking = artifacts.require("TokenLocking"); const VotingReputation = artifacts.require("VotingReputation"); const OneTxPayment = artifacts.require("OneTxPayment"); @@ -683,10 +685,11 @@ contract("Voting Reputation", (accounts) => { motionId = await voting.getMotionCount(); let expenditureMotionCount; + let expenditureSlot; + expenditureMotionCount = await voting.getExpenditureMotionCount(soliditySha3(expenditureId, 0)); expect(expenditureMotionCount).to.be.zero; - let expenditureSlot; expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); expect(expenditureSlot.claimDelay).to.be.zero; @@ -699,32 +702,34 @@ contract("Voting Reputation", (accounts) => { expect(expenditureSlot.claimDelay).to.eq.BN(UINT256_MAX.divn(3)); await checkErrorRevert(colony.claimExpenditurePayout(expenditureId, 0, token.address), "colony-expenditure-cannot-claim"); + + await forwardTime(STAKE_PERIOD, this); + await voting.finalizeMotion(motionId); + + expenditureMotionCount = await voting.getExpenditureMotionCount(soliditySha3(expenditureId, 0)); + expect(expenditureMotionCount).to.be.zero; + + expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); + expect(expenditureSlot.claimDelay).to.be.zero; }); - it("can update the expenditure slot claimDelay if voting on expenditure payout state", async () => { + it("can update the expenditure slot claimDelay if voting on expenditure payout states", async () => { await colony.makeExpenditure(1, UINT256_MAX, 1); const expenditureId = await colony.getExpenditureCount(); await colony.finalizeExpenditure(expenditureId); // Set payout to WAD for expenditure slot 0, internal token - const action = await encodeTxData(colony, "setExpenditureState", [ - 1, - UINT256_MAX, - expenditureId, - 27, - [false, false], - ["0x0", bn2bytes32(new BN(token.address.slice(2), 16))], - WAD32, - ]); + const action = await encodeTxData(colony, "setExpenditurePayout", [1, UINT256_MAX, expenditureId, 0, token.address, WAD]); await voting.createMotion(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); motionId = await voting.getMotionCount(); let expenditureMotionCount; + let expenditureSlot; + expenditureMotionCount = await voting.getExpenditureMotionCount(soliditySha3(expenditureId, 0)); expect(expenditureMotionCount).to.be.zero; - let expenditureSlot; expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); expect(expenditureSlot.claimDelay).to.be.zero; @@ -737,6 +742,95 @@ contract("Voting Reputation", (accounts) => { expect(expenditureSlot.claimDelay).to.eq.BN(UINT256_MAX.divn(3)); await checkErrorRevert(colony.claimExpenditurePayout(expenditureId, 0, token.address), "colony-expenditure-cannot-claim"); + + await forwardTime(STAKE_PERIOD, this); + await voting.finalizeMotion(motionId); + + expenditureMotionCount = await voting.getExpenditureMotionCount(soliditySha3(expenditureId, 0)); + expect(expenditureMotionCount).to.be.zero; + + expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); + expect(expenditureSlot.claimDelay).to.be.zero; + }); + + it("can update the expenditure slot claimDelay if voting on multiple expenditure payout states", async () => { + const tokenArgs = getTokenArgs(); + const otherToken = await Token.new(...tokenArgs); + + await colony.makeExpenditure(1, UINT256_MAX, 1); + const expenditureId = await colony.getExpenditureCount(); + await colony.finalizeExpenditure(expenditureId); + + let action; + + // Two actions on the first slot, one on the second + action = await encodeTxData(colony, "setExpenditurePayout", [1, UINT256_MAX, expenditureId, 0, token.address, WAD]); + await voting.createMotion(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + motionId = await voting.getMotionCount(); + await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + + action = await encodeTxData(colony, "setExpenditurePayout", [1, UINT256_MAX, expenditureId, 0, otherToken.address, WAD]); + await voting.createMotion(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + motionId = await voting.getMotionCount(); + await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + + action = await encodeTxData(colony, "setExpenditurePayout", [1, UINT256_MAX, expenditureId, 1, token.address, WAD]); + await voting.createMotion(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + motionId = await voting.getMotionCount(); + await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + + let expenditureMotionCount; + let expenditureSlot; + + expenditureMotionCount = await voting.getExpenditureMotionCount(soliditySha3(expenditureId, 0)); + expect(expenditureMotionCount).to.eq.BN(2); + + expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); + expect(expenditureSlot.claimDelay).to.eq.BN(UINT256_MAX.divn(3)); + + expenditureMotionCount = await voting.getExpenditureMotionCount(soliditySha3(expenditureId, 1)); + expect(expenditureMotionCount).to.eq.BN(1); + + expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); + expect(expenditureSlot.claimDelay).to.eq.BN(UINT256_MAX.divn(3)); + }); + + it("can properly manage repeat motions if voting on expenditure payout states", async () => { + await colony.makeExpenditure(1, UINT256_MAX, 1); + const expenditureId = await colony.getExpenditureCount(); + await colony.finalizeExpenditure(expenditureId); + + let action; + let payout; + + // Set payout for expenditure slot 0, internal token, to WAD + action = await encodeTxData(colony, "setExpenditurePayout", [1, UINT256_MAX, expenditureId, 0, token.address, WAD]); + + await voting.createMotion(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + motionId = await voting.getMotionCount(); + + await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + + await forwardTime(STAKE_PERIOD, this); + await voting.finalizeMotion(motionId); + + payout = await colony.getExpenditureSlotPayout(expenditureId, 0, token.address); + expect(payout).to.eq.BN(WAD); + + // Set payout for expenditure slot 0, internal token, back to 0 + action = await encodeTxData(colony, "setExpenditurePayout", [1, UINT256_MAX, expenditureId, 0, token.address, 0]); + + await voting.createMotion(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + motionId = await voting.getMotionCount(); + + await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + + await forwardTime(STAKE_PERIOD, this); + await voting.finalizeMotion(motionId); + + // Motion doesn't execute because the same reputation is voting + payout = await colony.getExpenditureSlotPayout(expenditureId, 0, token.address); + expect(payout).to.eq.BN(WAD); }); it("can update the expenditure slot claimDelay if voting on multiple expenditure states", async () => { @@ -770,22 +864,6 @@ contract("Voting Reputation", (accounts) => { motionId = await voting.getMotionCount(); await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - // Motion 2 - // Set payout to WAD for expenditure slot 0, internal token - action = await encodeTxData(colony, "setExpenditureState", [ - 1, - UINT256_MAX, - expenditureId, - 27, - [false, false], - ["0x0", bn2bytes32(new BN(token.address.slice(2), 16))], - WAD32, - ]); - - await voting.createMotion(1, UINT256_MAX, ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); - motionId = await voting.getMotionCount(); - await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - const expenditure = await colony.getExpenditure(expenditureId); expect(expenditure.globalClaimDelay).to.eq.BN(UINT256_MAX.divn(3)); @@ -824,15 +902,15 @@ contract("Voting Reputation", (accounts) => { WAD32, ]); - // Set payout to WAD for expenditure slot 0, internal token + // Set recipient to USER0 const action2 = await encodeTxData(colony, "setExpenditureState", [ 1, UINT256_MAX, expenditureId, - 27, - [false, false], - ["0x0", bn2bytes32(new BN(token.address.slice(2), 16))], - WAD32, + 26, + [false, true], + ["0x0", bn2bytes32(new BN(0))], + USER0, ]); await voting.createMotion(1, UINT256_MAX, ADDRESS_ZERO, action1, domain1Key, domain1Value, domain1Mask, domain1Siblings); diff --git a/truffle.js b/truffle.js index 42f086151b..454e768caf 100644 --- a/truffle.js +++ b/truffle.js @@ -1,7 +1,7 @@ const HDWalletProvider = require("truffle-hdwallet-provider"); const ganache = require("ganache"); -const ganacheProvider = ganache.provider({ total_accounts: 14, seed: "smoketest" }); +const ganacheProvider = ganache.provider({ total_accounts: 14, seed: "smoketest", logging: { quiet: true } }); const LedgerWalletProvider = require("@umaprotocol/truffle-ledger-provider"); const ledgerOptions = { @@ -14,6 +14,28 @@ const ledgerOptions = { const DISABLE_DOCKER = !process.env.DISABLE_DOCKER; +const coverageOptimiserSettings = { + enabled: false, + runs: 200, + details: { + peephole: false, + jumpdestRemover: false, + orderLiterals: true, // <-- TRUE! Stack too deep when false + deduplicate: false, + cse: false, + constantOptimizer: false, + yul: true, + yulDetails: { + stackAllocation: true, + }, + }, +}; + +const normalOptimizerSettings = { + enabled: true, + runs: 200, +}; + module.exports = { networks: { development: { @@ -97,10 +119,7 @@ module.exports = { docker: DISABLE_DOCKER, parser: "solcjs", settings: { - optimizer: { - enabled: true, - runs: 200, - }, + optimizer: process.env.SOLIDITY_COVERAGE ? coverageOptimiserSettings : normalOptimizerSettings, evmVersion: "istanbul", }, }, diff --git a/yarn.lock b/yarn.lock index 8deaa55af2..73ba3934e7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10758,7 +10758,7 @@ semver@6.2.0: resolved "https://registry.npmjs.org/semver/-/semver-6.2.0.tgz" integrity sha512-jdFC1VdUGT/2Scgbimf7FSx9iJLXoqfglSF+gJeuNWVpiE37OIbc1jywR/GJyFdz3mnkz2/id0L0J/cr0izR5A== -semver@7.3.7, semver@^7.3.5: +semver@7.3.7, semver@^7.3.4, semver@^7.3.5: version "7.3.7" resolved "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz" integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==