From ca731fd922248829a643b940e2ebb55e4f2dfac0 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Thu, 26 May 2022 07:40:58 -0700 Subject: [PATCH 01/59] Add a new setExpenditurePayouts --- contracts/colony/ColonyFunding.sol | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/contracts/colony/ColonyFunding.sol b/contracts/colony/ColonyFunding.sol index 5ddbb5face..06f28f8b88 100755 --- a/contracts/colony/ColonyFunding.sol +++ b/contracts/colony/ColonyFunding.sol @@ -141,13 +141,27 @@ contract ColonyFunding is ColonyStorage { // ignore-swc-123 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); } + function setExpenditurePayouts( + uint256 _permissionDomainId, + uint256 _childSkillIndex, + uint256 _id, + uint256[] memory _slots, + address _token, + uint256[] memory _amounts + ) + public + stoppable + authDomain(_permissionDomainId, _childSkillIndex, expenditures[_id].domainId) + { + setExpenditurePayoutsInternal(_id, _slots, _token, _amounts); + } + function setExpenditurePayout(uint256 _id, uint256 _slot, address _token, uint256 _amount) public stoppable @@ -378,6 +392,7 @@ contract ColonyFunding is ColonyStorage { // ignore-swc-123 function setExpenditurePayoutsInternal(uint256 _id, uint256[] memory _slots, address _token, uint256[] memory _amounts) internal + expenditureExists(_id) { require(_slots.length == _amounts.length, "colony-expenditure-bad-slots"); From 67ceb3e547bdb0f450346bf80ba8baa96276891a Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Wed, 11 May 2022 17:32:22 -0400 Subject: [PATCH 02/59] Change implementation for updating expenditure payouts via arbitration --- contracts/colony/ColonyAuthority.sol | 5 ++- contracts/colony/ColonyExpenditure.sol | 5 --- contracts/colony/ColonyFunding.sol | 37 ++++++++++++++++++++ contracts/colony/IColony.sol | 22 ++++++++++++ test/contracts-network/colony-expenditure.js | 8 ++--- 5 files changed, 65 insertions(+), 12 deletions(-) diff --git a/contracts/colony/ColonyAuthority.sol b/contracts/colony/ColonyAuthority.sol index 8e8282ff20..f72dc8db9a 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 (g-lwss) + addRoleCapability(ARBITRATION_ROLE, "setExpenditurePayouts(uint256,uint256,uint256,uint256[],address,uint256[])"); } function addRoleCapability(uint8 role, bytes memory sig) private { diff --git a/contracts/colony/ColonyExpenditure.sol b/contracts/colony/ColonyExpenditure.sol index 2dc5bc5c10..ba3f42eb1a 100644 --- a/contracts/colony/ColonyExpenditure.sol +++ b/contracts/colony/ColonyExpenditure.sol @@ -268,7 +268,6 @@ contract ColonyExpenditure is ColonyStorage { uint256 constant EXPENDITURES_SLOT = 25; uint256 constant EXPENDITURESLOTS_SLOT = 26; - uint256 constant EXPENDITURESLOTPAYOUTS_SLOT = 27; function setExpenditureState( uint256 _permissionDomainId, @@ -306,10 +305,6 @@ 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"); } diff --git a/contracts/colony/ColonyFunding.sol b/contracts/colony/ColonyFunding.sol index 06f28f8b88..d226005753 100755 --- a/contracts/colony/ColonyFunding.sol +++ b/contracts/colony/ColonyFunding.sol @@ -26,6 +26,32 @@ contract ColonyFunding is ColonyStorage { // ignore-swc-123 // Public + function moveFundsBetweenPots( + uint256 _permissionDomainId, + uint256 _childSkillIndex, + uint256 _domainId, + uint256 _fromChildSkillIndex, + uint256 _toChildSkillIndex, + uint256 _fromPot, + uint256 _toPot, + uint256[] memory _amounts, + address[] memory _tokens + ) + public + stoppable + domainNotDeprecated(getDomainFromFundingPot(_toPot)) + 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(_amounts.length == _tokens.length, "colony-invalid-arguments"); + + for (uint256 i; i < _amounts.length; i++) { + moveFundsBetweenPotsFunctionality(_fromPot, _toPot, _amounts[i], _tokens[i]); + } + } + function moveFundsBetweenPots( uint256 _permissionDomainId, uint256 _childSkillIndex, @@ -435,6 +461,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(); @@ -465,4 +493,13 @@ contract ColonyFunding is ColonyStorage { // ignore-swc-123 emit PayoutClaimed(msgSender(), _fundingPotId, _token, remainder); } + + 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); + } + } } \ No newline at end of file diff --git a/contracts/colony/IColony.sol b/contracts/colony/IColony.sol index c35e65d403..9e42503ada 100644 --- a/contracts/colony/IColony.sol +++ b/contracts/colony/IColony.sol @@ -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,6 +437,7 @@ 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 @@ -440,6 +445,23 @@ interface IColony is ColonyDataTypes, IRecovery, IBasicMetaTransaction { function setExpenditurePayouts(uint256 _id, uint256[] memory _slots, address _token, uint256[] memory _amounts) external; /// @notice @deprecated + /// @notice Set the token payouts in given expenditure slots. 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 _slots Array of slots to set payouts + /// @param _token Address of the token, `0x0` value indicates Ether + /// @param _amounts Payout amounts + function setExpenditurePayouts( + uint256 _permissionDomainId, + uint256 _childSkillIndex, + uint256 _id, + uint256[] memory _slots, + address _token, + uint256[] memory _amounts + ) external; + + /// @notice Deprecated /// @notice Sets the skill on an expenditure slot. Can only be called by expenditure owner. /// @param _id Expenditure identifier /// @param _slot Number of the slot diff --git a/test/contracts-network/colony-expenditure.js b/test/contracts-network/colony-expenditure.js index b52c662bb2..68ca8e8194 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; @@ -1109,11 +1108,8 @@ contract("Colony Expenditure", (accounts) => { }); 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)); - - await colony.setExpenditureState(1, UINT256_MAX, expenditureId, EXPENDITURESLOTPAYOUTS_SLOT, mask, keys, value, { from: ARBITRATOR }); + const setExpenditurePayouts = colony.methods["setExpenditurePayouts(uint256,uint256,uint256,uint256[],address,uint256[])"]; + await setExpenditurePayouts(1, UINT256_MAX, expenditureId, [0], token.address, [100], { from: ARBITRATOR }); const expenditureSlotPayout = await colony.getExpenditureSlotPayout(expenditureId, 0, token.address); expect(expenditureSlotPayout).to.eq.BN(100); From 83891a4b190cc3d75f5476bfae47dcf460adcdcf Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Thu, 12 May 2022 13:09:58 -0400 Subject: [PATCH 03/59] Create ColonyRewards.sol, reorganize ColonyFunding, allow funding with multiple tokens at once --- contracts/colony/ColonyAuthority.sol | 3 ++- contracts/colony/ColonyExpenditure.sol | 3 ++- contracts/colony/ColonyFunding.sol | 2 +- contracts/colony/ColonyRewards.sol | 2 +- contracts/colony/IColony.sol | 23 +++++++++++++++++ test/contracts-network/colony-expenditure.js | 11 +++++--- test/contracts-network/colony-funding.js | 27 ++++++++++++++++++++ 7 files changed, 64 insertions(+), 7 deletions(-) diff --git a/contracts/colony/ColonyAuthority.sol b/contracts/colony/ColonyAuthority.sol index f72dc8db9a..2bdc29f8e2 100644 --- a/contracts/colony/ColonyAuthority.sol +++ b/contracts/colony/ColonyAuthority.sol @@ -125,8 +125,9 @@ contract ColonyAuthority is CommonAuthority { addRoleCapability(ARCHITECTURE_ROLE, "deprecateDomain(uint256,uint256,uint256,bool)"); addRoleCapability(ROOT_ROLE, "editColonyByDelta(string)"); - // Added in colony v10 (g-lwss) + // Added in colony v10 (ginger-lwss) addRoleCapability(ARBITRATION_ROLE, "setExpenditurePayouts(uint256,uint256,uint256,uint256[],address,uint256[])"); + addRoleCapability(FUNDING_ROLE, "moveFundsBetweenPots(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256[],address[])"); } function addRoleCapability(uint8 role, bytes memory sig) private { diff --git a/contracts/colony/ColonyExpenditure.sol b/contracts/colony/ColonyExpenditure.sol index ba3f42eb1a..e8c59181e5 100644 --- a/contracts/colony/ColonyExpenditure.sol +++ b/contracts/colony/ColonyExpenditure.sol @@ -283,7 +283,8 @@ contract ColonyExpenditure is ColonyStorage { expenditureExists(_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"); diff --git a/contracts/colony/ColonyFunding.sol b/contracts/colony/ColonyFunding.sol index d226005753..aa45ba2cc3 100755 --- a/contracts/colony/ColonyFunding.sol +++ b/contracts/colony/ColonyFunding.sol @@ -502,4 +502,4 @@ contract ColonyFunding is ColonyStorage { // ignore-swc-123 moveFundsBetweenPotsFunctionality(_fundingPotId, domains[domainId].fundingPotId, surplus, _token); } } -} \ No newline at end of file +} 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/IColony.sol b/contracts/colony/IColony.sol index 9e42503ada..edc6bdf34d 100644 --- a/contracts/colony/IColony.sol +++ b/contracts/colony/IColony.sol @@ -960,6 +960,29 @@ interface IColony is ColonyDataTypes, IRecovery, IBasicMetaTransaction { /// @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 + /// @param _amounts An array of the amounts of funds + /// @param _tokens An array of the addresses of the tokens, `0x0` value indicates Ether + function moveFundsBetweenPots( + uint256 _permissionDomainId, + uint256 _childSkillIndex, + uint256 _domainId, + uint256 _fromChildSkillIndex, + uint256 _toChildSkillIndex, + uint256 _fromPot, + uint256 _toPot, + uint256[] memory _amounts, + address[] memory _tokens + ) external; + + /// @notice Move a given amount: `_amount` of `_token` funds from funding pot with id `_fromPot` to one with id `_toPot`. + /// @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 _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 /// @param _amount Amount of funds /// @param _token Address of the token, `0x0` value indicates Ether function moveFundsBetweenPots( diff --git a/test/contracts-network/colony-expenditure.js b/test/contracts-network/colony-expenditure.js index 68ca8e8194..bef5e2de3a 100644 --- a/test/contracts-network/colony-expenditure.js +++ b/test/contracts-network/colony-expenditure.js @@ -358,7 +358,8 @@ contract("Colony Expenditure", (accounts) => { }); it("should allow owners to update many slot payouts at once", async () => { - await colony.setExpenditurePayouts(expenditureId, [SLOT1, SLOT2], token.address, [10, 20], { from: ADMIN }); + const setExpenditurePayouts = colony.methods["setExpenditurePayouts(uint256,uint256[],address,uint256[])"]; + await setExpenditurePayouts(expenditureId, [SLOT1, SLOT2], token.address, [10, 20], { from: ADMIN }); let payout; payout = await colony.getExpenditureSlotPayout(expenditureId, SLOT0, token.address); @@ -370,8 +371,10 @@ contract("Colony Expenditure", (accounts) => { }); it("should not allow owners to update many slot payouts with mismatched arguments", async () => { + const setExpenditurePayouts = colony.methods["setExpenditurePayouts(uint256,uint256[],address,uint256[])"]; + await checkErrorRevert( - colony.setExpenditurePayouts(expenditureId, [SLOT0, SLOT1], token.address, [WAD], { from: ADMIN }), + setExpenditurePayouts(expenditureId, [SLOT0, SLOT1], token.address, [WAD], { from: ADMIN }), "colony-expenditure-bad-slots" ); }); @@ -496,8 +499,10 @@ contract("Colony Expenditure", (accounts) => { }); it("should not allow the owner to set payouts", async () => { + const setExpenditurePayouts = colony.methods["setExpenditurePayouts(uint256,uint256[],address,uint256[])"]; + await checkErrorRevert( - colony.setExpenditurePayouts(expenditureId, [SLOT0], token.address, [WAD], { from: ADMIN }), + setExpenditurePayouts(expenditureId, [SLOT0], token.address, [WAD], { from: ADMIN }), "colony-expenditure-not-draft" ); }); diff --git a/test/contracts-network/colony-funding.js b/test/contracts-network/colony-funding.js index 7ba146902d..b8df5697bd 100755 --- a/test/contracts-network/colony-funding.js +++ b/test/contracts-network/colony-funding.js @@ -96,6 +96,33 @@ contract("Colony Funding", (accounts) => { expect(pot2Balance).to.eq.BN(51); }); + it("should let multiple tokens be moved between funding pots at once", async () => { + await fundColonyWithTokens(colony, token, 100); + await fundColonyWithTokens(colony, otherToken, 200); + + await colony.addDomain(1, UINT256_MAX, 1); + + const sig = "moveFundsBetweenPots(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256[],address[])"; + const moveFundsBetweenPots = colony.methods[sig]; + await moveFundsBetweenPots(1, UINT256_MAX, 1, UINT256_MAX, 0, 1, 2, [50, 100], [token.address, otherToken.address]); + + const colonyTokenBalance = await token.balanceOf(colony.address); + const colonyOtherTokenBalance = await otherToken.balanceOf(colony.address); + + const potTokenBalance = await colony.getFundingPotBalance(1, token.address); + const potOtherTokenBalance = await colony.getFundingPotBalance(1, otherToken.address); + + const pot2TokenBalance = await colony.getFundingPotBalance(2, token.address); + const pot2OtherTokenBalance = await colony.getFundingPotBalance(2, otherToken.address); + + expect(colonyTokenBalance).to.eq.BN(100); + expect(colonyOtherTokenBalance).to.eq.BN(200); + expect(potTokenBalance).to.eq.BN(49); + expect(potOtherTokenBalance).to.eq.BN(98); + expect(pot2TokenBalance).to.eq.BN(50); + expect(pot2OtherTokenBalance).to.eq.BN(100); + }); + it("when moving tokens between pots, should respect permission inheritance", async () => { await removeSubdomainLimit(colonyNetwork); // Temporary for tests until we allow subdomain depth > 1 await fundColonyWithTokens(colony, otherToken, 100); From 9fdf2b83456990da1ee3e7def4a1855aca265576 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Fri, 13 May 2022 16:12:05 -0400 Subject: [PATCH 04/59] Add automatic refund functionality --- test/contracts-network/colony-expenditure.js | 29 ++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/test/contracts-network/colony-expenditure.js b/test/contracts-network/colony-expenditure.js index bef5e2de3a..a379d43e9d 100644 --- a/test/contracts-network/colony-expenditure.js +++ b/test/contracts-network/colony-expenditure.js @@ -746,6 +746,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 }); From 039d8044b32e36dcd9f12ace3517128e88893bed Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Tue, 24 May 2022 12:16:18 -0700 Subject: [PATCH 05/59] Rename EvaluatedExpenditure to ExpenditureUtils, add staking functionality --- ...edExpenditure.sol => ExpenditureUtils.sol} | 115 +++++++-- migrations/9_setup_extensions.js | 4 +- test/contracts-network/colony-expenditure.js | 2 +- test/extensions/evaluated-expenditures.js | 141 ----------- test/extensions/expenditure-utils.js | 226 ++++++++++++++++++ 5 files changed, 330 insertions(+), 158 deletions(-) rename contracts/extensions/{EvaluatedExpenditure.sol => ExpenditureUtils.sol} (51%) delete mode 100644 test/extensions/evaluated-expenditures.js create mode 100644 test/extensions/expenditure-utils.js diff --git a/contracts/extensions/EvaluatedExpenditure.sol b/contracts/extensions/ExpenditureUtils.sol similarity index 51% rename from contracts/extensions/EvaluatedExpenditure.sol rename to contracts/extensions/ExpenditureUtils.sol index 1f47a871b2..286d98e640 100644 --- a/contracts/extensions/EvaluatedExpenditure.sol +++ b/contracts/extensions/ExpenditureUtils.sol @@ -18,28 +18,39 @@ pragma solidity 0.7.3; pragma experimental ABIEncoderV2; -import "./ColonyExtension.sol"; -import "./../common/BasicMetaTransaction.sol"; +import "./../colony/ColonyDataTypes.sol"; +import "./../colonyNetwork/IColonyNetwork.sol"; +import "./../patriciaTree/PatriciaTreeProofs.sol"; +import "./ColonyExtensionMeta.sol"; // ignore-file-swc-108 -contract EvaluatedExpenditure is ColonyExtension, BasicMetaTransaction { +contract ExpenditureUtils is ColonyExtensionMeta, PatriciaTreeProofs { - uint256 constant EXPENDITURESLOTS_SLOT = 26; - uint256 constant PAYOUT_MODIFIER_OFFSET = 2; - bool constant MAPPING = false; - bool constant ARRAY = true; - mapping(address => uint256) metatransactionNonces; + // Storage + + uint256 stakeFraction; + + mapping (address => mapping (uint256 => uint256)) stakes; + + // Modifiers + + modifier onlyRoot() { + require(colony.hasUserRole(msgSender(), 1, ColonyDataTypes.ColonyRole.Root), "voting-rep-caller-not-root"); + _; + } + + // Overrides /// @notice Returns the identifier of the extension function identifier() public override pure returns (bytes32) { - return keccak256("EvaluatedExpenditure"); + return keccak256("ExpenditureUtils"); } /// @notice Returns the version of the extension function version() public override pure returns (uint256) { - return 2; + return 1; } /// @notice Configures the extension @@ -63,14 +74,54 @@ contract EvaluatedExpenditure is ColonyExtension, BasicMetaTransaction { selfdestruct(address(uint160(address(colony)))); } - function getMetatransactionNonce(address _userAddress) override public view returns (uint256 nonce){ - return metatransactionNonces[_userAddress]; + // Public + + function setStakeFraction(uint256 _stakeFraction) public onlyRoot { + stakeFraction = _stakeFraction; } - function incrementMetatransactionNonce(address _user) override internal { - metatransactionNonces[_user] = add(metatransactionNonces[_user], 1); + function makeExpenditureWithStake( + uint256 _permissionDomainId, + uint256 _childSkillIndex, + uint256 _domainId, + bytes memory _key, + bytes memory _value, + uint256 _branchMask, + bytes32[] memory _siblings + ) + public + { + uint256 domainRep = getReputationFromProof(_domainId, _key, _value, _branchMask, _siblings); + uint256 stake = wmul(domainRep, stakeFraction); + + colony.obligateStake(msgSender(), _domainId, stake); + colony.makeExpenditure(_permissionDomainId, _childSkillIndex, _domainId); + + uint256 expenditureId = colony.getExpenditureCount(); + stakes[msgSender()][expenditureId] = stake; + colony.transferExpenditure(expenditureId, msgSender()); } + function reclaimStake(uint256 _expenditureId) public { + uint256 stake = stakes[msgSender()][_expenditureId]; + require(stake > 0, "expenditure-utils-nothing-to-claim"); + + ColonyDataTypes.Expenditure memory expenditure = colony.getExpenditure(_expenditureId); + require( + expenditure.status == ColonyDataTypes.ExpenditureStatus.Cancelled || + expenditure.status == ColonyDataTypes.ExpenditureStatus.Finalized, + "expenditure-utils-expenditure-invalid-state" + ); + + delete stakes[msgSender()][_expenditureId]; + colony.deobligateStake(msgSender(), expenditure.domainId, stake); + } + + uint256 constant EXPENDITURESLOTS_SLOT = 26; + uint256 constant PAYOUT_MODIFIER_OFFSET = 2; + bool constant MAPPING = false; + bool constant ARRAY = true; + /// @notice Sets the payout modifiers in given expenditure slots, using the arbitration permission /// @param _permissionDomainId The domainId in which the extension has the arbitration permission /// @param _childSkillIndex The index that the `_domainId` is relative to `_permissionDomainId` @@ -112,4 +163,40 @@ contract EvaluatedExpenditure is ColonyExtension, BasicMetaTransaction { } } +// Internal + +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, "expenditure-utils-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), "expenditure-utils-invalid-colony-address"); + require(keySkillId == colony.getDomain(_domainId).skillId, "expenditure-utils-invalid-skill-id"); + require(keyUserAddress == address(0x0), "expenditure-utils-invalid-user-address"); + + return reputationValue; + } } diff --git a/migrations/9_setup_extensions.js b/migrations/9_setup_extensions.js index 3b6337437a..4cbb7f1fcb 100644 --- a/migrations/9_setup_extensions.js +++ b/migrations/9_setup_extensions.js @@ -5,7 +5,7 @@ const { soliditySha3 } = require("web3-utils"); const { setupEtherRouter } = require("../helpers/upgradable-contracts"); const CoinMachine = artifacts.require("./CoinMachine"); -const EvaluatedExpenditure = artifacts.require("./EvaluatedExpenditure"); +const ExpenditureUtils = artifacts.require("./ExpenditureUtils"); const FundingQueue = artifacts.require("./FundingQueue"); const OneTxPayment = artifacts.require("./OneTxPayment"); const StreamingPayments = artifacts.require("./StreamingPayments"); @@ -37,7 +37,7 @@ module.exports = async function (deployer, network, accounts) { const colonyNetwork = await IColonyNetwork.at(etherRouterDeployed.address); await addExtension(colonyNetwork, "CoinMachine", CoinMachine); - await addExtension(colonyNetwork, "EvaluatedExpenditure", EvaluatedExpenditure); + await addExtension(colonyNetwork, "ExpenditureUtils", ExpenditureUtils); await addExtension(colonyNetwork, "FundingQueue", FundingQueue); await addExtension(colonyNetwork, "OneTxPayment", OneTxPayment); await addExtension(colonyNetwork, "StreamingPayments", StreamingPayments); diff --git a/test/contracts-network/colony-expenditure.js b/test/contracts-network/colony-expenditure.js index a379d43e9d..db120cebe6 100644 --- a/test/contracts-network/colony-expenditure.js +++ b/test/contracts-network/colony-expenditure.js @@ -31,7 +31,7 @@ const Token = artifacts.require("Token"); const TestExtension0 = artifacts.require("TestExtension0"); const Resolver = artifacts.require("Resolver"); -contract("Colony Expenditure", (accounts) => { +contract.only("Colony Expenditure", (accounts) => { const SLOT0 = 0; const SLOT1 = 1; const SLOT2 = 2; diff --git a/test/extensions/evaluated-expenditures.js b/test/extensions/evaluated-expenditures.js deleted file mode 100644 index 5975e30c8a..0000000000 --- a/test/extensions/evaluated-expenditures.js +++ /dev/null @@ -1,141 +0,0 @@ -/* globals artifacts */ - -const chai = require("chai"); -const bnChai = require("bn-chai"); -const { ethers } = require("ethers"); -const { soliditySha3 } = require("web3-utils"); - -const { UINT256_MAX, WAD, ADDRESS_ZERO } = require("../../helpers/constants"); -const { checkErrorRevert, web3GetCode } = require("../../helpers/test-helper"); -const { setupRandomColony, getMetaTransactionParameters } = require("../../helpers/test-data-generator"); - -const { expect } = chai; -chai.use(bnChai(web3.utils.BN)); - -const IColonyNetwork = artifacts.require("IColonyNetwork"); -const EtherRouter = artifacts.require("EtherRouter"); -const EvaluatedExpenditure = artifacts.require("EvaluatedExpenditure"); - -const EVALUATED_EXPENDITURE = soliditySha3("EvaluatedExpenditure"); - -contract("EvaluatedExpenditure", (accounts) => { - let colonyNetwork; - let colony; - let evaluatedExpenditure; - let version; - - const USER0 = accounts[0]; - const USER1 = accounts[1]; - - before(async () => { - const etherRouter = await EtherRouter.deployed(); - colonyNetwork = await IColonyNetwork.at(etherRouter.address); - - const extension = await EvaluatedExpenditure.new(); - version = await extension.version(); - }); - - beforeEach(async () => { - ({ colony } = await setupRandomColony(colonyNetwork)); - - await colony.installExtension(EVALUATED_EXPENDITURE, version); - - const evaluatedExpenditureAddress = await colonyNetwork.getExtensionInstallation(EVALUATED_EXPENDITURE, colony.address); - evaluatedExpenditure = await EvaluatedExpenditure.at(evaluatedExpenditureAddress); - - await colony.setArbitrationRole(1, UINT256_MAX, evaluatedExpenditure.address, 1, true); - }); - - describe("managing the extension", async () => { - it("can install the extension manually", async () => { - evaluatedExpenditure = await EvaluatedExpenditure.new(); - await evaluatedExpenditure.install(colony.address); - - await checkErrorRevert(evaluatedExpenditure.install(colony.address), "extension-already-installed"); - - const identifier = await evaluatedExpenditure.identifier(); - expect(identifier).to.equal(EVALUATED_EXPENDITURE); - - const capabilityRoles = await evaluatedExpenditure.getCapabilityRoles("0x0"); - expect(capabilityRoles).to.equal(ethers.constants.HashZero); - - await evaluatedExpenditure.finishUpgrade(); - await evaluatedExpenditure.deprecate(true); - await evaluatedExpenditure.uninstall(); - - const code = await web3GetCode(evaluatedExpenditure.address); - expect(code).to.equal("0x"); - }); - - it("can install the extension with the extension manager", async () => { - ({ colony } = await setupRandomColony(colonyNetwork)); - await colony.installExtension(EVALUATED_EXPENDITURE, version, { from: USER0 }); - - await checkErrorRevert(colony.installExtension(EVALUATED_EXPENDITURE, version, { from: USER0 }), "colony-network-extension-already-installed"); - await checkErrorRevert(colony.uninstallExtension(EVALUATED_EXPENDITURE, { from: USER1 }), "ds-auth-unauthorized"); - - await colony.uninstallExtension(EVALUATED_EXPENDITURE, { from: USER0 }); - }); - - it("can't use the network-level functions if installed via ColonyNetwork", async () => { - await checkErrorRevert(evaluatedExpenditure.install(ADDRESS_ZERO, { from: USER1 }), "ds-auth-unauthorized"); - await checkErrorRevert(evaluatedExpenditure.finishUpgrade({ from: USER1 }), "ds-auth-unauthorized"); - await checkErrorRevert(evaluatedExpenditure.deprecate(true, { from: USER1 }), "ds-auth-unauthorized"); - await checkErrorRevert(evaluatedExpenditure.uninstall({ from: USER1 }), "ds-auth-unauthorized"); - }); - }); - - describe("using the extension", async () => { - let expenditureId; - - beforeEach(async () => { - await colony.makeExpenditure(1, UINT256_MAX, 1); - expenditureId = await colony.getExpenditureCount(); - - await colony.lockExpenditure(expenditureId); - }); - - it("can set the payout modifier in the locked state", async () => { - let expenditureSlot; - - expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); - expect(expenditureSlot.payoutModifier).to.be.zero; - - await evaluatedExpenditure.setExpenditurePayoutModifiers(1, UINT256_MAX, expenditureId, [0], [WAD], { from: USER0 }); - - expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); - expect(expenditureSlot.payoutModifier).to.eq.BN(WAD); - }); - - it("cannot set the payout modifier with bad arguments", async () => { - await checkErrorRevert( - evaluatedExpenditure.setExpenditurePayoutModifiers(1, UINT256_MAX, expenditureId, [0], [], { from: USER0 }), - "evaluated-expenditure-bad-slots" - ); - }); - - it("cannot set the payout modifier if not the owner", async () => { - await checkErrorRevert( - evaluatedExpenditure.setExpenditurePayoutModifiers(1, UINT256_MAX, expenditureId, [0], [WAD], { from: USER1 }), - "evaluated-expenditure-not-owner" - ); - }); - - it("can set the payout modifier via metatransaction", async () => { - const txData = await evaluatedExpenditure.contract.methods - .setExpenditurePayoutModifiers(1, UINT256_MAX.toString(), expenditureId.toString(), [0], [WAD.toString()]) - .encodeABI(); - - const { r, s, v } = await getMetaTransactionParameters(txData, USER0, evaluatedExpenditure.address); - - let expenditureSlot; - expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); - expect(expenditureSlot.payoutModifier).to.be.zero; - - await evaluatedExpenditure.executeMetaTransaction(USER0, txData, r, s, v, { from: USER1 }); - - expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); - expect(expenditureSlot.payoutModifier).to.eq.BN(WAD); - }); - }); -}); diff --git a/test/extensions/expenditure-utils.js b/test/extensions/expenditure-utils.js new file mode 100644 index 0000000000..c23315c209 --- /dev/null +++ b/test/extensions/expenditure-utils.js @@ -0,0 +1,226 @@ +/* globals artifacts */ + +import chai from "chai"; +import bnChai from "bn-chai"; +import { ethers } from "ethers"; +import { soliditySha3 } from "web3-utils"; + +import { UINT256_MAX, WAD, MINING_CYCLE_DURATION, CHALLENGE_RESPONSE_WINDOW_DURATION } from "../../helpers/constants"; +import { setupRandomColony, getMetaTransactionParameters } from "../../helpers/test-data-generator"; +import { checkErrorRevert, web3GetCode, makeReputationKey, makeReputationValue, getActiveRepCycle, forwardTime } from "../../helpers/test-helper"; + +import PatriciaTree from "../../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 ExpenditureUtils = artifacts.require("ExpenditureUtils"); + +const EXPENDITURE_UTILS = soliditySha3("ExpenditureUtils"); + +contract("ExpenditureUtils", (accounts) => { + let colonyNetwork; + let colony; + let token; + let tokenLocking; + let expenditureUtils; + let version; + + let reputationTree; + let domain1Key; + let domain1Value; + let domain1Mask; + let domain1Siblings; + + const USER0 = accounts[0]; + const USER1 = accounts[1]; + const MINER = accounts[5]; + + 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 ExpenditureUtils.new(); + version = await extension.version(); + }); + + beforeEach(async () => { + ({ colony, token } = await setupRandomColony(colonyNetwork)); + + await colony.installExtension(EXPENDITURE_UTILS, version); + + const expenditureUtilsAddress = await colonyNetwork.getExtensionInstallation(EXPENDITURE_UTILS, colony.address); + expenditureUtils = await ExpenditureUtils.at(expenditureUtilsAddress); + + await colony.setArbitrationRole(1, UINT256_MAX, expenditureUtils.address, 1, true); + await colony.setAdministrationRole(1, UINT256_MAX, expenditureUtils.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) + ); + + 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 () => { + expenditureUtils = await ExpenditureUtils.new(); + await expenditureUtils.install(colony.address); + + await checkErrorRevert(expenditureUtils.install(colony.address), "extension-already-installed"); + + const identifier = await expenditureUtils.identifier(); + expect(identifier).to.equal(EXPENDITURE_UTILS); + + const capabilityRoles = await expenditureUtils.getCapabilityRoles("0x0"); + expect(capabilityRoles).to.equal(ethers.constants.HashZero); + + await expenditureUtils.finishUpgrade(); + await expenditureUtils.deprecate(true); + await expenditureUtils.uninstall(); + + const code = await web3GetCode(expenditureUtils.address); + expect(code).to.equal("0x"); + }); + + it("can install the extension with the extension manager", async () => { + ({ colony } = await setupRandomColony(colonyNetwork)); + await colony.installExtension(EXPENDITURE_UTILS, version, { from: USER0 }); + + await checkErrorRevert(colony.installExtension(EXPENDITURE_UTILS, version, { from: USER0 }), "colony-network-extension-already-installed"); + await checkErrorRevert(colony.uninstallExtension(EXPENDITURE_UTILS, { from: USER1 }), "ds-auth-unauthorized"); + + await colony.uninstallExtension(EXPENDITURE_UTILS, { from: USER0 }); + }); + }); + + describe.only("using stakes to manage expenditures", async () => { + beforeEach(async () => { + await expenditureUtils.setStakeFraction(WAD.divn(10)); // Stake of .3 WADs + + 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(expenditureUtils.address, 1, WAD, { from: USER0 }); + }); + + it("can create an expenditure by submitting a stake", async () => { + await expenditureUtils.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(WAD.muln(3).divn(10)); + }); + + it("can reclaim the stake by cancelling the expenditure", async () => { + await expenditureUtils.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); + const expenditureId = await colony.getExpenditureCount(); + + await colony.cancelExpenditure(expenditureId); + + await expenditureUtils.reclaimStake(expenditureId); + + const obligation = await tokenLocking.getObligation(USER0, token.address, colony.address); + expect(obligation).to.be.zero; + }); + + it("can reclaim the stake by finalizing the expenditure", async () => { + await expenditureUtils.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); + const expenditureId = await colony.getExpenditureCount(); + + await colony.finalizeExpenditure(expenditureId); + + await expenditureUtils.reclaimStake(expenditureId); + + const obligation = await tokenLocking.getObligation(USER0, token.address, colony.address); + expect(obligation).to.be.zero; + }); + + it("cannot reclaim the stake while the expenditure is in progress", async () => { + await expenditureUtils.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); + const expenditureId = await colony.getExpenditureCount(); + + await checkErrorRevert(expenditureUtils.reclaimStake(expenditureId), "expenditure-utils-expenditure-invalid-state"); + }); + + it("cannot reclaim a nonexistent stake", async () => { + await checkErrorRevert(expenditureUtils.reclaimStake(0), "expenditure-utils-nothing-to-claim"); + }); + }); + + describe("setting the payout modifiers with arbitration", async () => { + let expenditureId; + + beforeEach(async () => { + await colony.makeExpenditure(1, UINT256_MAX, 1); + constexpenditureId = await colony.getExpenditureCount(); + + await colony.lockExpenditure(expenditureId); + }); + + it("can set the payout modifier in the locked state", async () => { + let expenditureSlot; + + expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); + expect(expenditureSlot.payoutModifier).to.be.zero; + + await expenditureUtils.setExpenditurePayoutModifiers(1, UINT256_MAX, expenditureId, [0], [WAD], { from: USER0 }); + + expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); + expect(expenditureSlot.payoutModifier).to.eq.BN(WAD); + }); + + it("cannot set the payout modifier with bad arguments", async () => { + await checkErrorRevert( + expenditureUtils.setExpenditurePayoutModifiers(1, UINT256_MAX, expenditureId, [0], [], { from: USER0 }), + "evaluated-expenditure-bad-slots" + ); + }); + + it("cannot set the payout modifier if not the owner", async () => { + await checkErrorRevert( + expenditureUtils.setExpenditurePayoutModifiers(1, UINT256_MAX, expenditureId, [0], [WAD], { from: USER1 }), + "evaluated-expenditure-not-owner" + ); + }); + + it("can set the payout modifier via metatransaction", async () => { + const txData = await expenditureUtils.contract.methods + .setExpenditurePayoutModifiers(1, UINT256_MAX.toString(), expenditureId.toString(), [0], [WAD.toString()]) + .encodeABI(); + + const { r, s, v } = await getMetaTransactionParameters(txData, USER0, expenditureUtils.address); + + let expenditureSlot; + expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); + expect(expenditureSlot.payoutModifier).to.be.zero; + + await expenditureUtils.executeMetaTransaction(USER0, txData, r, s, v, { from: USER1 }); + + expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); + expect(expenditureSlot.payoutModifier).to.eq.BN(WAD); + }); + }); +}); From 8bcb05be07badcf4ce870326a4f946feb3584239 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Tue, 24 May 2022 21:01:09 -0700 Subject: [PATCH 06/59] Add setExpenditureValues --- contracts/colony/ColonyExpenditure.sol | 40 +++++++++++++ contracts/colony/ColonyStorage.sol | 2 +- contracts/colony/IColony.sol | 27 +++++++++ test/contracts-network/colony-expenditure.js | 60 +++++++++++++++++++- 4 files changed, 127 insertions(+), 2 deletions(-) diff --git a/contracts/colony/ColonyExpenditure.sol b/contracts/colony/ColonyExpenditure.sol index e8c59181e5..c11238b892 100644 --- a/contracts/colony/ColonyExpenditure.sol +++ b/contracts/colony/ColonyExpenditure.sol @@ -230,6 +230,33 @@ 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 + expenditureExists(_id) + expenditureDraft(_id) + expenditureOnlyOwner(_id) + { + setExpenditureRecipients(_id, _recipientSlots, _recipients); + setExpenditureSkills(_id, _skillIdSlots, _skillIds); + setExpenditureClaimDelays(_id, _claimDelaySlots, _claimDelays); + setExpenditurePayoutModifiers(_id, _payoutModifierSlots, _payoutModifiers); + setExpenditurePayouts(_id, _payoutTokens, _payoutSlots, _payoutValues); + } + // Deprecated function setExpenditureRecipient(uint256 _id, uint256 _slot, address payable _recipient) public @@ -333,6 +360,19 @@ contract ColonyExpenditure is ColonyStorage { // Internal functions + function setExpenditurePayouts( + uint256 _id, + address[] memory _payoutTokens, + uint256[][] memory _payoutSlots, + uint256[][] memory _payoutValues + ) + internal + { + for (uint256 i; i < _payoutTokens.length; i++) { + IColony(address(this)).setExpenditurePayouts(_id, _payoutSlots[i], _payoutTokens[i], _payoutValues[i]); + } + } + bool constant MAPPING = false; bool constant ARRAY = true; uint256 constant MAX_ARRAY = 1024; // Prevent writing arbitrary slots diff --git a/contracts/colony/ColonyStorage.sol b/contracts/colony/ColonyStorage.sol index 266aaf9a37..4adc1d1ca6 100755 --- a/contracts/colony/ColonyStorage.sol +++ b/contracts/colony/ColonyStorage.sol @@ -194,7 +194,7 @@ contract ColonyStorage is ColonyDataTypes, ColonyNetworkDataTypes, DSMath, Commo } modifier expenditureOnlyOwner(uint256 _id) { - require(expenditures[_id].owner == msgSender(), "colony-expenditure-not-owner"); + require(expenditures[_id].owner == msgSender() || address(this) == msgSender(), "colony-expenditure-not-owner"); _; } diff --git a/contracts/colony/IColony.sol b/contracts/colony/IColony.sol index edc6bdf34d..f783357557 100644 --- a/contracts/colony/IColony.sol +++ b/contracts/colony/IColony.sol @@ -493,6 +493,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`, diff --git a/test/contracts-network/colony-expenditure.js b/test/contracts-network/colony-expenditure.js index db120cebe6..ddd4b084c3 100644 --- a/test/contracts-network/colony-expenditure.js +++ b/test/contracts-network/colony-expenditure.js @@ -31,7 +31,7 @@ const Token = artifacts.require("Token"); const TestExtension0 = artifacts.require("TestExtension0"); const Resolver = artifacts.require("Resolver"); -contract.only("Colony Expenditure", (accounts) => { +contract("Colony Expenditure", (accounts) => { const SLOT0 = 0; const SLOT1 = 1; const SLOT2 = 2; @@ -445,6 +445,64 @@ contract.only("Colony Expenditure", (accounts) => { totalPayout = await colony.getFundingPotPayout(expenditure.fundingPotId, token.address); expect(totalPayout).to.eq.BN(WAD); }); + + 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)); + }); }); describe("when locking expenditures", () => { From 7de9f42764859050c281dbd7a0a9db63fa5486a2 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Tue, 24 May 2022 21:49:21 -0700 Subject: [PATCH 07/59] Add ability to slash expenditure stakes --- contracts/extensions/ExpenditureUtils.sol | 22 +++++++++++++++++++++- test/extensions/expenditure-utils.js | 21 ++++++++++++++++++++- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/contracts/extensions/ExpenditureUtils.sol b/contracts/extensions/ExpenditureUtils.sol index 286d98e640..3bd2ee77f2 100644 --- a/contracts/extensions/ExpenditureUtils.sol +++ b/contracts/extensions/ExpenditureUtils.sol @@ -37,7 +37,7 @@ contract ExpenditureUtils is ColonyExtensionMeta, PatriciaTreeProofs { // Modifiers modifier onlyRoot() { - require(colony.hasUserRole(msgSender(), 1, ColonyDataTypes.ColonyRole.Root), "voting-rep-caller-not-root"); + require(colony.hasUserRole(msgSender(), 1, ColonyDataTypes.ColonyRole.Root), "evaluated-expenditure-caller-not-root"); _; } @@ -102,6 +102,26 @@ contract ExpenditureUtils is ColonyExtensionMeta, PatriciaTreeProofs { colony.transferExpenditure(expenditureId, msgSender()); } + function slashStake( + uint256 _permissionDomainId, + uint256 _childSkillIndex, + uint256 _expenditureId + ) + public + { + ColonyDataTypes.Expenditure memory expenditure = colony.getExpenditure(_expenditureId); + uint256 stake = stakes[expenditure.owner][_expenditureId]; + require(stake > 0, "expenditure-utils-nothing-to-claim"); + + require( + colony.hasInheritedUserRole(msgSender(), _permissionDomainId, ColonyDataTypes.ColonyRole.Arbitration, _childSkillIndex, expenditure.domainId), + "expenditure-utils-caller-not-arbitration" + ); + + delete stakes[expenditure.owner][_expenditureId]; + colony.transferStake(_permissionDomainId, _childSkillIndex, address(this), expenditure.owner, expenditure.domainId, stake, address(0x0)); + } + function reclaimStake(uint256 _expenditureId) public { uint256 stake = stakes[msgSender()][_expenditureId]; require(stake > 0, "expenditure-utils-nothing-to-claim"); diff --git a/test/extensions/expenditure-utils.js b/test/extensions/expenditure-utils.js index c23315c209..7bd363541a 100644 --- a/test/extensions/expenditure-utils.js +++ b/test/extensions/expenditure-utils.js @@ -113,7 +113,7 @@ contract("ExpenditureUtils", (accounts) => { }); }); - describe.only("using stakes to manage expenditures", async () => { + describe("using stakes to manage expenditures", async () => { beforeEach(async () => { await expenditureUtils.setStakeFraction(WAD.divn(10)); // Stake of .3 WADs @@ -134,6 +134,19 @@ contract("ExpenditureUtils", (accounts) => { expect(obligation).to.eq.BN(WAD.muln(3).divn(10)); }); + it("can slash the stake with the arbitration permission", async () => { + await expenditureUtils.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); + const expenditureId = await colony.getExpenditureCount(); + + await expenditureUtils.slashStake(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.sub(WAD.muln(3).divn(10))); + }); + it("can reclaim the stake by cancelling the expenditure", async () => { await expenditureUtils.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); const expenditureId = await colony.getExpenditureCount(); @@ -144,6 +157,9 @@ contract("ExpenditureUtils", (accounts) => { 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 reclaim the stake by finalizing the expenditure", async () => { @@ -156,6 +172,9 @@ contract("ExpenditureUtils", (accounts) => { 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 () => { From 4dbb0d962540eec76dd44d35705d7551a932a1cb Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Wed, 25 May 2022 02:25:12 -0700 Subject: [PATCH 08/59] Fix linting --- test/contracts-network/colony-expenditure.js | 9 +++------ test/extensions/expenditure-utils.js | 2 +- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/test/contracts-network/colony-expenditure.js b/test/contracts-network/colony-expenditure.js index ddd4b084c3..6d439bb4ba 100644 --- a/test/contracts-network/colony-expenditure.js +++ b/test/contracts-network/colony-expenditure.js @@ -460,11 +460,11 @@ contract("Colony Expenditure", (accounts) => { [token.address, otherToken.address], [ [SLOT0, SLOT1], - [SLOT1, SLOT2] + [SLOT1, SLOT2], ], [ [WAD.muln(10), WAD.muln(20)], - [WAD.muln(30), WAD.muln(40)] + [WAD.muln(30), WAD.muln(40)], ], { from: ADMIN } ); @@ -559,10 +559,7 @@ contract("Colony Expenditure", (accounts) => { it("should not allow the owner to set payouts", async () => { const setExpenditurePayouts = colony.methods["setExpenditurePayouts(uint256,uint256[],address,uint256[])"]; - await checkErrorRevert( - setExpenditurePayouts(expenditureId, [SLOT0], token.address, [WAD], { from: ADMIN }), - "colony-expenditure-not-draft" - ); + await checkErrorRevert(setExpenditurePayouts(expenditureId, [SLOT0], token.address, [WAD], { from: ADMIN }), "colony-expenditure-not-draft"); }); }); diff --git a/test/extensions/expenditure-utils.js b/test/extensions/expenditure-utils.js index 7bd363541a..7a659d9959 100644 --- a/test/extensions/expenditure-utils.js +++ b/test/extensions/expenditure-utils.js @@ -194,7 +194,7 @@ contract("ExpenditureUtils", (accounts) => { beforeEach(async () => { await colony.makeExpenditure(1, UINT256_MAX, 1); - constexpenditureId = await colony.getExpenditureCount(); + expenditureId = await colony.getExpenditureCount(); await colony.lockExpenditure(expenditureId); }); From f1abfbc740d5ccb964fb772b957e6c7c817d24f4 Mon Sep 17 00:00:00 2001 From: Alex Rea Date: Fri, 27 May 2022 14:27:02 +0100 Subject: [PATCH 09/59] Fix coverage --- truffle.js | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/truffle.js b/truffle.js index 42f086151b..c1a704569f 100644 --- a/truffle.js +++ b/truffle.js @@ -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", }, }, From 1fba3365455f529090d9959aa6570e7022e9111f Mon Sep 17 00:00:00 2001 From: Alex Rea Date: Fri, 27 May 2022 14:33:40 +0100 Subject: [PATCH 10/59] Fix slither, eslint checks --- contracts/extensions/ExpenditureUtils.sol | 3 +-- scripts/check-recovery.js | 2 +- scripts/check-storage.js | 2 +- test/contracts-network/colony-expenditure.js | 5 ----- 4 files changed, 3 insertions(+), 9 deletions(-) diff --git a/contracts/extensions/ExpenditureUtils.sol b/contracts/extensions/ExpenditureUtils.sol index 3bd2ee77f2..c7e39062b2 100644 --- a/contracts/extensions/ExpenditureUtils.sol +++ b/contracts/extensions/ExpenditureUtils.sol @@ -95,9 +95,8 @@ contract ExpenditureUtils is ColonyExtensionMeta, PatriciaTreeProofs { uint256 stake = wmul(domainRep, stakeFraction); colony.obligateStake(msgSender(), _domainId, stake); - colony.makeExpenditure(_permissionDomainId, _childSkillIndex, _domainId); + uint256 expenditureId = colony.makeExpenditure(_permissionDomainId, _childSkillIndex, _domainId); - uint256 expenditureId = colony.getExpenditureCount(); stakes[msgSender()][expenditureId] = stake; colony.transferExpenditure(expenditureId, msgSender()); } diff --git a/scripts/check-recovery.js b/scripts/check-recovery.js index 815de9a6e6..12ca19b476 100755 --- a/scripts/check-recovery.js +++ b/scripts/check-recovery.js @@ -47,7 +47,7 @@ walkSync("./contracts/").forEach((contractName) => { "contracts/extensions/CoinMachine.sol", "contracts/extensions/ColonyExtension.sol", "contracts/extensions/ColonyExtensionMeta.sol", - "contracts/extensions/EvaluatedExpenditure.sol", + "contracts/extensions/ExpenditureUtils.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..46041633ce 100755 --- a/scripts/check-storage.js +++ b/scripts/check-storage.js @@ -28,7 +28,7 @@ walkSync("./contracts/").forEach((contractName) => { "contracts/common/TokenAuthority.sol", // Imported from colonyToken repo "contracts/ens/ENSRegistry.sol", // Not directly used by any colony contracts "contracts/extensions/CoinMachine.sol", - "contracts/extensions/EvaluatedExpenditure.sol", + "contracts/extensions/ExpenditureUtils.sol", "contracts/extensions/FundingQueue.sol", "contracts/extensions/ColonyExtension.sol", "contracts/extensions/ColonyExtensionMeta.sol", diff --git a/test/contracts-network/colony-expenditure.js b/test/contracts-network/colony-expenditure.js index 6d439bb4ba..899cd7d994 100644 --- a/test/contracts-network/colony-expenditure.js +++ b/test/contracts-network/colony-expenditure.js @@ -1259,11 +1259,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" From ea54988f2bafec27e343274a15f2ee7a50ffa493 Mon Sep 17 00:00:00 2001 From: Alex Rea Date: Fri, 27 May 2022 15:03:05 +0100 Subject: [PATCH 11/59] Update test file to CommonJS --- test/extensions/expenditure-utils.js | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/test/extensions/expenditure-utils.js b/test/extensions/expenditure-utils.js index 7a659d9959..ed5e2f3242 100644 --- a/test/extensions/expenditure-utils.js +++ b/test/extensions/expenditure-utils.js @@ -1,15 +1,22 @@ /* globals artifacts */ -import chai from "chai"; -import bnChai from "bn-chai"; -import { ethers } from "ethers"; -import { soliditySha3 } from "web3-utils"; - -import { UINT256_MAX, WAD, MINING_CYCLE_DURATION, CHALLENGE_RESPONSE_WINDOW_DURATION } from "../../helpers/constants"; -import { setupRandomColony, getMetaTransactionParameters } from "../../helpers/test-data-generator"; -import { checkErrorRevert, web3GetCode, makeReputationKey, makeReputationValue, getActiveRepCycle, forwardTime } from "../../helpers/test-helper"; - -import PatriciaTree from "../../packages/reputation-miner/patricia"; +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 } = require("../../helpers/constants"); +const { setupRandomColony, getMetaTransactionParameters } = 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)); From 5895d6e0eaab55a3066925e488f4bb4da8b0b7b0 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Fri, 27 May 2022 13:01:45 -0700 Subject: [PATCH 12/59] Fix expenditure tests --- contracts/colony/ColonyFunding.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/colony/ColonyFunding.sol b/contracts/colony/ColonyFunding.sol index aa45ba2cc3..8c52dce9c6 100755 --- a/contracts/colony/ColonyFunding.sol +++ b/contracts/colony/ColonyFunding.sol @@ -167,6 +167,7 @@ contract ColonyFunding is ColonyStorage { // ignore-swc-123 function setExpenditurePayouts(uint256 _id, uint256[] memory _slots, address _token, uint256[] memory _amounts) public stoppable + expenditureExists(_id) expenditureDraft(_id) expenditureOnlyOwner(_id) { @@ -183,6 +184,7 @@ contract ColonyFunding is ColonyStorage { // ignore-swc-123 ) public stoppable + expenditureExists(_id) authDomain(_permissionDomainId, _childSkillIndex, expenditures[_id].domainId) { setExpenditurePayoutsInternal(_id, _slots, _token, _amounts); @@ -418,7 +420,6 @@ contract ColonyFunding is ColonyStorage { // ignore-swc-123 function setExpenditurePayoutsInternal(uint256 _id, uint256[] memory _slots, address _token, uint256[] memory _amounts) internal - expenditureExists(_id) { require(_slots.length == _amounts.length, "colony-expenditure-bad-slots"); From 855e92f771db7cdc48b9e9b048eb710d398d184a Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Fri, 27 May 2022 14:18:49 -0700 Subject: [PATCH 13/59] Update in response to review comments I --- contracts/colony/ColonyExpenditure.sol | 30 ++++++++++---------- contracts/colony/ColonyFunding.sol | 7 ++--- contracts/colony/ColonyStorage.sol | 4 +-- contracts/extensions/ExpenditureUtils.sol | 15 +++++----- test/contracts-network/colony-expenditure.js | 23 ++++++++------- test/contracts-network/colony-funding.js | 15 ++++++---- test/extensions/expenditure-utils.js | 27 ++++++++++++++++-- 7 files changed, 74 insertions(+), 47 deletions(-) diff --git a/contracts/colony/ColonyExpenditure.sol b/contracts/colony/ColonyExpenditure.sol index c11238b892..e78461b6be 100644 --- a/contracts/colony/ColonyExpenditure.sol +++ b/contracts/colony/ColonyExpenditure.sol @@ -70,7 +70,7 @@ contract ColonyExpenditure is ColonyStorage { stoppable expenditureExists(_id) expenditureDraftOrLocked(_id) - expenditureOnlyOwner(_id) + expenditureSelfOrOwner(_id) { expenditures[_id].owner = _newOwner; @@ -100,7 +100,7 @@ contract ColonyExpenditure is ColonyStorage { stoppable expenditureExists(_id) expenditureDraft(_id) - expenditureOnlyOwner(_id) + expenditureSelfOrOwner(_id) { expenditures[_id].status = ExpenditureStatus.Cancelled; @@ -112,7 +112,7 @@ contract ColonyExpenditure is ColonyStorage { stoppable expenditureExists(_id) expenditureDraft(_id) - expenditureOnlyOwner(_id) + expenditureSelfOrOwner(_id) { expenditures[_id].status = ExpenditureStatus.Locked; @@ -124,7 +124,7 @@ contract ColonyExpenditure is ColonyStorage { stoppable expenditureExists(_id) expenditureDraftOrLocked(_id) - expenditureOnlyOwner(_id) + expenditureSelfOrOwner(_id) { FundingPot storage fundingPot = fundingPots[expenditures[_id].fundingPotId]; require(fundingPot.payoutsWeCannotMake == 0, "colony-expenditure-not-funded"); @@ -140,7 +140,7 @@ contract ColonyExpenditure is ColonyStorage { stoppable expenditureExists(_id) expenditureDraft(_id) - expenditureOnlyOwner(_id) + expenditureSelfOrOwner(_id) { emit ExpenditureMetadataSet(msgSender(), _id, _metadata); } @@ -164,7 +164,7 @@ contract ColonyExpenditure is ColonyStorage { stoppable expenditureExists(_id) expenditureDraft(_id) - expenditureOnlyOwner(_id) + expenditureSelfOrOwner(_id) { require(_slots.length == _recipients.length, "colony-expenditure-bad-slots"); @@ -180,7 +180,7 @@ contract ColonyExpenditure is ColonyStorage { stoppable expenditureExists(_id) expenditureDraft(_id) - expenditureOnlyOwner(_id) + expenditureSelfOrOwner(_id) { require(_slots.length == _skillIds.length, "colony-expenditure-bad-slots"); IColonyNetwork colonyNetworkContract = IColonyNetwork(colonyNetworkAddress); @@ -203,7 +203,7 @@ contract ColonyExpenditure is ColonyStorage { stoppable expenditureExists(_id) expenditureDraft(_id) - expenditureOnlyOwner(_id) + expenditureSelfOrOwner(_id) { require(_slots.length == _claimDelays.length, "colony-expenditure-bad-slots"); @@ -219,7 +219,7 @@ contract ColonyExpenditure is ColonyStorage { stoppable expenditureExists(_id) expenditureDraft(_id) - expenditureOnlyOwner(_id) + expenditureSelfOrOwner(_id) { require(_slots.length == _payoutModifiers.length, "colony-expenditure-bad-slots"); @@ -248,13 +248,13 @@ contract ColonyExpenditure is ColonyStorage { stoppable expenditureExists(_id) expenditureDraft(_id) - expenditureOnlyOwner(_id) + expenditureSelfOrOwner(_id) { - setExpenditureRecipients(_id, _recipientSlots, _recipients); - setExpenditureSkills(_id, _skillIdSlots, _skillIds); - setExpenditureClaimDelays(_id, _claimDelaySlots, _claimDelays); - setExpenditurePayoutModifiers(_id, _payoutModifierSlots, _payoutModifiers); - setExpenditurePayouts(_id, _payoutTokens, _payoutSlots, _payoutValues); + 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 diff --git a/contracts/colony/ColonyFunding.sol b/contracts/colony/ColonyFunding.sol index 8c52dce9c6..dfacb5bc41 100755 --- a/contracts/colony/ColonyFunding.sol +++ b/contracts/colony/ColonyFunding.sol @@ -169,7 +169,7 @@ contract ColonyFunding is ColonyStorage { // ignore-swc-123 stoppable expenditureExists(_id) expenditureDraft(_id) - expenditureOnlyOwner(_id) + expenditureSelfOrOwner(_id) { setExpenditurePayoutsInternal(_id, _slots, _token, _amounts); } @@ -230,12 +230,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 diff --git a/contracts/colony/ColonyStorage.sol b/contracts/colony/ColonyStorage.sol index 4adc1d1ca6..62aa2f15c0 100755 --- a/contracts/colony/ColonyStorage.sol +++ b/contracts/colony/ColonyStorage.sol @@ -193,8 +193,8 @@ contract ColonyStorage is ColonyDataTypes, ColonyNetworkDataTypes, DSMath, Commo _; } - modifier expenditureOnlyOwner(uint256 _id) { - require(expenditures[_id].owner == msgSender() || address(this) == msgSender(), "colony-expenditure-not-owner"); + modifier expenditureSelfOrOwner(uint256 _id) { + require(expenditures[_id].owner == msgSender() || address(this) == msgSender(), "colony-expenditure-not-self-or-owner"); _; } diff --git a/contracts/extensions/ExpenditureUtils.sol b/contracts/extensions/ExpenditureUtils.sol index c7e39062b2..a04fba60c0 100644 --- a/contracts/extensions/ExpenditureUtils.sol +++ b/contracts/extensions/ExpenditureUtils.sol @@ -104,21 +104,22 @@ contract ExpenditureUtils is ColonyExtensionMeta, PatriciaTreeProofs { function slashStake( uint256 _permissionDomainId, uint256 _childSkillIndex, - uint256 _expenditureId + uint256 _expenditureId, + address _owner ) public { - ColonyDataTypes.Expenditure memory expenditure = colony.getExpenditure(_expenditureId); - uint256 stake = stakes[expenditure.owner][_expenditureId]; - require(stake > 0, "expenditure-utils-nothing-to-claim"); + uint256 stake = stakes[_owner][_expenditureId]; + require(stake > 0, "expenditure-utils-nothing-to-slash"); + uint256 expenditureDomainId = colony.getExpenditure(_expenditureId).domainId; require( - colony.hasInheritedUserRole(msgSender(), _permissionDomainId, ColonyDataTypes.ColonyRole.Arbitration, _childSkillIndex, expenditure.domainId), + colony.hasInheritedUserRole(msgSender(), _permissionDomainId, ColonyDataTypes.ColonyRole.Arbitration, _childSkillIndex, expenditureDomainId), "expenditure-utils-caller-not-arbitration" ); - delete stakes[expenditure.owner][_expenditureId]; - colony.transferStake(_permissionDomainId, _childSkillIndex, address(this), expenditure.owner, expenditure.domainId, stake, address(0x0)); + delete stakes[_owner][_expenditureId]; + colony.transferStake(_permissionDomainId, _childSkillIndex, address(this), _owner, expenditureDomainId, stake, address(colony)); } function reclaimStake(uint256 _expenditureId) public { diff --git a/test/contracts-network/colony-expenditure.js b/test/contracts-network/colony-expenditure.js index 899cd7d994..1b55b17e38 100644 --- a/test/contracts-network/colony-expenditure.js +++ b/test/contracts-network/colony-expenditure.js @@ -111,7 +111,7 @@ contract("Colony Expenditure", (accounts) => { let expenditure = await colony.getExpenditure(expenditureId); expect(expenditure.status).to.eq.BN(DRAFT); - await checkErrorRevert(colony.cancelExpenditure(expenditureId, { from: USER }), "colony-expenditure-not-owner"); + await checkErrorRevert(colony.cancelExpenditure(expenditureId, { from: USER }), "colony-expenditure-not-self-or-owner"); await colony.cancelExpenditure(expenditureId, { from: ADMIN }); expenditure = await colony.getExpenditure(expenditureId); @@ -126,7 +126,7 @@ contract("Colony Expenditure", (accounts) => { let expenditure = await colony.getExpenditure(expenditureId); expect(expenditure.owner).to.equal(ADMIN); - await checkErrorRevert(colony.transferExpenditure(expenditureId, USER), "colony-expenditure-not-owner"); + await checkErrorRevert(colony.transferExpenditure(expenditureId, USER), "colony-expenditure-not-self-or-owner"); await colony.transferExpenditure(expenditureId, USER, { from: ADMIN }); expenditure = await colony.getExpenditure(expenditureId); @@ -205,7 +205,7 @@ contract("Colony Expenditure", (accounts) => { await expectEvent(tx, "ExpenditureMetadataSet", [ADMIN, expenditureId, IPFS_HASH]); - await checkErrorRevert(setExpenditureMetadata(expenditureId, IPFS_HASH, { from: USER }), "colony-expenditure-not-owner"); + await checkErrorRevert(setExpenditureMetadata(expenditureId, IPFS_HASH, { from: USER }), "colony-expenditure-not-self-or-owner"); }); it("should allow arbitrators to update the metadata", async () => { @@ -232,7 +232,7 @@ contract("Colony Expenditure", (accounts) => { it("should allow only owners to update many slot recipients at once", async () => { await checkErrorRevert( colony.setExpenditureRecipients(expenditureId, [SLOT1, SLOT2], [RECIPIENT, USER], { from: USER }), - "colony-expenditure-not-owner" + "colony-expenditure-not-self-or-owner" ); await colony.setExpenditureRecipients(expenditureId, [SLOT1, SLOT2], [RECIPIENT, USER], { from: ADMIN }); @@ -311,7 +311,10 @@ contract("Colony Expenditure", (accounts) => { }); it("should allow only owners to update a slot claim delay", async () => { - await checkErrorRevert(colony.setExpenditureClaimDelay(expenditureId, SLOT0, SECONDS_PER_DAY, { from: USER }), "colony-expenditure-not-owner"); + await checkErrorRevert( + colony.setExpenditureClaimDelay(expenditureId, SLOT0, SECONDS_PER_DAY, { from: USER }), + "colony-expenditure-not-self-or-owner" + ); await colony.setExpenditureClaimDelay(expenditureId, SLOT0, SECONDS_PER_DAY, { from: ADMIN }); @@ -337,7 +340,7 @@ contract("Colony Expenditure", (accounts) => { it("should allow only owners to update many slot payout modifiers at once", async () => { await checkErrorRevert( colony.setExpenditurePayoutModifiers(expenditureId, [SLOT1, SLOT2], [WAD.divn(2), WAD], { from: USER }), - "colony-expenditure-not-owner" + "colony-expenditure-not-self-or-owner" ); await colony.setExpenditurePayoutModifiers(expenditureId, [SLOT1, SLOT2], [WAD.divn(2), WAD], { from: ADMIN }); @@ -390,8 +393,8 @@ contract("Colony Expenditure", (accounts) => { }); it("should not allow non-owners to update skills or payouts", async () => { - await checkErrorRevert(colony.setExpenditureSkill(expenditureId, SLOT0, GLOBAL_SKILL_ID), "colony-expenditure-not-owner"); - await checkErrorRevert(colony.setExpenditurePayout(expenditureId, SLOT0, token.address, WAD), "colony-expenditure-not-owner"); + await checkErrorRevert(colony.setExpenditureSkill(expenditureId, SLOT0, GLOBAL_SKILL_ID), "colony-expenditure-not-self-or-owner"); + await checkErrorRevert(colony.setExpenditurePayout(expenditureId, SLOT0, token.address, WAD), "colony-expenditure-not-self-or-owner"); }); it("should allow owners to add a slot payout", async () => { @@ -522,7 +525,7 @@ contract("Colony Expenditure", (accounts) => { let expenditure = await colony.getExpenditure(expenditureId); expect(expenditure.status).to.eq.BN(DRAFT); - await checkErrorRevert(colony.lockExpenditure(expenditureId, { from: USER }), "colony-expenditure-not-owner"); + await checkErrorRevert(colony.lockExpenditure(expenditureId, { from: USER }), "colony-expenditure-not-self-or-owner"); await colony.lockExpenditure(expenditureId, { from: ADMIN }); expenditure = await colony.getExpenditure(expenditureId); @@ -575,7 +578,7 @@ contract("Colony Expenditure", (accounts) => { let expenditure = await colony.getExpenditure(expenditureId); expect(expenditure.status).to.eq.BN(DRAFT); - await checkErrorRevert(colony.finalizeExpenditure(expenditureId, { from: USER }), "colony-expenditure-not-owner"); + await checkErrorRevert(colony.finalizeExpenditure(expenditureId, { from: USER }), "colony-expenditure-not-self-or-owner"); const tx = await colony.finalizeExpenditure(expenditureId, { from: ADMIN }); const currTime = await getBlockTime(tx.receipt.blockNumber); diff --git a/test/contracts-network/colony-funding.js b/test/contracts-network/colony-funding.js index b8df5697bd..333a3821f3 100755 --- a/test/contracts-network/colony-funding.js +++ b/test/contracts-network/colony-funding.js @@ -536,19 +536,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/extensions/expenditure-utils.js b/test/extensions/expenditure-utils.js index ed5e2f3242..cc436cc3d7 100644 --- a/test/extensions/expenditure-utils.js +++ b/test/extensions/expenditure-utils.js @@ -120,7 +120,7 @@ contract("ExpenditureUtils", (accounts) => { }); }); - describe("using stakes to manage expenditures", async () => { + describe.only("using stakes to manage expenditures", async () => { beforeEach(async () => { await expenditureUtils.setStakeFraction(WAD.divn(10)); // Stake of .3 WADs @@ -145,7 +145,7 @@ contract("ExpenditureUtils", (accounts) => { await expenditureUtils.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); const expenditureId = await colony.getExpenditureCount(); - await expenditureUtils.slashStake(1, UINT256_MAX, expenditureId); + await expenditureUtils.slashStake(1, UINT256_MAX, expenditureId, USER0); const obligation = await tokenLocking.getObligation(USER0, token.address, colony.address); expect(obligation).to.be.zero; @@ -154,6 +154,29 @@ contract("ExpenditureUtils", (accounts) => { expect(userLock.balance).to.eq.BN(WAD.sub(WAD.muln(3).divn(10))); }); + it("if ownership is transferred, the original owner is still slashed", async () => { + await expenditureUtils.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); + const expenditureId = await colony.getExpenditureCount(); + + await colony.transferExpenditure(expenditureId, USER1, { from: USER0 }); + + // New owner can't be slahed + await checkErrorRevert(expenditureUtils.slashStake(1, UINT256_MAX, 0, USER1), "expenditure-utils-nothing-to-slash"); + + // Original owner can be + await expenditureUtils.slashStake(1, UINT256_MAX, expenditureId, USER0); + + 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(WAD.muln(3).divn(10))); + }); + + it("cannot slash a nonexistent stake", async () => { + await checkErrorRevert(expenditureUtils.slashStake(1, UINT256_MAX, 0, USER0), "expenditure-utils-nothing-to-slash"); + }); + it("can reclaim the stake by cancelling the expenditure", async () => { await expenditureUtils.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); const expenditureId = await colony.getExpenditureCount(); From 4721bffd3afff8f24abe79b8cefb4de03c305794 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Thu, 2 Jun 2022 12:18:11 -0400 Subject: [PATCH 14/59] Update smoke tests --- test-smoke/colony-storage-consistent.js | 10 +++++----- test/extensions/expenditure-utils.js | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/test-smoke/colony-storage-consistent.js b/test-smoke/colony-storage-consistent.js index 0e5a980f39..39d6555db5 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("0x8559c2d1ede0619475f3dd5b00bce2d8c5add4da909899a1d7dcab7412019cfd"); + expect(colonyStateHash).to.equal("0xcfa14190f8d21c23dbeb3cfd3a0910d6db837c6505ff63f99c79f8ac46b0ad80"); + expect(metaColonyStateHash).to.equal("0x103db27eda4f844d4cc09d762568e2aefde2cf02fd7608a383309bb11fb3f5df"); + expect(miningCycleStateHash).to.equal("0x64a9fc6f052001247ace92ee7594be3d4c75496f7278ce39c62617587712526f"); + expect(tokenLockingStateHash).to.equal("0x974e41fdb7c36bab361489ebf788732c32ffff8ce1a852f157576b7bc0e4d2fb"); }); }); }); diff --git a/test/extensions/expenditure-utils.js b/test/extensions/expenditure-utils.js index cc436cc3d7..1981f2be5f 100644 --- a/test/extensions/expenditure-utils.js +++ b/test/extensions/expenditure-utils.js @@ -120,7 +120,7 @@ contract("ExpenditureUtils", (accounts) => { }); }); - describe.only("using stakes to manage expenditures", async () => { + describe("using stakes to manage expenditures", async () => { beforeEach(async () => { await expenditureUtils.setStakeFraction(WAD.divn(10)); // Stake of .3 WADs From 4a07ff6ba4294fa3e6dfc22d5985232a4cf69aa7 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Fri, 3 Jun 2022 15:04:15 -0400 Subject: [PATCH 15/59] Update VotingReputation to handle setExpenditureStates --- contracts/extensions/VotingReputation.sol | 18 +++++---- test/extensions/voting-rep.js | 48 ++++++----------------- 2 files changed, 23 insertions(+), 43 deletions(-) diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index 3292525a3e..671d574a27 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 CHANGE_FUNCTION_SIG_1 = bytes4(keccak256( "setExpenditureState(uint256,uint256,uint256,uint256,bool[],bytes32[],bytes32)" )); + bytes4 constant CHANGE_FUNCTION_SIG_2 = bytes4(keccak256( + "setExpenditurePayouts(uint256,uint256,uint256,uint256[],address,uint256[])" + )); + bytes4 constant OLD_MOVE_FUNDS_SIG = bytes4(keccak256( "moveFundsBetweenPots(uint256,uint256,uint256,uint256,uint256,uint256,address)" )); @@ -394,7 +398,7 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs, BasicMetaTrans _vote == YAY && !motion.escalated && motion.stakes[YAY] == requiredStake && - getSig(motion.action) == CHANGE_FUNCTION_SIG && + (getSig(motion.action) == CHANGE_FUNCTION_SIG_1 || getSig(motion.action) == CHANGE_FUNCTION_SIG_2) && motion.altTarget == address(0x0) ) { bytes32 structHash = hashExpenditureActionStruct(motion.action); @@ -590,7 +594,7 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs, BasicMetaTrans ); if ( - getSig(motion.action) == CHANGE_FUNCTION_SIG && + (getSig(motion.action) == CHANGE_FUNCTION_SIG_1 || getSig(motion.action) == CHANGE_FUNCTION_SIG_2) && getTarget(motion.altTarget) == address(colony) ) { bytes32 structHash = hashExpenditureActionStruct(motion.action); @@ -1051,7 +1055,7 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs, BasicMetaTrans } function hashExpenditureActionStruct(bytes memory action) internal returns (bytes32 hash) { - assert(getSig(action) == CHANGE_FUNCTION_SIG); + assert(getSig(action) == CHANGE_FUNCTION_SIG_1 || getSig(action) == CHANGE_FUNCTION_SIG_2); uint256 expenditureId; uint256 storageSlot; @@ -1082,14 +1086,14 @@ 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 functionSignature = CHANGE_FUNCTION_SIG_1; + uint256 permissionDomainId; uint256 childSkillIndex; uint256 expenditureId; uint256 storageSlot; assembly { - functionSignature := mload(add(action, 0x20)) permissionDomainId := mload(add(action, 0x24)) childSkillIndex := mload(add(action, 0x44)) expenditureId := mload(add(action, 0x64)) @@ -1097,7 +1101,7 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs, BasicMetaTrans } // If we are editing the main expenditure struct - if (storageSlot == 25) { + if (storageSlot == 25 || getSig(action) == CHANGE_FUNCTION_SIG_2) { bytes memory mainClaimDelayAction = new bytes(4 + 32 * 11); // 356 bytes assembly { diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index f3e488e3e6..e79e30c0a3 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -701,21 +701,13 @@ contract("Voting Reputation", (accounts) => { await checkErrorRevert(colony.claimExpenditurePayout(expenditureId, 0, token.address), "colony-expenditure-cannot-claim"); }); - it("can update the expenditure slot claimDelay if voting on expenditure payout state", async () => { + it("can update the expenditure globalClaimDelay 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, "setExpenditurePayouts", [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(); @@ -724,17 +716,17 @@ contract("Voting Reputation", (accounts) => { 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; + let expenditure; + expenditure = await colony.getExpenditure(expenditureId); + expect(expenditure.globalClaimDelay).to.be.zero; await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); expenditureMotionCount = await voting.getExpenditureMotionCount(soliditySha3(expenditureId, 0)); expect(expenditureMotionCount).to.eq.BN(1); - expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); - expect(expenditureSlot.claimDelay).to.eq.BN(UINT256_MAX.divn(3)); + expenditure = await colony.getExpenditure(expenditureId); + expect(expenditure.globalClaimDelay).to.eq.BN(UINT256_MAX.divn(3)); await checkErrorRevert(colony.claimExpenditurePayout(expenditureId, 0, token.address), "colony-expenditure-cannot-claim"); }); @@ -770,22 +762,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 +800,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); From e87db1ad011eef74b5591ef9849972278d0df127 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Fri, 3 Jun 2022 15:28:15 -0400 Subject: [PATCH 16/59] Burn stakes if transferred to address(0x0) --- contracts/extensions/ExpenditureUtils.sol | 2 +- contracts/tokenLocking/TokenLocking.sol | 6 +++++- test/contracts-network/colony-staking.js | 13 ++++++++++++- test/extensions/expenditure-utils.js | 3 +++ 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/contracts/extensions/ExpenditureUtils.sol b/contracts/extensions/ExpenditureUtils.sol index a04fba60c0..fa60fd6804 100644 --- a/contracts/extensions/ExpenditureUtils.sol +++ b/contracts/extensions/ExpenditureUtils.sol @@ -119,7 +119,7 @@ contract ExpenditureUtils is ColonyExtensionMeta, PatriciaTreeProofs { ); delete stakes[_owner][_expenditureId]; - colony.transferStake(_permissionDomainId, _childSkillIndex, address(this), _owner, expenditureDomainId, stake, address(colony)); + colony.transferStake(_permissionDomainId, _childSkillIndex, address(this), _owner, expenditureDomainId, stake, address(0x0)); } function reclaimStake(uint256 _expenditureId) public { diff --git a/contracts/tokenLocking/TokenLocking.sol b/contracts/tokenLocking/TokenLocking.sol index 20fdacf9d2..60a58a2140 100644 --- a/contracts/tokenLocking/TokenLocking.sol +++ b/contracts/tokenLocking/TokenLocking.sol @@ -195,7 +195,11 @@ contract TokenLocking is TokenLockingStorage, DSMath, BasicMetaTransaction { // Lock storage userLock = userLocks[_token][_user]; userLock.balance = sub(userLock.balance, _amount); - makeConditionalDeposit(_token, _amount, _recipient); + if (_recipient == address(0x0)) { + ERC20Extended(_token).burn(_amount); + } else { + makeConditionalDeposit(_token, _amount, _recipient); + } emit StakeTransferred(_token, msgSender(), _user, _recipient, _amount); } diff --git a/test/contracts-network/colony-staking.js b/test/contracts-network/colony-staking.js index a92eeefe2b..69d6e63ef6 100644 --- a/test/contracts-network/colony-staking.js +++ b/test/contracts-network/colony-staking.js @@ -3,7 +3,7 @@ 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"); @@ -236,5 +236,16 @@ contract("Colony Staking", (accounts) => { const { balance } = await tokenLocking.getUserLock(token.address, USER2); expect(balance).to.eq.BN(WAD); }); + + it("should burn slashed stake if sent to address(0x0)", 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); + }); }); }); diff --git a/test/extensions/expenditure-utils.js b/test/extensions/expenditure-utils.js index 1981f2be5f..0ada6b8b8a 100644 --- a/test/extensions/expenditure-utils.js +++ b/test/extensions/expenditure-utils.js @@ -128,6 +128,9 @@ contract("ExpenditureUtils", (accounts) => { await token.approve(tokenLocking.address, WAD, { from: USER0 }); await tokenLocking.deposit(token.address, WAD, false, { from: USER0 }); await colony.approveStake(expenditureUtils.address, 1, WAD, { from: USER0 }); + + const userLock = await tokenLocking.getUserLock(token.address, USER0); + expect(userLock.balance).to.eq.BN(WAD); }); it("can create an expenditure by submitting a stake", async () => { From 6eb642e8835d1f43bd1e2ebb45f930736d4e1869 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Mon, 6 Jun 2022 14:30:55 -0400 Subject: [PATCH 17/59] Update slashing implementation in tokenLocking --- contracts/tokenLocking/TokenLocking.sol | 2 +- test/contracts-network/colony-staking.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/tokenLocking/TokenLocking.sol b/contracts/tokenLocking/TokenLocking.sol index 60a58a2140..6adb481263 100644 --- a/contracts/tokenLocking/TokenLocking.sol +++ b/contracts/tokenLocking/TokenLocking.sol @@ -196,7 +196,7 @@ contract TokenLocking is TokenLockingStorage, DSMath, BasicMetaTransaction { // userLock.balance = sub(userLock.balance, _amount); if (_recipient == address(0x0)) { - ERC20Extended(_token).burn(_amount); + require(ERC20Extended(_token).transfer(address(0x0), _amount), "colony-token-locking-burn-failed"); } else { makeConditionalDeposit(_token, _amount, _recipient); } diff --git a/test/contracts-network/colony-staking.js b/test/contracts-network/colony-staking.js index 69d6e63ef6..97a5f35783 100644 --- a/test/contracts-network/colony-staking.js +++ b/test/contracts-network/colony-staking.js @@ -237,14 +237,14 @@ contract("Colony Staking", (accounts) => { expect(balance).to.eq.BN(WAD); }); - it("should burn slashed stake if sent to address(0x0)", async () => { - const supplyBefore = await token.totalSupply(); + it("should send stake out of tokenLocking if sent to address(0x0)", async () => { + 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.totalSupply(); + const supplyAfter = await token.balanceOf(tokenLocking.address); expect(supplyBefore.sub(supplyAfter)).to.eq.BN(WAD); }); }); From 9e8e782eb83d3650b660581f6a8307a4d67a10be Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Mon, 6 Jun 2022 15:24:50 -0400 Subject: [PATCH 18/59] Add tests to improve coverage --- contracts/extensions/ExpenditureUtils.sol | 6 +- test/extensions/expenditure-utils.js | 84 ++++++++++++++++++++++- 2 files changed, 84 insertions(+), 6 deletions(-) diff --git a/contracts/extensions/ExpenditureUtils.sol b/contracts/extensions/ExpenditureUtils.sol index fa60fd6804..2d4df6a3a2 100644 --- a/contracts/extensions/ExpenditureUtils.sol +++ b/contracts/extensions/ExpenditureUtils.sol @@ -37,7 +37,7 @@ contract ExpenditureUtils is ColonyExtensionMeta, PatriciaTreeProofs { // Modifiers modifier onlyRoot() { - require(colony.hasUserRole(msgSender(), 1, ColonyDataTypes.ColonyRole.Root), "evaluated-expenditure-caller-not-root"); + require(colony.hasUserRole(msgSender(), 1, ColonyDataTypes.ColonyRole.Root), "expenditure-utils-caller-not-root"); _; } @@ -157,8 +157,8 @@ contract ExpenditureUtils is ColonyExtensionMeta, PatriciaTreeProofs { ) public { - require(_slots.length == _payoutModifiers.length, "evaluated-expenditure-bad-slots"); - require(colony.getExpenditure(_id).owner == msgSender(), "evaluated-expenditure-not-owner"); + require(_slots.length == _payoutModifiers.length, "expenditure-utils-bad-slots"); + require(colony.getExpenditure(_id).owner == msgSender(), "expenditure-utils-not-owner"); bool[] memory mask = new bool[](2); bytes32[] memory keys = new bytes32[](2); diff --git a/test/extensions/expenditure-utils.js b/test/extensions/expenditure-utils.js index 0ada6b8b8a..af337f60c1 100644 --- a/test/extensions/expenditure-utils.js +++ b/test/extensions/expenditure-utils.js @@ -5,7 +5,7 @@ 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 } = require("../../helpers/constants"); +const { UINT256_MAX, WAD, MINING_CYCLE_DURATION, CHALLENGE_RESPONSE_WINDOW_DURATION, ADDRESS_ZERO } = require("../../helpers/constants"); const { setupRandomColony, getMetaTransactionParameters } = require("../../helpers/test-data-generator"); const { checkErrorRevert, @@ -76,6 +76,20 @@ contract("ExpenditureUtils", (accounts) => { 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); @@ -109,6 +123,15 @@ contract("ExpenditureUtils", (accounts) => { expect(code).to.equal("0x"); }); + it("cannot manage the extension if not owner", async () => { + expenditureUtils = await ExpenditureUtils.new(); + + await checkErrorRevert(expenditureUtils.install(colony.address, { from: USER1 }), "ds-auth-unauthorized"); + await checkErrorRevert(expenditureUtils.finishUpgrade({ from: USER1 }), "ds-auth-unauthorized"); + await checkErrorRevert(expenditureUtils.deprecate(true, { from: USER1 }), "ds-auth-unauthorized"); + await checkErrorRevert(expenditureUtils.uninstall({ from: USER1 }), "ds-auth-unauthorized"); + }); + it("can install the extension with the extension manager", async () => { ({ colony } = await setupRandomColony(colonyNetwork)); await colony.installExtension(EXPENDITURE_UTILS, version, { from: USER0 }); @@ -133,6 +156,10 @@ contract("ExpenditureUtils", (accounts) => { expect(userLock.balance).to.eq.BN(WAD); }); + it("cannot set the stake fraction if not root", async () => { + await checkErrorRevert(expenditureUtils.setStakeFraction(WAD, { from: USER1 }), "expenditure-utils-caller-not-root"); + }); + it("can create an expenditure by submitting a stake", async () => { await expenditureUtils.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); const expenditureId = await colony.getExpenditureCount(); @@ -144,6 +171,47 @@ contract("ExpenditureUtils", (accounts) => { expect(obligation).to.eq.BN(WAD.muln(3).divn(10)); }); + 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( + expenditureUtils.makeExpenditureWithStake(1, UINT256_MAX, 1, key, value, mask, siblings), + "expenditure-utils-invalid-root-hash" + ); + + key = makeReputationKey(ADDRESS_ZERO, domain1.skillId); + value = makeReputationValue(WAD, 2); + [mask, siblings] = await reputationTree.getProof(key); + await checkErrorRevert( + expenditureUtils.makeExpenditureWithStake(1, UINT256_MAX, 1, key, value, mask, siblings), + "expenditure-utils-invalid-colony-address" + ); + + key = makeReputationKey(colony.address, 100); + value = makeReputationValue(WAD, 3); + [mask, siblings] = await reputationTree.getProof(key); + await checkErrorRevert( + expenditureUtils.makeExpenditureWithStake(1, UINT256_MAX, 1, key, value, mask, siblings), + "expenditure-utils-invalid-skill-id" + ); + + key = makeReputationKey(colony.address, domain1.skillId, USER0); + value = makeReputationValue(WAD, 4); + [mask, siblings] = await reputationTree.getProof(key); + await checkErrorRevert( + expenditureUtils.makeExpenditureWithStake(1, UINT256_MAX, 1, key, value, mask, siblings), + "expenditure-utils-invalid-user-address" + ); + }); + it("can slash the stake with the arbitration permission", async () => { await expenditureUtils.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); const expenditureId = await colony.getExpenditureCount(); @@ -157,6 +225,16 @@ contract("ExpenditureUtils", (accounts) => { expect(userLock.balance).to.eq.BN(WAD.sub(WAD.muln(3).divn(10))); }); + it("cannot slash the stake without the arbitration permission", async () => { + await expenditureUtils.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); + const expenditureId = await colony.getExpenditureCount(); + + await checkErrorRevert( + expenditureUtils.slashStake(1, UINT256_MAX, expenditureId, USER0, { from: USER1 }), + "expenditure-utils-caller-not-arbitration" + ); + }); + it("if ownership is transferred, the original owner is still slashed", async () => { await expenditureUtils.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); const expenditureId = await colony.getExpenditureCount(); @@ -247,14 +325,14 @@ contract("ExpenditureUtils", (accounts) => { it("cannot set the payout modifier with bad arguments", async () => { await checkErrorRevert( expenditureUtils.setExpenditurePayoutModifiers(1, UINT256_MAX, expenditureId, [0], [], { from: USER0 }), - "evaluated-expenditure-bad-slots" + "expenditure-utils-bad-slots" ); }); it("cannot set the payout modifier if not the owner", async () => { await checkErrorRevert( expenditureUtils.setExpenditurePayoutModifiers(1, UINT256_MAX, expenditureId, [0], [WAD], { from: USER1 }), - "evaluated-expenditure-not-owner" + "expenditure-utils-not-owner" ); }); From 30b40c0aeb482dbbdef8f170c6bba12ccacee64b Mon Sep 17 00:00:00 2001 From: Alex Rea Date: Tue, 7 Jun 2022 16:28:45 +0100 Subject: [PATCH 19/59] Increase branch coverage on extension tests --- test/extensions/expenditure-utils.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/extensions/expenditure-utils.js b/test/extensions/expenditure-utils.js index af337f60c1..253bfd861d 100644 --- a/test/extensions/expenditure-utils.js +++ b/test/extensions/expenditure-utils.js @@ -123,10 +123,8 @@ contract("ExpenditureUtils", (accounts) => { expect(code).to.equal("0x"); }); - it("cannot manage the extension if not owner", async () => { - expenditureUtils = await ExpenditureUtils.new(); - - await checkErrorRevert(expenditureUtils.install(colony.address, { from: USER1 }), "ds-auth-unauthorized"); + it("can't use the network-level functions if installed via ColonyNetwork", async () => { + await checkErrorRevert(expenditureUtils.install(ADDRESS_ZERO, { from: USER1 }), "ds-auth-unauthorized"); await checkErrorRevert(expenditureUtils.finishUpgrade({ from: USER1 }), "ds-auth-unauthorized"); await checkErrorRevert(expenditureUtils.deprecate(true, { from: USER1 }), "ds-auth-unauthorized"); await checkErrorRevert(expenditureUtils.uninstall({ from: USER1 }), "ds-auth-unauthorized"); From 904a34d3fac62672b15517b292f4f2a5c558cf3d Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Thu, 9 Jun 2022 12:48:58 -0400 Subject: [PATCH 20/59] Update in response to review feedback --- contracts/colony/Colony.sol | 12 ++-- contracts/extensions/ExpenditureUtils.sol | 71 ++++++++++++++------ test/contracts-network/colony-expenditure.js | 39 +++++++++++ 3 files changed, 94 insertions(+), 28 deletions(-) diff --git a/contracts/colony/Colony.sol b/contracts/colony/Colony.sol index f8d5a43785..72f22f3ebb 100755 --- a/contracts/colony/Colony.sol +++ b/contracts/colony/Colony.sol @@ -309,14 +309,14 @@ 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; - // ColonyAuthority colonyAuthority = ColonyAuthority(address(authority)); - // bytes4 sig; - - // sig = bytes4(keccak256("addLocalSkill()")); - // colonyAuthority.setRoleCapability(uint8(ColonyRole.Root), address(this), sig, true); + sig = bytes4(keccak256("setExpenditurePayouts(uint256,uint256,uint256,uint256[],address,uint256[])")); + colonyAuthority.setRoleCapability(uint8(ColonyRole.Arbitration), address(this), sig, true); + sig = bytes4(keccak256("moveFundsBetweenPots(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256[],address[])")); + colonyAuthority.setRoleCapability(uint8(ColonyRole.Funding), address(this), sig, true); } function getMetatransactionNonce(address _user) override public view returns (uint256 nonce){ diff --git a/contracts/extensions/ExpenditureUtils.sol b/contracts/extensions/ExpenditureUtils.sol index 2d4df6a3a2..c2c14cdada 100644 --- a/contracts/extensions/ExpenditureUtils.sol +++ b/contracts/extensions/ExpenditureUtils.sol @@ -28,11 +28,22 @@ import "./ColonyExtensionMeta.sol"; contract ExpenditureUtils is ColonyExtensionMeta, PatriciaTreeProofs { + // Events + + event ExpenditureMadeViaStake(address indexed creator, uint256 expenditureId, uint256 stake); + + // Datatypes + + struct Stake { + address creator; + uint256 amount; + } + // Storage uint256 stakeFraction; - mapping (address => mapping (uint256 => uint256)) stakes; + mapping (uint256 => Stake) stakes; // Modifiers @@ -92,25 +103,44 @@ contract ExpenditureUtils is ColonyExtensionMeta, PatriciaTreeProofs { public { uint256 domainRep = getReputationFromProof(_domainId, _key, _value, _branchMask, _siblings); - uint256 stake = wmul(domainRep, stakeFraction); + uint256 stakeAmount = wmul(domainRep, stakeFraction); - colony.obligateStake(msgSender(), _domainId, stake); + colony.obligateStake(msgSender(), _domainId, stakeAmount); uint256 expenditureId = colony.makeExpenditure(_permissionDomainId, _childSkillIndex, _domainId); - stakes[msgSender()][expenditureId] = stake; + stakes[expenditureId] = Stake({ creator: msgSender(), amount: stakeAmount }); colony.transferExpenditure(expenditureId, msgSender()); + + emit ExpenditureMadeViaStake(msgSender(), expenditureId, stakeAmount); + } + + function reclaimStake(uint256 _expenditureId) public { + Stake storage stake = stakes[_expenditureId]; + require(stake.amount > 0, "expenditure-utils-nothing-to-claim"); + + ColonyDataTypes.Expenditure memory expenditure = colony.getExpenditure(_expenditureId); + require( + expenditure.status == ColonyDataTypes.ExpenditureStatus.Cancelled || + expenditure.status == ColonyDataTypes.ExpenditureStatus.Finalized, + "expenditure-utils-expenditure-invalid-state" + ); + + colony.deobligateStake(stake.creator, expenditure.domainId, stake.amount); + + // slither-disable-next-line reentrancy-no-eth + delete stakes[_expenditureId]; } function slashStake( uint256 _permissionDomainId, uint256 _childSkillIndex, uint256 _expenditureId, - address _owner + bool _penalizeRep ) public { - uint256 stake = stakes[_owner][_expenditureId]; - require(stake > 0, "expenditure-utils-nothing-to-slash"); + Stake storage stake = stakes[_expenditureId]; + require(stake.amount > 0, "expenditure-utils-nothing-to-slash"); uint256 expenditureDomainId = colony.getExpenditure(_expenditureId).domainId; require( @@ -118,23 +148,20 @@ contract ExpenditureUtils is ColonyExtensionMeta, PatriciaTreeProofs { "expenditure-utils-caller-not-arbitration" ); - delete stakes[_owner][_expenditureId]; - colony.transferStake(_permissionDomainId, _childSkillIndex, address(this), _owner, expenditureDomainId, stake, address(0x0)); - } - - function reclaimStake(uint256 _expenditureId) public { - uint256 stake = stakes[msgSender()][_expenditureId]; - require(stake > 0, "expenditure-utils-nothing-to-claim"); + colony.transferStake(_permissionDomainId, _childSkillIndex, address(this), stake.creator, expenditureDomainId, stake.amount, address(0x0)); - ColonyDataTypes.Expenditure memory expenditure = colony.getExpenditure(_expenditureId); - require( - expenditure.status == ColonyDataTypes.ExpenditureStatus.Cancelled || - expenditure.status == ColonyDataTypes.ExpenditureStatus.Finalized, - "expenditure-utils-expenditure-invalid-state" - ); + if (_penalizeRep) { + colony.emitDomainReputationPenalty( + _permissionDomainId, + _childSkillIndex, + expenditureDomainId, + stake.creator, + -int256(stake.amount) + ); + } - delete stakes[msgSender()][_expenditureId]; - colony.deobligateStake(msgSender(), expenditure.domainId, stake); + // slither-disable-next-line reentrancy-no-eth + delete stakes[_expenditureId]; } uint256 constant EXPENDITURESLOTS_SLOT = 26; diff --git a/test/contracts-network/colony-expenditure.js b/test/contracts-network/colony-expenditure.js index 1b55b17e38..6839cf1975 100644 --- a/test/contracts-network/colony-expenditure.js +++ b/test/contracts-network/colony-expenditure.js @@ -506,6 +506,45 @@ contract("Colony Expenditure", (accounts) => { payout = await colony.getExpenditureSlotPayout(expenditureId, SLOT2, otherToken.address); expect(payout).to.eq.BN(WAD.muln(40)); }); + + it("will not update values if empty arrays are passed", async () => { + await colony.setExpenditureValues(expenditureId, [], [], [], [], [], [], [], [], [], [[], []], [[], []], { from: ADMIN }); + + let slot; + slot = await colony.getExpenditureSlot(expenditureId, SLOT0); + console.log(slot); + expect(slot.recipient).to.equal(ADDRESS_ZERO); + expect(slot.skills[0]).to.be.zero; + expect(slot.claimDelay).to.be.zero; + expect(slot.payoutModifier).to.be.zero; + + slot = await colony.getExpenditureSlot(expenditureId, SLOT1); + expect(slot.recipient).to.equal(ADDRESS_ZERO); + expect(slot.skills[0]).to.be.zero; + expect(slot.claimDelay).to.be.zero; + expect(slot.payoutModifier).to.be.zero; + + slot = await colony.getExpenditureSlot(expenditureId, SLOT2); + expect(slot.recipient).to.equal(ADDRESS_ZERO); + expect(slot.skills[0]).to.be.zero; + expect(slot.claimDelay).to.be.zero; + expect(slot.payoutModifier).to.be.zero; + + let payout; + payout = await colony.getExpenditureSlotPayout(expenditureId, SLOT0, token.address); + expect(payout).to.be.zero; + payout = await colony.getExpenditureSlotPayout(expenditureId, SLOT1, token.address); + expect(payout).to.be.zero; + 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.be.zero; + payout = await colony.getExpenditureSlotPayout(expenditureId, SLOT2, otherToken.address); + expect(payout).to.be.zero; + }); }); describe("when locking expenditures", () => { From f7b9d6579241afac5cca36f9d4489fef09b6aa55 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Fri, 10 Jun 2022 15:22:47 -0700 Subject: [PATCH 21/59] Update in response to review feedback II --- contracts/extensions/ExpenditureUtils.sol | 39 ++++++++--- test/extensions/expenditure-utils.js | 80 +++++++++++++++++++---- 2 files changed, 99 insertions(+), 20 deletions(-) diff --git a/contracts/extensions/ExpenditureUtils.sol b/contracts/extensions/ExpenditureUtils.sol index c2c14cdada..87695875d4 100644 --- a/contracts/extensions/ExpenditureUtils.sol +++ b/contracts/extensions/ExpenditureUtils.sol @@ -42,6 +42,7 @@ contract ExpenditureUtils is ColonyExtensionMeta, PatriciaTreeProofs { // Storage uint256 stakeFraction; + uint256 repPenaltyFraction; mapping (uint256 => Stake) stakes; @@ -88,9 +89,15 @@ contract ExpenditureUtils is ColonyExtensionMeta, PatriciaTreeProofs { // Public function setStakeFraction(uint256 _stakeFraction) public onlyRoot { + require(_stakeFraction <= WAD, "expenditure-utils-value-too-large"); stakeFraction = _stakeFraction; } + function setRepPenaltyFraction(uint256 _repPenaltyFraction) public onlyRoot { + require(_repPenaltyFraction <= WAD, "expenditure-utils-value-too-large"); + repPenaltyFraction = _repPenaltyFraction; + } + function makeExpenditureWithStake( uint256 _permissionDomainId, uint256 _childSkillIndex, @@ -134,29 +141,35 @@ contract ExpenditureUtils is ColonyExtensionMeta, PatriciaTreeProofs { function slashStake( uint256 _permissionDomainId, uint256 _childSkillIndex, - uint256 _expenditureId, - bool _penalizeRep + uint256 _expenditureId ) public { Stake storage stake = stakes[_expenditureId]; require(stake.amount > 0, "expenditure-utils-nothing-to-slash"); - uint256 expenditureDomainId = colony.getExpenditure(_expenditureId).domainId; + ColonyDataTypes.Expenditure memory expenditure = colony.getExpenditure(_expenditureId); + + require( + expenditure.status == ColonyDataTypes.ExpenditureStatus.Locked, + "expenditure-utils-expenditure-not-locked" + ); + require( - colony.hasInheritedUserRole(msgSender(), _permissionDomainId, ColonyDataTypes.ColonyRole.Arbitration, _childSkillIndex, expenditureDomainId), + colony.hasInheritedUserRole(msgSender(), _permissionDomainId, ColonyDataTypes.ColonyRole.Arbitration, _childSkillIndex, expenditure.domainId), "expenditure-utils-caller-not-arbitration" ); - colony.transferStake(_permissionDomainId, _childSkillIndex, address(this), stake.creator, expenditureDomainId, stake.amount, address(0x0)); + colony.transferStake(_permissionDomainId, _childSkillIndex, address(this), stake.creator, expenditure.domainId, stake.amount, address(0x0)); - if (_penalizeRep) { + if (repPenaltyFraction > 0) { + int256 repPenalty = -int256(wmul(stake.amount, repPenaltyFraction)); colony.emitDomainReputationPenalty( _permissionDomainId, _childSkillIndex, - expenditureDomainId, + expenditure.domainId, stake.creator, - -int256(stake.amount) + repPenalty ); } @@ -210,6 +223,16 @@ contract ExpenditureUtils is ColonyExtensionMeta, PatriciaTreeProofs { } } +// View + +function getStakeFraction() public view returns (uint256) { + return stakeFraction; +} + +function getRepPenaltyFraction() public view returns (uint256) { + return repPenaltyFraction; +} + // Internal function getReputationFromProof( diff --git a/test/extensions/expenditure-utils.js b/test/extensions/expenditure-utils.js index 253bfd861d..6c4dd4faad 100644 --- a/test/extensions/expenditure-utils.js +++ b/test/extensions/expenditure-utils.js @@ -25,6 +25,7 @@ const IColonyNetwork = artifacts.require("IColonyNetwork"); const ITokenLocking = artifacts.require("ITokenLocking"); const EtherRouter = artifacts.require("EtherRouter"); const ExpenditureUtils = artifacts.require("ExpenditureUtils"); +const IReputationMiningCycle = artifacts.require("IReputationMiningCycle"); const EXPENDITURE_UTILS = soliditySha3("ExpenditureUtils"); @@ -42,6 +43,8 @@ contract("ExpenditureUtils", (accounts) => { let domain1Mask; let domain1Siblings; + let requiredStake; + const USER0 = accounts[0]; const USER1 = accounts[1]; const MINER = accounts[5]; @@ -144,6 +147,7 @@ contract("ExpenditureUtils", (accounts) => { describe("using stakes to manage expenditures", async () => { beforeEach(async () => { await expenditureUtils.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 }); @@ -154,8 +158,30 @@ contract("ExpenditureUtils", (accounts) => { expect(userLock.balance).to.eq.BN(WAD); }); - it("cannot set the stake fraction if not root", async () => { + it("can set the stake fraction", async () => { + await expenditureUtils.setStakeFraction(WAD, { from: USER0 }); + + const stakeFraction = await expenditureUtils.getStakeFraction(); + expect(stakeFraction).to.eq.BN(WAD); + + // But not if not root! await checkErrorRevert(expenditureUtils.setStakeFraction(WAD, { from: USER1 }), "expenditure-utils-caller-not-root"); + + // Also not greater than WAD! + await checkErrorRevert(expenditureUtils.setStakeFraction(WAD.addn(1), { from: USER0 }), "expenditure-utils-value-too-large"); + }); + + it("can set the reputation penalty fraction", async () => { + await expenditureUtils.setRepPenaltyFraction(WAD, { from: USER0 }); + + const repPenaltyFraction = await expenditureUtils.getRepPenaltyFraction(); + expect(repPenaltyFraction).to.eq.BN(WAD); + + // But not if not root! + await checkErrorRevert(expenditureUtils.setRepPenaltyFraction(WAD, { from: USER1 }), "expenditure-utils-caller-not-root"); + + // Also not greater than WAD! + await checkErrorRevert(expenditureUtils.setRepPenaltyFraction(WAD.addn(1), { from: USER0 }), "expenditure-utils-value-too-large"); }); it("can create an expenditure by submitting a stake", async () => { @@ -166,7 +192,7 @@ contract("ExpenditureUtils", (accounts) => { expect(owner).to.equal(USER0); const obligation = await tokenLocking.getObligation(USER0, token.address, colony.address); - expect(obligation).to.eq.BN(WAD.muln(3).divn(10)); + expect(obligation).to.eq.BN(requiredStake); }); it("cannot create an expenditure with an invalid proof", async () => { @@ -213,47 +239,77 @@ contract("ExpenditureUtils", (accounts) => { it("can slash the stake with the arbitration permission", async () => { await expenditureUtils.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); const expenditureId = await colony.getExpenditureCount(); + await colony.lockExpenditure(expenditureId); - await expenditureUtils.slashStake(1, UINT256_MAX, expenditureId, USER0); + await expenditureUtils.slashStake(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.sub(WAD.muln(3).divn(10))); + expect(userLock.balance).to.eq.BN(WAD.sub(requiredStake)); + }); + + it("can slash the stake and give a reputation penalty", async () => { + await expenditureUtils.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); + const expenditureId = await colony.getExpenditureCount(); + await colony.lockExpenditure(expenditureId); + + // Give a penalty equal to 100% of the stake + await expenditureUtils.setRepPenaltyFraction(WAD, { from: USER0 }); + + await expenditureUtils.slashStake(1, UINT256_MAX, expenditureId); + + 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); }); it("cannot slash the stake without the arbitration permission", async () => { await expenditureUtils.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); const expenditureId = await colony.getExpenditureCount(); + await colony.lockExpenditure(expenditureId); await checkErrorRevert( - expenditureUtils.slashStake(1, UINT256_MAX, expenditureId, USER0, { from: USER1 }), + expenditureUtils.slashStake(1, UINT256_MAX, expenditureId, { from: USER1 }), "expenditure-utils-caller-not-arbitration" ); }); + it("cannot slash the stake unless the expenditure is in the locked state", async () => { + await expenditureUtils.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); + const expenditureId = await colony.getExpenditureCount(); + + await checkErrorRevert( + expenditureUtils.slashStake(1, UINT256_MAX, expenditureId, { from: USER1 }), + "expenditure-utils-expenditure-not-locked" + ); + }); + it("if ownership is transferred, the original owner is still slashed", async () => { await expenditureUtils.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 }); - // New owner can't be slahed - await checkErrorRevert(expenditureUtils.slashStake(1, UINT256_MAX, 0, USER1), "expenditure-utils-nothing-to-slash"); - - // Original owner can be - await expenditureUtils.slashStake(1, UINT256_MAX, expenditureId, USER0); + await expenditureUtils.slashStake(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.sub(WAD.muln(3).divn(10))); + expect(userLock.balance).to.eq.BN(WAD.sub(requiredStake)); }); it("cannot slash a nonexistent stake", async () => { - await checkErrorRevert(expenditureUtils.slashStake(1, UINT256_MAX, 0, USER0), "expenditure-utils-nothing-to-slash"); + await checkErrorRevert(expenditureUtils.slashStake(1, UINT256_MAX, 0), "expenditure-utils-nothing-to-slash"); }); it("can reclaim the stake by cancelling the expenditure", async () => { From 8f52deddbd021e542931c08d348e51c41a0bef5a Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Tue, 14 Jun 2022 17:54:49 -0700 Subject: [PATCH 22/59] Automatically cancel expenditure when stake is slashed --- contracts/extensions/ExpenditureUtils.sol | 13 ++++++++++++- test/extensions/expenditure-utils.js | 15 +++++++-------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/contracts/extensions/ExpenditureUtils.sol b/contracts/extensions/ExpenditureUtils.sol index 87695875d4..ed06b585a5 100644 --- a/contracts/extensions/ExpenditureUtils.sol +++ b/contracts/extensions/ExpenditureUtils.sol @@ -173,10 +173,21 @@ contract ExpenditureUtils is ColonyExtensionMeta, PatriciaTreeProofs { ); } - // slither-disable-next-line reentrancy-no-eth + + // 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)); + + bytes32 value = bytes32(bytes20(expenditure.owner)) >> 0x58 | bytes32(uint256(ColonyDataTypes.ExpenditureStatus.Cancelled)); + colony.setExpenditureState(_permissionDomainId, _childSkillIndex, _expenditureId, EXPENDITURE_SLOT, mask, keys, value); + + // slither-disable-next-line reentrancy-no-eth delete stakes[_expenditureId]; } + uint256 constant EXPENDITURE_SLOT = 25; uint256 constant EXPENDITURESLOTS_SLOT = 26; uint256 constant PAYOUT_MODIFIER_OFFSET = 2; bool constant MAPPING = false; diff --git a/test/extensions/expenditure-utils.js b/test/extensions/expenditure-utils.js index 6c4dd4faad..982d98a485 100644 --- a/test/extensions/expenditure-utils.js +++ b/test/extensions/expenditure-utils.js @@ -248,6 +248,11 @@ contract("ExpenditureUtils", (accounts) => { const userLock = await tokenLocking.getUserLock(token.address, USER0); expect(userLock.balance).to.eq.BN(WAD.sub(requiredStake)); + + // The expenditure is automatically cancelled + const cancelled = 1; + const expenditure = await colony.getExpenditure(expenditureId); + expect(expenditure.status).to.eq.BN(cancelled); }); it("can slash the stake and give a reputation penalty", async () => { @@ -276,20 +281,14 @@ contract("ExpenditureUtils", (accounts) => { const expenditureId = await colony.getExpenditureCount(); await colony.lockExpenditure(expenditureId); - await checkErrorRevert( - expenditureUtils.slashStake(1, UINT256_MAX, expenditureId, { from: USER1 }), - "expenditure-utils-caller-not-arbitration" - ); + await checkErrorRevert(expenditureUtils.slashStake(1, UINT256_MAX, expenditureId, { from: USER1 }), "expenditure-utils-caller-not-arbitration"); }); it("cannot slash the stake unless the expenditure is in the locked state", async () => { await expenditureUtils.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); const expenditureId = await colony.getExpenditureCount(); - await checkErrorRevert( - expenditureUtils.slashStake(1, UINT256_MAX, expenditureId, { from: USER1 }), - "expenditure-utils-expenditure-not-locked" - ); + await checkErrorRevert(expenditureUtils.slashStake(1, UINT256_MAX, expenditureId, { from: USER1 }), "expenditure-utils-expenditure-not-locked"); }); it("if ownership is transferred, the original owner is still slashed", async () => { From a1ef145fc64c17b681a95156802671524fa8f185 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Wed, 15 Jun 2022 08:46:24 -0700 Subject: [PATCH 23/59] Update smoke tests --- test-smoke/colony-storage-consistent.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test-smoke/colony-storage-consistent.js b/test-smoke/colony-storage-consistent.js index 39d6555db5..1b2bf66da4 100644 --- a/test-smoke/colony-storage-consistent.js +++ b/test-smoke/colony-storage-consistent.js @@ -149,8 +149,8 @@ contract("Contract Storage", (accounts) => { console.log("miningCycleStateHash:", miningCycleStateHash); console.log("tokenLockingStateHash:", tokenLockingStateHash); - expect(colonyNetworkStateHash).to.equal("0x8559c2d1ede0619475f3dd5b00bce2d8c5add4da909899a1d7dcab7412019cfd"); - expect(colonyStateHash).to.equal("0xcfa14190f8d21c23dbeb3cfd3a0910d6db837c6505ff63f99c79f8ac46b0ad80"); + expect(colonyNetworkStateHash).to.equal("0x4446f04f12b01009aaa58cec0c23ec26c64fa8be9118f10f089cc789801ead9b"); + expect(colonyStateHash).to.equal("0xc9ae8edd5c816f7e776076cee2aced723b81981445a311c8bcb3ede06db337c8"); expect(metaColonyStateHash).to.equal("0x103db27eda4f844d4cc09d762568e2aefde2cf02fd7608a383309bb11fb3f5df"); expect(miningCycleStateHash).to.equal("0x64a9fc6f052001247ace92ee7594be3d4c75496f7278ce39c62617587712526f"); expect(tokenLockingStateHash).to.equal("0x974e41fdb7c36bab361489ebf788732c32ffff8ce1a852f157576b7bc0e4d2fb"); From 6be60d005d61d094ca6a7d701dcd28dde7be5e89 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Fri, 17 Jun 2022 08:55:52 -0700 Subject: [PATCH 24/59] Remove ability to give variable rep penalty --- contracts/extensions/ExpenditureUtils.sol | 56 +++++++++++------------ test/extensions/expenditure-utils.js | 16 ------- 2 files changed, 28 insertions(+), 44 deletions(-) diff --git a/contracts/extensions/ExpenditureUtils.sol b/contracts/extensions/ExpenditureUtils.sol index ed06b585a5..13c20718f7 100644 --- a/contracts/extensions/ExpenditureUtils.sol +++ b/contracts/extensions/ExpenditureUtils.sol @@ -93,11 +93,6 @@ contract ExpenditureUtils is ColonyExtensionMeta, PatriciaTreeProofs { stakeFraction = _stakeFraction; } - function setRepPenaltyFraction(uint256 _repPenaltyFraction) public onlyRoot { - require(_repPenaltyFraction <= WAD, "expenditure-utils-value-too-large"); - repPenaltyFraction = _repPenaltyFraction; - } - function makeExpenditureWithStake( uint256 _permissionDomainId, uint256 _childSkillIndex, @@ -162,26 +157,15 @@ contract ExpenditureUtils is ColonyExtensionMeta, PatriciaTreeProofs { colony.transferStake(_permissionDomainId, _childSkillIndex, address(this), stake.creator, expenditure.domainId, stake.amount, address(0x0)); - if (repPenaltyFraction > 0) { - int256 repPenalty = -int256(wmul(stake.amount, repPenaltyFraction)); - colony.emitDomainReputationPenalty( - _permissionDomainId, - _childSkillIndex, - expenditure.domainId, - stake.creator, - repPenalty - ); - } - - - // 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)); + colony.emitDomainReputationPenalty( + _permissionDomainId, + _childSkillIndex, + expenditure.domainId, + stake.creator, + -int256(stake.amount) + ); - bytes32 value = bytes32(bytes20(expenditure.owner)) >> 0x58 | bytes32(uint256(ColonyDataTypes.ExpenditureStatus.Cancelled)); - colony.setExpenditureState(_permissionDomainId, _childSkillIndex, _expenditureId, EXPENDITURE_SLOT, mask, keys, value); + cancelExpenditure(_permissionDomainId, _childSkillIndex,_expenditureId, expenditure.owner); // slither-disable-next-line reentrancy-no-eth delete stakes[_expenditureId]; @@ -240,12 +224,28 @@ function getStakeFraction() public view returns (uint256) { return stakeFraction; } -function getRepPenaltyFraction() public view returns (uint256) { - return repPenaltyFraction; -} - // Internal +function cancelExpenditure( + uint256 _permissionDomainId, + uint256 _childSkillIndex, + uint256 _expenditureId, + address _owner +) + 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 0x{owner}{state} value + bytes32 value = bytes32(bytes20(_owner)) >> 0x58 | bytes32(uint256(ColonyDataTypes.ExpenditureStatus.Cancelled)); + + colony.setExpenditureState(_permissionDomainId, _childSkillIndex, _expenditureId, EXPENDITURE_SLOT, mask, keys, value); +} + function getReputationFromProof( uint256 _domainId, bytes memory _key, diff --git a/test/extensions/expenditure-utils.js b/test/extensions/expenditure-utils.js index 982d98a485..d07f3a69c2 100644 --- a/test/extensions/expenditure-utils.js +++ b/test/extensions/expenditure-utils.js @@ -171,19 +171,6 @@ contract("ExpenditureUtils", (accounts) => { await checkErrorRevert(expenditureUtils.setStakeFraction(WAD.addn(1), { from: USER0 }), "expenditure-utils-value-too-large"); }); - it("can set the reputation penalty fraction", async () => { - await expenditureUtils.setRepPenaltyFraction(WAD, { from: USER0 }); - - const repPenaltyFraction = await expenditureUtils.getRepPenaltyFraction(); - expect(repPenaltyFraction).to.eq.BN(WAD); - - // But not if not root! - await checkErrorRevert(expenditureUtils.setRepPenaltyFraction(WAD, { from: USER1 }), "expenditure-utils-caller-not-root"); - - // Also not greater than WAD! - await checkErrorRevert(expenditureUtils.setRepPenaltyFraction(WAD.addn(1), { from: USER0 }), "expenditure-utils-value-too-large"); - }); - it("can create an expenditure by submitting a stake", async () => { await expenditureUtils.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); const expenditureId = await colony.getExpenditureCount(); @@ -260,9 +247,6 @@ contract("ExpenditureUtils", (accounts) => { const expenditureId = await colony.getExpenditureCount(); await colony.lockExpenditure(expenditureId); - // Give a penalty equal to 100% of the stake - await expenditureUtils.setRepPenaltyFraction(WAD, { from: USER0 }); - await expenditureUtils.slashStake(1, UINT256_MAX, expenditureId); const addr = await colonyNetwork.getReputationMiningCycle(false); From b6c9a423df2532444d6844f8658181ae1f8d7967 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Fri, 17 Jun 2022 09:02:33 -0700 Subject: [PATCH 25/59] Update smoke tests --- test-smoke/colony-storage-consistent.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test-smoke/colony-storage-consistent.js b/test-smoke/colony-storage-consistent.js index 1b2bf66da4..caa489c4ef 100644 --- a/test-smoke/colony-storage-consistent.js +++ b/test-smoke/colony-storage-consistent.js @@ -149,8 +149,8 @@ contract("Contract Storage", (accounts) => { console.log("miningCycleStateHash:", miningCycleStateHash); console.log("tokenLockingStateHash:", tokenLockingStateHash); - expect(colonyNetworkStateHash).to.equal("0x4446f04f12b01009aaa58cec0c23ec26c64fa8be9118f10f089cc789801ead9b"); - expect(colonyStateHash).to.equal("0xc9ae8edd5c816f7e776076cee2aced723b81981445a311c8bcb3ede06db337c8"); + expect(colonyNetworkStateHash).to.equal("0xe4fbe2255acb920686f02300cb3106e81615fce53afc3f85995d60d3ffba136c"); + expect(colonyStateHash).to.equal("0xc518b7990ea77e039476631c26daa51cc46c2752f504c03faf78b4693aef77cb"); expect(metaColonyStateHash).to.equal("0x103db27eda4f844d4cc09d762568e2aefde2cf02fd7608a383309bb11fb3f5df"); expect(miningCycleStateHash).to.equal("0x64a9fc6f052001247ace92ee7594be3d4c75496f7278ce39c62617587712526f"); expect(tokenLockingStateHash).to.equal("0x974e41fdb7c36bab361489ebf788732c32ffff8ce1a852f157576b7bc0e4d2fb"); From b2384d99065221600ef5777a730d10cc2150d8e8 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Fri, 17 Jun 2022 11:45:30 -0700 Subject: [PATCH 26/59] Combine slash and cancel into a single function --- contracts/extensions/ExpenditureUtils.sol | 64 +++++++++------------ test/extensions/expenditure-utils.js | 69 +++++++++++++++++------ 2 files changed, 79 insertions(+), 54 deletions(-) diff --git a/contracts/extensions/ExpenditureUtils.sol b/contracts/extensions/ExpenditureUtils.sol index 13c20718f7..829988177d 100644 --- a/contracts/extensions/ExpenditureUtils.sol +++ b/contracts/extensions/ExpenditureUtils.sol @@ -133,16 +133,14 @@ contract ExpenditureUtils is ColonyExtensionMeta, PatriciaTreeProofs { delete stakes[_expenditureId]; } - function slashStake( + function cancelExpenditure( uint256 _permissionDomainId, uint256 _childSkillIndex, - uint256 _expenditureId + uint256 _expenditureId, + bool _punish ) public { - Stake storage stake = stakes[_expenditureId]; - require(stake.amount > 0, "expenditure-utils-nothing-to-slash"); - ColonyDataTypes.Expenditure memory expenditure = colony.getExpenditure(_expenditureId); require( @@ -155,20 +153,34 @@ contract ExpenditureUtils is ColonyExtensionMeta, PatriciaTreeProofs { "expenditure-utils-caller-not-arbitration" ); - colony.transferStake(_permissionDomainId, _childSkillIndex, address(this), stake.creator, expenditure.domainId, stake.amount, address(0x0)); + if (_punish) { + Stake storage stake = stakes[_expenditureId]; + require(stake.amount > 0, "expenditure-utils-nothing-to-slash"); - colony.emitDomainReputationPenalty( - _permissionDomainId, - _childSkillIndex, - expenditure.domainId, - stake.creator, - -int256(stake.amount) - ); + colony.transferStake(_permissionDomainId, _childSkillIndex, address(this), stake.creator, expenditure.domainId, stake.amount, address(0x0)); - cancelExpenditure(_permissionDomainId, _childSkillIndex,_expenditureId, expenditure.owner); + colony.emitDomainReputationPenalty( + _permissionDomainId, + _childSkillIndex, + expenditure.domainId, + stake.creator, + -int256(stake.amount) + ); - // slither-disable-next-line reentrancy-no-eth - delete stakes[_expenditureId]; + // slither-disable-next-line reentrancy-no-eth + delete stakes[_expenditureId]; + } + + // 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 0x{owner}{state} value + bytes32 value = bytes32(bytes20(expenditure.owner)) >> 0x58 | bytes32(uint256(ColonyDataTypes.ExpenditureStatus.Cancelled)); + + colony.setExpenditureState(_permissionDomainId, _childSkillIndex, _expenditureId, EXPENDITURE_SLOT, mask, keys, value); } uint256 constant EXPENDITURE_SLOT = 25; @@ -226,26 +238,6 @@ function getStakeFraction() public view returns (uint256) { // Internal -function cancelExpenditure( - uint256 _permissionDomainId, - uint256 _childSkillIndex, - uint256 _expenditureId, - address _owner -) - 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 0x{owner}{state} value - bytes32 value = bytes32(bytes20(_owner)) >> 0x58 | bytes32(uint256(ColonyDataTypes.ExpenditureStatus.Cancelled)); - - colony.setExpenditureState(_permissionDomainId, _childSkillIndex, _expenditureId, EXPENDITURE_SLOT, mask, keys, value); -} - function getReputationFromProof( uint256 _domainId, bytes memory _key, diff --git a/test/extensions/expenditure-utils.js b/test/extensions/expenditure-utils.js index d07f3a69c2..57261861aa 100644 --- a/test/extensions/expenditure-utils.js +++ b/test/extensions/expenditure-utils.js @@ -49,6 +49,8 @@ contract("ExpenditureUtils", (accounts) => { const USER1 = accounts[1]; const MINER = accounts[5]; + const CANCELLED = 1; + before(async () => { const etherRouter = await EtherRouter.deployed(); colonyNetwork = await IColonyNetwork.at(etherRouter.address); @@ -228,7 +230,7 @@ contract("ExpenditureUtils", (accounts) => { const expenditureId = await colony.getExpenditureCount(); await colony.lockExpenditure(expenditureId); - await expenditureUtils.slashStake(1, UINT256_MAX, expenditureId); + await expenditureUtils.cancelExpenditure(1, UINT256_MAX, expenditureId, true); const obligation = await tokenLocking.getObligation(USER0, token.address, colony.address); expect(obligation).to.be.zero; @@ -236,19 +238,7 @@ contract("ExpenditureUtils", (accounts) => { const userLock = await tokenLocking.getUserLock(token.address, USER0); expect(userLock.balance).to.eq.BN(WAD.sub(requiredStake)); - // The expenditure is automatically cancelled - const cancelled = 1; - const expenditure = await colony.getExpenditure(expenditureId); - expect(expenditure.status).to.eq.BN(cancelled); - }); - - it("can slash the stake and give a reputation penalty", async () => { - await expenditureUtils.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); - const expenditureId = await colony.getExpenditureCount(); - await colony.lockExpenditure(expenditureId); - - await expenditureUtils.slashStake(1, UINT256_MAX, expenditureId); - + // Creator gets a reputation penalty const addr = await colonyNetwork.getReputationMiningCycle(false); const repCycle = await IReputationMiningCycle.at(addr); const numUpdates = await repCycle.getReputationUpdateLogLength(); @@ -258,6 +248,10 @@ contract("ExpenditureUtils", (accounts) => { 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 () => { @@ -265,14 +259,49 @@ contract("ExpenditureUtils", (accounts) => { const expenditureId = await colony.getExpenditureCount(); await colony.lockExpenditure(expenditureId); - await checkErrorRevert(expenditureUtils.slashStake(1, UINT256_MAX, expenditureId, { from: USER1 }), "expenditure-utils-caller-not-arbitration"); + await checkErrorRevert( + expenditureUtils.cancelExpenditure(1, UINT256_MAX, expenditureId, true, { from: USER1 }), + "expenditure-utils-caller-not-arbitration" + ); }); it("cannot slash the stake unless the expenditure is in the locked state", async () => { await expenditureUtils.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); const expenditureId = await colony.getExpenditureCount(); - await checkErrorRevert(expenditureUtils.slashStake(1, UINT256_MAX, expenditureId, { from: USER1 }), "expenditure-utils-expenditure-not-locked"); + await checkErrorRevert( + expenditureUtils.cancelExpenditure(1, UINT256_MAX, expenditureId, true, { from: USER1 }), + "expenditure-utils-expenditure-not-locked" + ); + }); + + it("can cancel the expenditure without penalty", async () => { + await expenditureUtils.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); + const expenditureId = await colony.getExpenditureCount(); + await colony.lockExpenditure(expenditureId); + + await expenditureUtils.cancelExpenditure(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 expenditureUtils.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 () => { @@ -282,7 +311,7 @@ contract("ExpenditureUtils", (accounts) => { await colony.transferExpenditure(expenditureId, USER1, { from: USER0 }); - await expenditureUtils.slashStake(1, UINT256_MAX, expenditureId); + await expenditureUtils.cancelExpenditure(1, UINT256_MAX, expenditureId, true); const obligation = await tokenLocking.getObligation(USER0, token.address, colony.address); expect(obligation).to.be.zero; @@ -292,7 +321,11 @@ contract("ExpenditureUtils", (accounts) => { }); it("cannot slash a nonexistent stake", async () => { - await checkErrorRevert(expenditureUtils.slashStake(1, UINT256_MAX, 0), "expenditure-utils-nothing-to-slash"); + await colony.makeExpenditure(1, UINT256_MAX, 1); + const expenditureId = await colony.getExpenditureCount(); + await colony.lockExpenditure(expenditureId); + + await checkErrorRevert(expenditureUtils.cancelExpenditure(1, UINT256_MAX, expenditureId, true), "expenditure-utils-nothing-to-slash"); }); it("can reclaim the stake by cancelling the expenditure", async () => { From c0431a0d302b5716921bb0cb0bee69b304befa9c Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Mon, 20 Jun 2022 11:04:26 -0700 Subject: [PATCH 27/59] Allow cancelling an expenditure from any state --- contracts/extensions/ExpenditureUtils.sol | 5 ----- test/extensions/expenditure-utils.js | 10 ---------- 2 files changed, 15 deletions(-) diff --git a/contracts/extensions/ExpenditureUtils.sol b/contracts/extensions/ExpenditureUtils.sol index 829988177d..d68d699fa6 100644 --- a/contracts/extensions/ExpenditureUtils.sol +++ b/contracts/extensions/ExpenditureUtils.sol @@ -143,11 +143,6 @@ contract ExpenditureUtils is ColonyExtensionMeta, PatriciaTreeProofs { { ColonyDataTypes.Expenditure memory expenditure = colony.getExpenditure(_expenditureId); - require( - expenditure.status == ColonyDataTypes.ExpenditureStatus.Locked, - "expenditure-utils-expenditure-not-locked" - ); - require( colony.hasInheritedUserRole(msgSender(), _permissionDomainId, ColonyDataTypes.ColonyRole.Arbitration, _childSkillIndex, expenditure.domainId), "expenditure-utils-caller-not-arbitration" diff --git a/test/extensions/expenditure-utils.js b/test/extensions/expenditure-utils.js index 57261861aa..470a7a41d1 100644 --- a/test/extensions/expenditure-utils.js +++ b/test/extensions/expenditure-utils.js @@ -265,16 +265,6 @@ contract("ExpenditureUtils", (accounts) => { ); }); - it("cannot slash the stake unless the expenditure is in the locked state", async () => { - await expenditureUtils.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); - const expenditureId = await colony.getExpenditureCount(); - - await checkErrorRevert( - expenditureUtils.cancelExpenditure(1, UINT256_MAX, expenditureId, true, { from: USER1 }), - "expenditure-utils-expenditure-not-locked" - ); - }); - it("can cancel the expenditure without penalty", async () => { await expenditureUtils.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); const expenditureId = await colony.getExpenditureCount(); From 7a06fe60fdb6b7e431d2939f9316bfbebc538fe0 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Tue, 21 Jun 2022 11:54:04 -0700 Subject: [PATCH 28/59] Burn slashed stakes if possible --- contracts/tokenLocking/TokenLocking.sol | 5 ++++- test/contracts-network/colony-staking.js | 25 +++++++++++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/contracts/tokenLocking/TokenLocking.sol b/contracts/tokenLocking/TokenLocking.sol index 6adb481263..168988f850 100644 --- a/contracts/tokenLocking/TokenLocking.sol +++ b/contracts/tokenLocking/TokenLocking.sol @@ -196,7 +196,10 @@ contract TokenLocking is TokenLockingStorage, DSMath, BasicMetaTransaction { // userLock.balance = sub(userLock.balance, _amount); if (_recipient == address(0x0)) { - require(ERC20Extended(_token).transfer(address(0x0), _amount), "colony-token-locking-burn-failed"); + // If the burn fails, transfer to 0x0 + try ERC20Extended(_token).burn(_amount) {} catch { + require(ERC20Extended(_token).transfer(address(0x0), _amount), "colony-token-locking-burn-failed"); + } } else { makeConditionalDeposit(_token, _amount, _recipient); } diff --git a/test/contracts-network/colony-staking.js b/test/contracts-network/colony-staking.js index 97a5f35783..b5afa97090 100644 --- a/test/contracts-network/colony-staking.js +++ b/test/contracts-network/colony-staking.js @@ -10,6 +10,7 @@ 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"); @@ -237,7 +238,26 @@ contract("Colony Staking", (accounts) => { expect(balance).to.eq.BN(WAD); }); - it("should send stake out of tokenLocking if sent to address(0x0)", async () => { + it("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("should send the stake to address(0x0) if the token does not support burning", async () => { + // This token does not support burning + const token = await ToggleableToken.new(WAD, { from: USER1 }); + const 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 }); @@ -246,6 +266,9 @@ contract("Colony Staking", (accounts) => { 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); }); }); }); From efc1aa2a25998dac2e2022689f1ad13dc37e4437 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Tue, 21 Jun 2022 11:54:27 -0700 Subject: [PATCH 29/59] Disallow cancelling expenditure from draft mode --- contracts/extensions/ExpenditureUtils.sol | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/contracts/extensions/ExpenditureUtils.sol b/contracts/extensions/ExpenditureUtils.sol index d68d699fa6..c78c4b2b70 100644 --- a/contracts/extensions/ExpenditureUtils.sol +++ b/contracts/extensions/ExpenditureUtils.sol @@ -133,7 +133,7 @@ contract ExpenditureUtils is ColonyExtensionMeta, PatriciaTreeProofs { delete stakes[_expenditureId]; } - function cancelExpenditure( + function cancelExpenditureAndPunish( uint256 _permissionDomainId, uint256 _childSkillIndex, uint256 _expenditureId, @@ -148,6 +148,11 @@ contract ExpenditureUtils is ColonyExtensionMeta, PatriciaTreeProofs { "expenditure-utils-caller-not-arbitration" ); + require( + expenditure.status != ColonyDataTypes.ExpenditureStatus.Draft, + "expenditure-utils-expenditure-still-draft" + ); + if (_punish) { Stake storage stake = stakes[_expenditureId]; require(stake.amount > 0, "expenditure-utils-nothing-to-slash"); From 38a40b6173fb209b4757f8c37280b2e96aa29cf0 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Tue, 21 Jun 2022 13:28:04 -0700 Subject: [PATCH 30/59] Allow owner to cancel & recliam in one tx --- contracts/extensions/ExpenditureUtils.sol | 73 ++++++++++++++++++----- test/contracts-network/colony-staking.js | 4 +- test/extensions/expenditure-utils.js | 33 ++++++++-- 3 files changed, 88 insertions(+), 22 deletions(-) diff --git a/contracts/extensions/ExpenditureUtils.sol b/contracts/extensions/ExpenditureUtils.sol index c78c4b2b70..8ccdfe2aa4 100644 --- a/contracts/extensions/ExpenditureUtils.sol +++ b/contracts/extensions/ExpenditureUtils.sol @@ -133,7 +133,28 @@ contract ExpenditureUtils is ColonyExtensionMeta, PatriciaTreeProofs { delete stakes[_expenditureId]; } - function cancelExpenditureAndPunish( + 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(), "expenditure-utils-must-be-owner"); + + require( + expenditure.status == ColonyDataTypes.ExpenditureStatus.Draft, + "expenditure-utils-expenditure-not-draft" + ); + + cancelExpenditure(_permissionDomainId, _childSkillIndex, _expenditureId, expenditure.owner); + reclaimStake(_expenditureId); + } + + function cancelAndPunish( uint256 _permissionDomainId, uint256 _childSkillIndex, uint256 _expenditureId, @@ -171,16 +192,7 @@ contract ExpenditureUtils is ColonyExtensionMeta, PatriciaTreeProofs { delete stakes[_expenditureId]; } - // 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 0x{owner}{state} value - bytes32 value = bytes32(bytes20(expenditure.owner)) >> 0x58 | bytes32(uint256(ColonyDataTypes.ExpenditureStatus.Cancelled)); - - colony.setExpenditureState(_permissionDomainId, _childSkillIndex, _expenditureId, EXPENDITURE_SLOT, mask, keys, value); + cancelExpenditure(_permissionDomainId, _childSkillIndex, _expenditureId, expenditure.owner); } uint256 constant EXPENDITURE_SLOT = 25; @@ -192,20 +204,20 @@ contract ExpenditureUtils is ColonyExtensionMeta, PatriciaTreeProofs { /// @notice Sets the payout modifiers in given expenditure slots, using the arbitration permission /// @param _permissionDomainId The domainId in which the extension has the arbitration permission /// @param _childSkillIndex The index that the `_domainId` is relative to `_permissionDomainId` - /// @param _id Expenditure identifier + /// @param _expenditureId Expenditure identifier /// @param _slots Array of slots to set payout modifiers /// @param _payoutModifiers Values (between +/- WAD) to modify the payout & reputation bonus function setExpenditurePayoutModifiers( uint256 _permissionDomainId, uint256 _childSkillIndex, - uint256 _id, + uint256 _expenditureId, uint256[] memory _slots, int256[] memory _payoutModifiers ) public { require(_slots.length == _payoutModifiers.length, "expenditure-utils-bad-slots"); - require(colony.getExpenditure(_id).owner == msgSender(), "expenditure-utils-not-owner"); + require(colony.getExpenditure(_expenditureId).owner == msgSender(), "expenditure-utils-not-owner"); bool[] memory mask = new bool[](2); bytes32[] memory keys = new bytes32[](2); @@ -221,7 +233,7 @@ contract ExpenditureUtils is ColonyExtensionMeta, PatriciaTreeProofs { colony.setExpenditureState( _permissionDomainId, _childSkillIndex, - _id, + _expenditureId, EXPENDITURESLOTS_SLOT, mask, keys, @@ -238,6 +250,37 @@ function getStakeFraction() public view returns (uint256) { // Internal +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 0x{owner}{state} value + bytes32 value = ( + bytes32(bytes20(_expenditureOwner)) >> 0x58 | + bytes32(uint256(ColonyDataTypes.ExpenditureStatus.Cancelled)) + ); + + colony.setExpenditureState( + _permissionDomainId, + _childSkillIndex, + _expenditureId, + EXPENDITURE_SLOT, + mask, + keys, + value + ); +} + function getReputationFromProof( uint256 _domainId, bytes memory _key, diff --git a/test/contracts-network/colony-staking.js b/test/contracts-network/colony-staking.js index b5afa97090..b919bf06d9 100644 --- a/test/contracts-network/colony-staking.js +++ b/test/contracts-network/colony-staking.js @@ -251,8 +251,8 @@ contract("Colony Staking", (accounts) => { it("should send the stake to address(0x0) if the token does not support burning", async () => { // This token does not support burning - const token = await ToggleableToken.new(WAD, { from: USER1 }); - const colony = await setupColony(colonyNetwork, token.address); + 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 }); diff --git a/test/extensions/expenditure-utils.js b/test/extensions/expenditure-utils.js index 470a7a41d1..4df1b5c99a 100644 --- a/test/extensions/expenditure-utils.js +++ b/test/extensions/expenditure-utils.js @@ -230,7 +230,7 @@ contract("ExpenditureUtils", (accounts) => { const expenditureId = await colony.getExpenditureCount(); await colony.lockExpenditure(expenditureId); - await expenditureUtils.cancelExpenditure(1, UINT256_MAX, expenditureId, true); + await expenditureUtils.cancelAndPunish(1, UINT256_MAX, expenditureId, true); const obligation = await tokenLocking.getObligation(USER0, token.address, colony.address); expect(obligation).to.be.zero; @@ -260,7 +260,7 @@ contract("ExpenditureUtils", (accounts) => { await colony.lockExpenditure(expenditureId); await checkErrorRevert( - expenditureUtils.cancelExpenditure(1, UINT256_MAX, expenditureId, true, { from: USER1 }), + expenditureUtils.cancelAndPunish(1, UINT256_MAX, expenditureId, true, { from: USER1 }), "expenditure-utils-caller-not-arbitration" ); }); @@ -270,7 +270,7 @@ contract("ExpenditureUtils", (accounts) => { const expenditureId = await colony.getExpenditureCount(); await colony.lockExpenditure(expenditureId); - await expenditureUtils.cancelExpenditure(1, UINT256_MAX, expenditureId, false); + await expenditureUtils.cancelAndPunish(1, UINT256_MAX, expenditureId, false); let obligation; let userLock; @@ -301,7 +301,7 @@ contract("ExpenditureUtils", (accounts) => { await colony.transferExpenditure(expenditureId, USER1, { from: USER0 }); - await expenditureUtils.cancelExpenditure(1, UINT256_MAX, expenditureId, true); + await expenditureUtils.cancelAndPunish(1, UINT256_MAX, expenditureId, true); const obligation = await tokenLocking.getObligation(USER0, token.address, colony.address); expect(obligation).to.be.zero; @@ -315,7 +315,7 @@ contract("ExpenditureUtils", (accounts) => { const expenditureId = await colony.getExpenditureCount(); await colony.lockExpenditure(expenditureId); - await checkErrorRevert(expenditureUtils.cancelExpenditure(1, UINT256_MAX, expenditureId, true), "expenditure-utils-nothing-to-slash"); + await checkErrorRevert(expenditureUtils.cancelAndPunish(1, UINT256_MAX, expenditureId, true), "expenditure-utils-nothing-to-slash"); }); it("can reclaim the stake by cancelling the expenditure", async () => { @@ -333,6 +333,29 @@ contract("ExpenditureUtils", (accounts) => { expect(userLock.balance).to.eq.BN(WAD); }); + it("can cancel and reclaim the stake in one transaction", async () => { + await expenditureUtils.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); + const expenditureId = await colony.getExpenditureCount(); + + await expenditureUtils.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 expenditureUtils.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); + const expenditureId = await colony.getExpenditureCount(); + + await checkErrorRevert( + expenditureUtils.cancelAndReclaimStake(1, UINT256_MAX, expenditureId, { from: USER1 }), + "expenditure-utils-must-be-owner" + ); + }); + it("can reclaim the stake by finalizing the expenditure", async () => { await expenditureUtils.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); const expenditureId = await colony.getExpenditureCount(); From 1c36be2752788114962b5dfdae32680ce1fff218 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Wed, 22 Jun 2022 09:00:25 -0700 Subject: [PATCH 31/59] Respond to review comments --- contracts/colony/ColonyExpenditure.sol | 8 +++---- contracts/colony/ColonyFunding.sol | 28 +++++++++++++++++++++-- contracts/colony/IColony.sol | 8 +++++++ contracts/extensions/ExpenditureUtils.sol | 21 +++++++++++++---- test/extensions/expenditure-utils.js | 17 ++++++++++---- 5 files changed, 65 insertions(+), 17 deletions(-) diff --git a/contracts/colony/ColonyExpenditure.sol b/contracts/colony/ColonyExpenditure.sol index e78461b6be..fd9fed4f60 100644 --- a/contracts/colony/ColonyExpenditure.sol +++ b/contracts/colony/ColonyExpenditure.sol @@ -254,7 +254,7 @@ contract ColonyExpenditure is ColonyStorage { 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); } + if (_payoutTokens.length > 0) { setExpenditurePayouts(_id, _payoutSlots, _payoutTokens, _payoutValues); } } // Deprecated @@ -362,15 +362,13 @@ contract ColonyExpenditure is ColonyStorage { function setExpenditurePayouts( uint256 _id, - address[] memory _payoutTokens, uint256[][] memory _payoutSlots, + address[] memory _payoutTokens, uint256[][] memory _payoutValues ) internal { - for (uint256 i; i < _payoutTokens.length; i++) { - IColony(address(this)).setExpenditurePayouts(_id, _payoutSlots[i], _payoutTokens[i], _payoutValues[i]); - } + IColony(address(this)).setExpenditurePayouts(_id, _payoutSlots, _payoutTokens, _payoutValues); } bool constant MAPPING = false; diff --git a/contracts/colony/ColonyFunding.sol b/contracts/colony/ColonyFunding.sol index dfacb5bc41..75901c75f9 100755 --- a/contracts/colony/ColonyFunding.sol +++ b/contracts/colony/ColonyFunding.sol @@ -52,7 +52,8 @@ contract ColonyFunding is ColonyStorage { // ignore-swc-123 } } - function moveFundsBetweenPots( + /// @notice @deprecated + function moveFundsBetweenPots( uint256 _permissionDomainId, uint256 _childSkillIndex, uint256 _domainId, @@ -75,6 +76,7 @@ contract ColonyFunding is ColonyStorage { // ignore-swc-123 moveFundsBetweenPotsFunctionality(_fromPot, _toPot, _amount, _token); } + /// @notice @deprecated function moveFundsBetweenPots( uint256 _permissionDomainId, uint256 _fromChildSkillIndex, @@ -164,7 +166,29 @@ contract ColonyFunding is ColonyStorage { // ignore-swc-123 } } - function setExpenditurePayouts(uint256 _id, uint256[] memory _slots, address _token, uint256[] memory _amounts) + function setExpenditurePayouts( + uint256 _id, + uint256[][] memory _slots, + address[] memory _tokens, + uint256[][] memory _amounts + ) + public + stoppable + expenditureExists(_id) + expenditureDraft(_id) + expenditureSelfOrOwner(_id) + { + for (uint256 i; i < _tokens.length; i++) { + setExpenditurePayoutsInternal(_id, _slots[i], _tokens[i], _amounts[i]); + } + } + + function setExpenditurePayouts( + uint256 _id, + uint256[] memory _slots, + address _token, + uint256[] memory _amounts + ) public stoppable expenditureExists(_id) diff --git a/contracts/colony/IColony.sol b/contracts/colony/IColony.sol index f783357557..3057c58d1e 100644 --- a/contracts/colony/IColony.sol +++ b/contracts/colony/IColony.sol @@ -445,6 +445,14 @@ interface IColony is ColonyDataTypes, IRecovery, IBasicMetaTransaction { function setExpenditurePayouts(uint256 _id, uint256[] memory _slots, address _token, uint256[] memory _amounts) external; /// @notice @deprecated + /// @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 2D array of slots to set payouts + /// @param _tokens Array of token addresses, `0x0` value indicates Ether + /// @param _amounts 2D array of payout amounts + function setExpenditurePayouts(uint256 _id, uint256[][] memory _slots, address[] memory _tokens, uint256[][] memory _amounts) external; + /// @notice Set the token payouts in given expenditure slots. 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` diff --git a/contracts/extensions/ExpenditureUtils.sol b/contracts/extensions/ExpenditureUtils.sol index 8ccdfe2aa4..28213db9b7 100644 --- a/contracts/extensions/ExpenditureUtils.sol +++ b/contracts/extensions/ExpenditureUtils.sol @@ -42,7 +42,6 @@ contract ExpenditureUtils is ColonyExtensionMeta, PatriciaTreeProofs { // Storage uint256 stakeFraction; - uint256 repPenaltyFraction; mapping (uint256 => Stake) stakes; @@ -157,6 +156,8 @@ contract ExpenditureUtils is ColonyExtensionMeta, PatriciaTreeProofs { function cancelAndPunish( uint256 _permissionDomainId, uint256 _childSkillIndex, + uint256 _callerPermissionDomainId, + uint256 _callerChildSkillIndex, uint256 _expenditureId, bool _punish ) @@ -165,7 +166,13 @@ contract ExpenditureUtils is ColonyExtensionMeta, PatriciaTreeProofs { ColonyDataTypes.Expenditure memory expenditure = colony.getExpenditure(_expenditureId); require( - colony.hasInheritedUserRole(msgSender(), _permissionDomainId, ColonyDataTypes.ColonyRole.Arbitration, _childSkillIndex, expenditure.domainId), + colony.hasInheritedUserRole( + msgSender(), + _callerPermissionDomainId, + ColonyDataTypes.ColonyRole.Arbitration, + _callerChildSkillIndex, + expenditure.domainId + ), "expenditure-utils-caller-not-arbitration" ); @@ -248,6 +255,10 @@ function getStakeFraction() public view returns (uint256) { return stakeFraction; } +function getStake(uint256 _expenditureId) public view returns (Stake memory stake) { + return stakes[_expenditureId]; +} + // Internal function cancelExpenditure( @@ -264,10 +275,10 @@ function cancelExpenditure( bytes32[] memory keys = new bytes32[](1); keys[0] = bytes32(uint256(0)); - // Prepare the new 0x{owner}{state} value + // Prepare the new 0x000...{owner}{state} value bytes32 value = ( - bytes32(bytes20(_expenditureOwner)) >> 0x58 | - bytes32(uint256(ColonyDataTypes.ExpenditureStatus.Cancelled)) + 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( diff --git a/test/extensions/expenditure-utils.js b/test/extensions/expenditure-utils.js index 4df1b5c99a..cc563cdab2 100644 --- a/test/extensions/expenditure-utils.js +++ b/test/extensions/expenditure-utils.js @@ -182,6 +182,10 @@ contract("ExpenditureUtils", (accounts) => { const obligation = await tokenLocking.getObligation(USER0, token.address, colony.address); expect(obligation).to.eq.BN(requiredStake); + + const stake = await expenditureUtils.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 () => { @@ -230,7 +234,7 @@ contract("ExpenditureUtils", (accounts) => { const expenditureId = await colony.getExpenditureCount(); await colony.lockExpenditure(expenditureId); - await expenditureUtils.cancelAndPunish(1, UINT256_MAX, expenditureId, true); + await expenditureUtils.cancelAndPunish(1, UINT256_MAX, 1, UINT256_MAX, expenditureId, true); const obligation = await tokenLocking.getObligation(USER0, token.address, colony.address); expect(obligation).to.be.zero; @@ -260,7 +264,7 @@ contract("ExpenditureUtils", (accounts) => { await colony.lockExpenditure(expenditureId); await checkErrorRevert( - expenditureUtils.cancelAndPunish(1, UINT256_MAX, expenditureId, true, { from: USER1 }), + expenditureUtils.cancelAndPunish(1, UINT256_MAX, 1, UINT256_MAX, expenditureId, true, { from: USER1 }), "expenditure-utils-caller-not-arbitration" ); }); @@ -270,7 +274,7 @@ contract("ExpenditureUtils", (accounts) => { const expenditureId = await colony.getExpenditureCount(); await colony.lockExpenditure(expenditureId); - await expenditureUtils.cancelAndPunish(1, UINT256_MAX, expenditureId, false); + await expenditureUtils.cancelAndPunish(1, UINT256_MAX, 1, UINT256_MAX, expenditureId, false); let obligation; let userLock; @@ -301,7 +305,7 @@ contract("ExpenditureUtils", (accounts) => { await colony.transferExpenditure(expenditureId, USER1, { from: USER0 }); - await expenditureUtils.cancelAndPunish(1, UINT256_MAX, expenditureId, true); + await expenditureUtils.cancelAndPunish(1, UINT256_MAX, 1, UINT256_MAX, expenditureId, true); const obligation = await tokenLocking.getObligation(USER0, token.address, colony.address); expect(obligation).to.be.zero; @@ -315,7 +319,10 @@ contract("ExpenditureUtils", (accounts) => { const expenditureId = await colony.getExpenditureCount(); await colony.lockExpenditure(expenditureId); - await checkErrorRevert(expenditureUtils.cancelAndPunish(1, UINT256_MAX, expenditureId, true), "expenditure-utils-nothing-to-slash"); + await checkErrorRevert( + expenditureUtils.cancelAndPunish(1, UINT256_MAX, 1, UINT256_MAX, expenditureId, true), + "expenditure-utils-nothing-to-slash" + ); }); it("can reclaim the stake by cancelling the expenditure", async () => { From 21bc08f51591d402ac4753cd61549af8744292be Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Wed, 22 Jun 2022 10:23:10 -0700 Subject: [PATCH 32/59] Restore EvaluatedExpenditure --- contracts/extensions/EvaluatedExpenditure.sol | 115 ++++++++++++++ contracts/extensions/ExpenditureUtils.sol | 118 +++++---------- migrations/9_setup_extensions.js | 2 + scripts/check-recovery.js | 1 + scripts/check-storage.js | 1 + test-smoke/colony-storage-consistent.js | 10 +- test/extensions/evaluated-expenditure.js | 141 ++++++++++++++++++ test/extensions/expenditure-utils.js | 56 +------ 8 files changed, 303 insertions(+), 141 deletions(-) create mode 100644 contracts/extensions/EvaluatedExpenditure.sol create mode 100644 test/extensions/evaluated-expenditure.js diff --git a/contracts/extensions/EvaluatedExpenditure.sol b/contracts/extensions/EvaluatedExpenditure.sol new file mode 100644 index 0000000000..1f47a871b2 --- /dev/null +++ b/contracts/extensions/EvaluatedExpenditure.sol @@ -0,0 +1,115 @@ +/* + 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 "./ColonyExtension.sol"; +import "./../common/BasicMetaTransaction.sol"; + +// ignore-file-swc-108 + + +contract EvaluatedExpenditure is ColonyExtension, BasicMetaTransaction { + + uint256 constant EXPENDITURESLOTS_SLOT = 26; + uint256 constant PAYOUT_MODIFIER_OFFSET = 2; + bool constant MAPPING = false; + bool constant ARRAY = true; + mapping(address => uint256) metatransactionNonces; + + /// @notice Returns the identifier of the extension + function identifier() public override pure returns (bytes32) { + return keccak256("EvaluatedExpenditure"); + } + + /// @notice Returns the version of the extension + function version() public override pure returns (uint256) { + return 2; + } + + /// @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)))); + } + + function getMetatransactionNonce(address _userAddress) override public view returns (uint256 nonce){ + return metatransactionNonces[_userAddress]; + } + + function incrementMetatransactionNonce(address _user) override internal { + metatransactionNonces[_user] = add(metatransactionNonces[_user], 1); + } + + /// @notice Sets the payout modifiers in given expenditure slots, using the arbitration permission + /// @param _permissionDomainId The domainId in which the extension has the arbitration permission + /// @param _childSkillIndex The index that the `_domainId` is relative to `_permissionDomainId` + /// @param _id Expenditure identifier + /// @param _slots Array of slots to set payout modifiers + /// @param _payoutModifiers Values (between +/- WAD) to modify the payout & reputation bonus + function setExpenditurePayoutModifiers( + uint256 _permissionDomainId, + uint256 _childSkillIndex, + uint256 _id, + uint256[] memory _slots, + int256[] memory _payoutModifiers + ) + public + { + require(_slots.length == _payoutModifiers.length, "evaluated-expenditure-bad-slots"); + require(colony.getExpenditure(_id).owner == msgSender(), "evaluated-expenditure-not-owner"); + + bool[] memory mask = new bool[](2); + bytes32[] memory keys = new bytes32[](2); + + mask[0] = MAPPING; + mask[1] = ARRAY; + + keys[1] = bytes32(PAYOUT_MODIFIER_OFFSET); + + for (uint256 i; i < _slots.length; i++) { + keys[0] = bytes32(_slots[i]); + + colony.setExpenditureState( + _permissionDomainId, + _childSkillIndex, + _id, + EXPENDITURESLOTS_SLOT, + mask, + keys, + bytes32(_payoutModifiers[i]) + ); + } + } + +} diff --git a/contracts/extensions/ExpenditureUtils.sol b/contracts/extensions/ExpenditureUtils.sol index 28213db9b7..574ac07b56 100644 --- a/contracts/extensions/ExpenditureUtils.sol +++ b/contracts/extensions/ExpenditureUtils.sol @@ -128,7 +128,7 @@ contract ExpenditureUtils is ColonyExtensionMeta, PatriciaTreeProofs { colony.deobligateStake(stake.creator, expenditure.domainId, stake.amount); - // slither-disable-next-line reentrancy-no-eth + // slither-disable-next-line reentrancy-no-eth delete stakes[_expenditureId]; } @@ -202,97 +202,53 @@ contract ExpenditureUtils is ColonyExtensionMeta, PatriciaTreeProofs { cancelExpenditure(_permissionDomainId, _childSkillIndex, _expenditureId, expenditure.owner); } + // View + + function getStakeFraction() public view returns (uint256) { + return stakeFraction; + } + + function getStake(uint256 _expenditureId) public view returns (Stake memory stake) { + return stakes[_expenditureId]; + } + + // Internal + uint256 constant EXPENDITURE_SLOT = 25; - uint256 constant EXPENDITURESLOTS_SLOT = 26; - uint256 constant PAYOUT_MODIFIER_OFFSET = 2; - bool constant MAPPING = false; bool constant ARRAY = true; - /// @notice Sets the payout modifiers in given expenditure slots, using the arbitration permission - /// @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 Expenditure identifier - /// @param _slots Array of slots to set payout modifiers - /// @param _payoutModifiers Values (between +/- WAD) to modify the payout & reputation bonus - function setExpenditurePayoutModifiers( + function cancelExpenditure( uint256 _permissionDomainId, uint256 _childSkillIndex, uint256 _expenditureId, - uint256[] memory _slots, - int256[] memory _payoutModifiers + address _expenditureOwner ) - public + internal { - require(_slots.length == _payoutModifiers.length, "expenditure-utils-bad-slots"); - require(colony.getExpenditure(_expenditureId).owner == msgSender(), "expenditure-utils-not-owner"); - - bool[] memory mask = new bool[](2); - bytes32[] memory keys = new bytes32[](2); - - mask[0] = MAPPING; - mask[1] = ARRAY; - - keys[1] = bytes32(PAYOUT_MODIFIER_OFFSET); - - for (uint256 i; i < _slots.length; i++) { - keys[0] = bytes32(_slots[i]); + // 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, - EXPENDITURESLOTS_SLOT, - mask, - keys, - bytes32(_payoutModifiers[i]) - ); - } + colony.setExpenditureState( + _permissionDomainId, + _childSkillIndex, + _expenditureId, + EXPENDITURE_SLOT, + mask, + keys, + value + ); } -// View - -function getStakeFraction() public view returns (uint256) { - return stakeFraction; -} - -function getStake(uint256 _expenditureId) public view returns (Stake memory stake) { - return stakes[_expenditureId]; -} - -// Internal - -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 - ); -} - -function getReputationFromProof( + function getReputationFromProof( uint256 _domainId, bytes memory _key, bytes memory _value, diff --git a/migrations/9_setup_extensions.js b/migrations/9_setup_extensions.js index 4cbb7f1fcb..964305ea0d 100644 --- a/migrations/9_setup_extensions.js +++ b/migrations/9_setup_extensions.js @@ -5,6 +5,7 @@ const { soliditySha3 } = require("web3-utils"); const { setupEtherRouter } = require("../helpers/upgradable-contracts"); const CoinMachine = artifacts.require("./CoinMachine"); +const EvaluatedExpenditure = artifacts.require("./EvaluatedExpenditure"); const ExpenditureUtils = artifacts.require("./ExpenditureUtils"); const FundingQueue = artifacts.require("./FundingQueue"); const OneTxPayment = artifacts.require("./OneTxPayment"); @@ -37,6 +38,7 @@ module.exports = async function (deployer, network, accounts) { const colonyNetwork = await IColonyNetwork.at(etherRouterDeployed.address); await addExtension(colonyNetwork, "CoinMachine", CoinMachine); + await addExtension(colonyNetwork, "EvaluatedExpenditure", EvaluatedExpenditure); await addExtension(colonyNetwork, "ExpenditureUtils", ExpenditureUtils); await addExtension(colonyNetwork, "FundingQueue", FundingQueue); await addExtension(colonyNetwork, "OneTxPayment", OneTxPayment); diff --git a/scripts/check-recovery.js b/scripts/check-recovery.js index 12ca19b476..923182cf10 100755 --- a/scripts/check-recovery.js +++ b/scripts/check-recovery.js @@ -47,6 +47,7 @@ walkSync("./contracts/").forEach((contractName) => { "contracts/extensions/CoinMachine.sol", "contracts/extensions/ColonyExtension.sol", "contracts/extensions/ColonyExtensionMeta.sol", + "contracts/extensions/EvaluatedExpenditure.sol", "contracts/extensions/ExpenditureUtils.sol", "contracts/extensions/FundingQueue.sol", "contracts/extensions/OneTxPayment.sol", diff --git a/scripts/check-storage.js b/scripts/check-storage.js index 46041633ce..057585da03 100755 --- a/scripts/check-storage.js +++ b/scripts/check-storage.js @@ -28,6 +28,7 @@ walkSync("./contracts/").forEach((contractName) => { "contracts/common/TokenAuthority.sol", // Imported from colonyToken repo "contracts/ens/ENSRegistry.sol", // Not directly used by any colony contracts "contracts/extensions/CoinMachine.sol", + "contracts/extensions/EvaluatedExpenditure.sol", "contracts/extensions/ExpenditureUtils.sol", "contracts/extensions/FundingQueue.sol", "contracts/extensions/ColonyExtension.sol", diff --git a/test-smoke/colony-storage-consistent.js b/test-smoke/colony-storage-consistent.js index caa489c4ef..2245605230 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("0xe4fbe2255acb920686f02300cb3106e81615fce53afc3f85995d60d3ffba136c"); - expect(colonyStateHash).to.equal("0xc518b7990ea77e039476631c26daa51cc46c2752f504c03faf78b4693aef77cb"); - expect(metaColonyStateHash).to.equal("0x103db27eda4f844d4cc09d762568e2aefde2cf02fd7608a383309bb11fb3f5df"); - expect(miningCycleStateHash).to.equal("0x64a9fc6f052001247ace92ee7594be3d4c75496f7278ce39c62617587712526f"); - expect(tokenLockingStateHash).to.equal("0x974e41fdb7c36bab361489ebf788732c32ffff8ce1a852f157576b7bc0e4d2fb"); + expect(colonyNetworkStateHash).to.equal("0x9f926099fdf7b8fbd8e8c6d246b0956ae6e51546b85be4a016bcf58911b98f4b"); + expect(colonyStateHash).to.equal("0x82cdec79eb889c8860763d07f4e7c5ad4cde875c966b1e904d75e951b128315f"); + expect(metaColonyStateHash).to.equal("0x97b3fe37d2143132be6760eda0dcb4cff66a1fc9f31f29377d20664c0b7406ed"); + expect(miningCycleStateHash).to.equal("0x63723c9783b0f0c20e8640fc383ed40dbc228a93cc73a18fa6c6e5f0c38e8694"); + expect(tokenLockingStateHash).to.equal("0xa4dd9734ad2f6b18ec82a5d10be671a5082d6400173d47ac1401580f57cd1638"); }); }); }); diff --git a/test/extensions/evaluated-expenditure.js b/test/extensions/evaluated-expenditure.js new file mode 100644 index 0000000000..5975e30c8a --- /dev/null +++ b/test/extensions/evaluated-expenditure.js @@ -0,0 +1,141 @@ +/* globals artifacts */ + +const chai = require("chai"); +const bnChai = require("bn-chai"); +const { ethers } = require("ethers"); +const { soliditySha3 } = require("web3-utils"); + +const { UINT256_MAX, WAD, ADDRESS_ZERO } = require("../../helpers/constants"); +const { checkErrorRevert, web3GetCode } = require("../../helpers/test-helper"); +const { setupRandomColony, getMetaTransactionParameters } = require("../../helpers/test-data-generator"); + +const { expect } = chai; +chai.use(bnChai(web3.utils.BN)); + +const IColonyNetwork = artifacts.require("IColonyNetwork"); +const EtherRouter = artifacts.require("EtherRouter"); +const EvaluatedExpenditure = artifacts.require("EvaluatedExpenditure"); + +const EVALUATED_EXPENDITURE = soliditySha3("EvaluatedExpenditure"); + +contract("EvaluatedExpenditure", (accounts) => { + let colonyNetwork; + let colony; + let evaluatedExpenditure; + let version; + + const USER0 = accounts[0]; + const USER1 = accounts[1]; + + before(async () => { + const etherRouter = await EtherRouter.deployed(); + colonyNetwork = await IColonyNetwork.at(etherRouter.address); + + const extension = await EvaluatedExpenditure.new(); + version = await extension.version(); + }); + + beforeEach(async () => { + ({ colony } = await setupRandomColony(colonyNetwork)); + + await colony.installExtension(EVALUATED_EXPENDITURE, version); + + const evaluatedExpenditureAddress = await colonyNetwork.getExtensionInstallation(EVALUATED_EXPENDITURE, colony.address); + evaluatedExpenditure = await EvaluatedExpenditure.at(evaluatedExpenditureAddress); + + await colony.setArbitrationRole(1, UINT256_MAX, evaluatedExpenditure.address, 1, true); + }); + + describe("managing the extension", async () => { + it("can install the extension manually", async () => { + evaluatedExpenditure = await EvaluatedExpenditure.new(); + await evaluatedExpenditure.install(colony.address); + + await checkErrorRevert(evaluatedExpenditure.install(colony.address), "extension-already-installed"); + + const identifier = await evaluatedExpenditure.identifier(); + expect(identifier).to.equal(EVALUATED_EXPENDITURE); + + const capabilityRoles = await evaluatedExpenditure.getCapabilityRoles("0x0"); + expect(capabilityRoles).to.equal(ethers.constants.HashZero); + + await evaluatedExpenditure.finishUpgrade(); + await evaluatedExpenditure.deprecate(true); + await evaluatedExpenditure.uninstall(); + + const code = await web3GetCode(evaluatedExpenditure.address); + expect(code).to.equal("0x"); + }); + + it("can install the extension with the extension manager", async () => { + ({ colony } = await setupRandomColony(colonyNetwork)); + await colony.installExtension(EVALUATED_EXPENDITURE, version, { from: USER0 }); + + await checkErrorRevert(colony.installExtension(EVALUATED_EXPENDITURE, version, { from: USER0 }), "colony-network-extension-already-installed"); + await checkErrorRevert(colony.uninstallExtension(EVALUATED_EXPENDITURE, { from: USER1 }), "ds-auth-unauthorized"); + + await colony.uninstallExtension(EVALUATED_EXPENDITURE, { from: USER0 }); + }); + + it("can't use the network-level functions if installed via ColonyNetwork", async () => { + await checkErrorRevert(evaluatedExpenditure.install(ADDRESS_ZERO, { from: USER1 }), "ds-auth-unauthorized"); + await checkErrorRevert(evaluatedExpenditure.finishUpgrade({ from: USER1 }), "ds-auth-unauthorized"); + await checkErrorRevert(evaluatedExpenditure.deprecate(true, { from: USER1 }), "ds-auth-unauthorized"); + await checkErrorRevert(evaluatedExpenditure.uninstall({ from: USER1 }), "ds-auth-unauthorized"); + }); + }); + + describe("using the extension", async () => { + let expenditureId; + + beforeEach(async () => { + await colony.makeExpenditure(1, UINT256_MAX, 1); + expenditureId = await colony.getExpenditureCount(); + + await colony.lockExpenditure(expenditureId); + }); + + it("can set the payout modifier in the locked state", async () => { + let expenditureSlot; + + expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); + expect(expenditureSlot.payoutModifier).to.be.zero; + + await evaluatedExpenditure.setExpenditurePayoutModifiers(1, UINT256_MAX, expenditureId, [0], [WAD], { from: USER0 }); + + expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); + expect(expenditureSlot.payoutModifier).to.eq.BN(WAD); + }); + + it("cannot set the payout modifier with bad arguments", async () => { + await checkErrorRevert( + evaluatedExpenditure.setExpenditurePayoutModifiers(1, UINT256_MAX, expenditureId, [0], [], { from: USER0 }), + "evaluated-expenditure-bad-slots" + ); + }); + + it("cannot set the payout modifier if not the owner", async () => { + await checkErrorRevert( + evaluatedExpenditure.setExpenditurePayoutModifiers(1, UINT256_MAX, expenditureId, [0], [WAD], { from: USER1 }), + "evaluated-expenditure-not-owner" + ); + }); + + it("can set the payout modifier via metatransaction", async () => { + const txData = await evaluatedExpenditure.contract.methods + .setExpenditurePayoutModifiers(1, UINT256_MAX.toString(), expenditureId.toString(), [0], [WAD.toString()]) + .encodeABI(); + + const { r, s, v } = await getMetaTransactionParameters(txData, USER0, evaluatedExpenditure.address); + + let expenditureSlot; + expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); + expect(expenditureSlot.payoutModifier).to.be.zero; + + await evaluatedExpenditure.executeMetaTransaction(USER0, txData, r, s, v, { from: USER1 }); + + expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); + expect(expenditureSlot.payoutModifier).to.eq.BN(WAD); + }); + }); +}); diff --git a/test/extensions/expenditure-utils.js b/test/extensions/expenditure-utils.js index cc563cdab2..26766f95d3 100644 --- a/test/extensions/expenditure-utils.js +++ b/test/extensions/expenditure-utils.js @@ -6,7 +6,7 @@ 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, getMetaTransactionParameters } = require("../../helpers/test-data-generator"); +const { setupRandomColony } = require("../../helpers/test-data-generator"); const { checkErrorRevert, web3GetCode, @@ -389,58 +389,4 @@ contract("ExpenditureUtils", (accounts) => { await checkErrorRevert(expenditureUtils.reclaimStake(0), "expenditure-utils-nothing-to-claim"); }); }); - - describe("setting the payout modifiers with arbitration", async () => { - let expenditureId; - - beforeEach(async () => { - await colony.makeExpenditure(1, UINT256_MAX, 1); - expenditureId = await colony.getExpenditureCount(); - - await colony.lockExpenditure(expenditureId); - }); - - it("can set the payout modifier in the locked state", async () => { - let expenditureSlot; - - expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); - expect(expenditureSlot.payoutModifier).to.be.zero; - - await expenditureUtils.setExpenditurePayoutModifiers(1, UINT256_MAX, expenditureId, [0], [WAD], { from: USER0 }); - - expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); - expect(expenditureSlot.payoutModifier).to.eq.BN(WAD); - }); - - it("cannot set the payout modifier with bad arguments", async () => { - await checkErrorRevert( - expenditureUtils.setExpenditurePayoutModifiers(1, UINT256_MAX, expenditureId, [0], [], { from: USER0 }), - "expenditure-utils-bad-slots" - ); - }); - - it("cannot set the payout modifier if not the owner", async () => { - await checkErrorRevert( - expenditureUtils.setExpenditurePayoutModifiers(1, UINT256_MAX, expenditureId, [0], [WAD], { from: USER1 }), - "expenditure-utils-not-owner" - ); - }); - - it("can set the payout modifier via metatransaction", async () => { - const txData = await expenditureUtils.contract.methods - .setExpenditurePayoutModifiers(1, UINT256_MAX.toString(), expenditureId.toString(), [0], [WAD.toString()]) - .encodeABI(); - - const { r, s, v } = await getMetaTransactionParameters(txData, USER0, expenditureUtils.address); - - let expenditureSlot; - expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); - expect(expenditureSlot.payoutModifier).to.be.zero; - - await expenditureUtils.executeMetaTransaction(USER0, txData, r, s, v, { from: USER1 }); - - expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); - expect(expenditureSlot.payoutModifier).to.eq.BN(WAD); - }); - }); }); From 098e8fb940cc8c016bda9cb5fa77b768bf6ad76d Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Thu, 23 Jun 2022 11:44:39 -0700 Subject: [PATCH 33/59] Rename ExpenditureUtils to StakedExpenditure --- ...nditureUtils.sol => StakedExpenditure.sol} | 4 +- migrations/9_setup_extensions.js | 4 +- scripts/check-recovery.js | 2 +- scripts/check-storage.js | 2 +- ...nditure-utils.js => staked-expenditure.js} | 110 +++++++++--------- 5 files changed, 61 insertions(+), 61 deletions(-) rename contracts/extensions/{ExpenditureUtils.sol => StakedExpenditure.sol} (98%) rename test/extensions/{expenditure-utils.js => staked-expenditure.js} (68%) diff --git a/contracts/extensions/ExpenditureUtils.sol b/contracts/extensions/StakedExpenditure.sol similarity index 98% rename from contracts/extensions/ExpenditureUtils.sol rename to contracts/extensions/StakedExpenditure.sol index 574ac07b56..1bf5ec3a87 100644 --- a/contracts/extensions/ExpenditureUtils.sol +++ b/contracts/extensions/StakedExpenditure.sol @@ -26,7 +26,7 @@ import "./ColonyExtensionMeta.sol"; // ignore-file-swc-108 -contract ExpenditureUtils is ColonyExtensionMeta, PatriciaTreeProofs { +contract StakedExpenditure is ColonyExtensionMeta, PatriciaTreeProofs { // Events @@ -56,7 +56,7 @@ contract ExpenditureUtils is ColonyExtensionMeta, PatriciaTreeProofs { /// @notice Returns the identifier of the extension function identifier() public override pure returns (bytes32) { - return keccak256("ExpenditureUtils"); + return keccak256("StakedExpenditure"); } /// @notice Returns the version of the extension diff --git a/migrations/9_setup_extensions.js b/migrations/9_setup_extensions.js index 964305ea0d..fc14f05482 100644 --- a/migrations/9_setup_extensions.js +++ b/migrations/9_setup_extensions.js @@ -6,7 +6,7 @@ const { setupEtherRouter } = require("../helpers/upgradable-contracts"); const CoinMachine = artifacts.require("./CoinMachine"); const EvaluatedExpenditure = artifacts.require("./EvaluatedExpenditure"); -const ExpenditureUtils = artifacts.require("./ExpenditureUtils"); +const StakedExpenditure = artifacts.require("./StakedExpenditure"); const FundingQueue = artifacts.require("./FundingQueue"); const OneTxPayment = artifacts.require("./OneTxPayment"); const StreamingPayments = artifacts.require("./StreamingPayments"); @@ -39,7 +39,7 @@ module.exports = async function (deployer, network, accounts) { await addExtension(colonyNetwork, "CoinMachine", CoinMachine); await addExtension(colonyNetwork, "EvaluatedExpenditure", EvaluatedExpenditure); - await addExtension(colonyNetwork, "ExpenditureUtils", ExpenditureUtils); + 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 923182cf10..5ffffa4ce4 100755 --- a/scripts/check-recovery.js +++ b/scripts/check-recovery.js @@ -48,7 +48,7 @@ walkSync("./contracts/").forEach((contractName) => { "contracts/extensions/ColonyExtension.sol", "contracts/extensions/ColonyExtensionMeta.sol", "contracts/extensions/EvaluatedExpenditure.sol", - "contracts/extensions/ExpenditureUtils.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 057585da03..583b0e6b55 100755 --- a/scripts/check-storage.js +++ b/scripts/check-storage.js @@ -29,7 +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/ExpenditureUtils.sol", + "contracts/extensions/StakedExpenditure.sol", "contracts/extensions/FundingQueue.sol", "contracts/extensions/ColonyExtension.sol", "contracts/extensions/ColonyExtensionMeta.sol", diff --git a/test/extensions/expenditure-utils.js b/test/extensions/staked-expenditure.js similarity index 68% rename from test/extensions/expenditure-utils.js rename to test/extensions/staked-expenditure.js index 26766f95d3..2773dcab25 100644 --- a/test/extensions/expenditure-utils.js +++ b/test/extensions/staked-expenditure.js @@ -24,17 +24,17 @@ chai.use(bnChai(web3.utils.BN)); const IColonyNetwork = artifacts.require("IColonyNetwork"); const ITokenLocking = artifacts.require("ITokenLocking"); const EtherRouter = artifacts.require("EtherRouter"); -const ExpenditureUtils = artifacts.require("ExpenditureUtils"); +const StakedExpenditure = artifacts.require("StakedExpenditure"); const IReputationMiningCycle = artifacts.require("IReputationMiningCycle"); -const EXPENDITURE_UTILS = soliditySha3("ExpenditureUtils"); +const EXPENDITURE_UTILS = soliditySha3("StakedExpenditure"); -contract("ExpenditureUtils", (accounts) => { +contract("StakedExpenditure", (accounts) => { let colonyNetwork; let colony; let token; let tokenLocking; - let expenditureUtils; + let stakedExpenditure; let version; let reputationTree; @@ -58,7 +58,7 @@ contract("ExpenditureUtils", (accounts) => { const tokenLockingAddress = await colonyNetwork.getTokenLocking(); tokenLocking = await ITokenLocking.at(tokenLockingAddress); - const extension = await ExpenditureUtils.new(); + const extension = await StakedExpenditure.new(); version = await extension.version(); }); @@ -67,11 +67,11 @@ contract("ExpenditureUtils", (accounts) => { await colony.installExtension(EXPENDITURE_UTILS, version); - const expenditureUtilsAddress = await colonyNetwork.getExtensionInstallation(EXPENDITURE_UTILS, colony.address); - expenditureUtils = await ExpenditureUtils.at(expenditureUtilsAddress); + const stakedExpenditureAddress = await colonyNetwork.getExtensionInstallation(EXPENDITURE_UTILS, colony.address); + stakedExpenditure = await StakedExpenditure.at(stakedExpenditureAddress); - await colony.setArbitrationRole(1, UINT256_MAX, expenditureUtils.address, 1, true); - await colony.setAdministrationRole(1, UINT256_MAX, expenditureUtils.address, 1, true); + 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); @@ -109,30 +109,30 @@ contract("ExpenditureUtils", (accounts) => { describe("managing the extension", async () => { it("can install the extension manually", async () => { - expenditureUtils = await ExpenditureUtils.new(); - await expenditureUtils.install(colony.address); + stakedExpenditure = await StakedExpenditure.new(); + await stakedExpenditure.install(colony.address); - await checkErrorRevert(expenditureUtils.install(colony.address), "extension-already-installed"); + await checkErrorRevert(stakedExpenditure.install(colony.address), "extension-already-installed"); - const identifier = await expenditureUtils.identifier(); + const identifier = await stakedExpenditure.identifier(); expect(identifier).to.equal(EXPENDITURE_UTILS); - const capabilityRoles = await expenditureUtils.getCapabilityRoles("0x0"); + const capabilityRoles = await stakedExpenditure.getCapabilityRoles("0x0"); expect(capabilityRoles).to.equal(ethers.constants.HashZero); - await expenditureUtils.finishUpgrade(); - await expenditureUtils.deprecate(true); - await expenditureUtils.uninstall(); + await stakedExpenditure.finishUpgrade(); + await stakedExpenditure.deprecate(true); + await stakedExpenditure.uninstall(); - const code = await web3GetCode(expenditureUtils.address); + 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(expenditureUtils.install(ADDRESS_ZERO, { from: USER1 }), "ds-auth-unauthorized"); - await checkErrorRevert(expenditureUtils.finishUpgrade({ from: USER1 }), "ds-auth-unauthorized"); - await checkErrorRevert(expenditureUtils.deprecate(true, { from: USER1 }), "ds-auth-unauthorized"); - await checkErrorRevert(expenditureUtils.uninstall({ from: USER1 }), "ds-auth-unauthorized"); + 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 () => { @@ -148,33 +148,33 @@ contract("ExpenditureUtils", (accounts) => { describe("using stakes to manage expenditures", async () => { beforeEach(async () => { - await expenditureUtils.setStakeFraction(WAD.divn(10)); // Stake of .3 WADs + 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(expenditureUtils.address, 1, WAD, { 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 expenditureUtils.setStakeFraction(WAD, { from: USER0 }); + await stakedExpenditure.setStakeFraction(WAD, { from: USER0 }); - const stakeFraction = await expenditureUtils.getStakeFraction(); + const stakeFraction = await stakedExpenditure.getStakeFraction(); expect(stakeFraction).to.eq.BN(WAD); // But not if not root! - await checkErrorRevert(expenditureUtils.setStakeFraction(WAD, { from: USER1 }), "expenditure-utils-caller-not-root"); + await checkErrorRevert(stakedExpenditure.setStakeFraction(WAD, { from: USER1 }), "expenditure-utils-caller-not-root"); // Also not greater than WAD! - await checkErrorRevert(expenditureUtils.setStakeFraction(WAD.addn(1), { from: USER0 }), "expenditure-utils-value-too-large"); + await checkErrorRevert(stakedExpenditure.setStakeFraction(WAD.addn(1), { from: USER0 }), "expenditure-utils-value-too-large"); }); it("can create an expenditure by submitting a stake", async () => { - await expenditureUtils.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); + await stakedExpenditure.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); const expenditureId = await colony.getExpenditureCount(); const { owner } = await colony.getExpenditure(expenditureId); @@ -183,7 +183,7 @@ contract("ExpenditureUtils", (accounts) => { const obligation = await tokenLocking.getObligation(USER0, token.address, colony.address); expect(obligation).to.eq.BN(requiredStake); - const stake = await expenditureUtils.getStake(expenditureId); + const stake = await stakedExpenditure.getStake(expenditureId); expect(stake.creator).to.equal(USER0); expect(stake.amount).to.eq.BN(requiredStake); }); @@ -200,7 +200,7 @@ contract("ExpenditureUtils", (accounts) => { value = makeReputationValue(WAD, 10); [mask, siblings] = await reputationTree.getProof(key); await checkErrorRevert( - expenditureUtils.makeExpenditureWithStake(1, UINT256_MAX, 1, key, value, mask, siblings), + stakedExpenditure.makeExpenditureWithStake(1, UINT256_MAX, 1, key, value, mask, siblings), "expenditure-utils-invalid-root-hash" ); @@ -208,7 +208,7 @@ contract("ExpenditureUtils", (accounts) => { value = makeReputationValue(WAD, 2); [mask, siblings] = await reputationTree.getProof(key); await checkErrorRevert( - expenditureUtils.makeExpenditureWithStake(1, UINT256_MAX, 1, key, value, mask, siblings), + stakedExpenditure.makeExpenditureWithStake(1, UINT256_MAX, 1, key, value, mask, siblings), "expenditure-utils-invalid-colony-address" ); @@ -216,7 +216,7 @@ contract("ExpenditureUtils", (accounts) => { value = makeReputationValue(WAD, 3); [mask, siblings] = await reputationTree.getProof(key); await checkErrorRevert( - expenditureUtils.makeExpenditureWithStake(1, UINT256_MAX, 1, key, value, mask, siblings), + stakedExpenditure.makeExpenditureWithStake(1, UINT256_MAX, 1, key, value, mask, siblings), "expenditure-utils-invalid-skill-id" ); @@ -224,17 +224,17 @@ contract("ExpenditureUtils", (accounts) => { value = makeReputationValue(WAD, 4); [mask, siblings] = await reputationTree.getProof(key); await checkErrorRevert( - expenditureUtils.makeExpenditureWithStake(1, UINT256_MAX, 1, key, value, mask, siblings), + stakedExpenditure.makeExpenditureWithStake(1, UINT256_MAX, 1, key, value, mask, siblings), "expenditure-utils-invalid-user-address" ); }); it("can slash the stake with the arbitration permission", async () => { - await expenditureUtils.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); + await stakedExpenditure.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); const expenditureId = await colony.getExpenditureCount(); await colony.lockExpenditure(expenditureId); - await expenditureUtils.cancelAndPunish(1, UINT256_MAX, 1, UINT256_MAX, expenditureId, true); + 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; @@ -259,22 +259,22 @@ contract("ExpenditureUtils", (accounts) => { }); it("cannot slash the stake without the arbitration permission", async () => { - await expenditureUtils.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); + await stakedExpenditure.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); const expenditureId = await colony.getExpenditureCount(); await colony.lockExpenditure(expenditureId); await checkErrorRevert( - expenditureUtils.cancelAndPunish(1, UINT256_MAX, 1, UINT256_MAX, expenditureId, true, { from: USER1 }), + stakedExpenditure.cancelAndPunish(1, UINT256_MAX, 1, UINT256_MAX, expenditureId, true, { from: USER1 }), "expenditure-utils-caller-not-arbitration" ); }); it("can cancel the expenditure without penalty", async () => { - await expenditureUtils.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); + await stakedExpenditure.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); const expenditureId = await colony.getExpenditureCount(); await colony.lockExpenditure(expenditureId); - await expenditureUtils.cancelAndPunish(1, UINT256_MAX, 1, UINT256_MAX, expenditureId, false); + await stakedExpenditure.cancelAndPunish(1, UINT256_MAX, 1, UINT256_MAX, expenditureId, false); let obligation; let userLock; @@ -289,7 +289,7 @@ contract("ExpenditureUtils", (accounts) => { expect(expenditure.status).to.eq.BN(CANCELLED); // User can reclaim the stake - await expenditureUtils.reclaimStake(expenditureId); + await stakedExpenditure.reclaimStake(expenditureId); obligation = await tokenLocking.getObligation(USER0, token.address, colony.address); expect(obligation).to.be.zero; @@ -299,13 +299,13 @@ contract("ExpenditureUtils", (accounts) => { }); it("if ownership is transferred, the original owner is still slashed", async () => { - await expenditureUtils.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); + 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 expenditureUtils.cancelAndPunish(1, UINT256_MAX, 1, UINT256_MAX, expenditureId, true); + 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; @@ -320,18 +320,18 @@ contract("ExpenditureUtils", (accounts) => { await colony.lockExpenditure(expenditureId); await checkErrorRevert( - expenditureUtils.cancelAndPunish(1, UINT256_MAX, 1, UINT256_MAX, expenditureId, true), + stakedExpenditure.cancelAndPunish(1, UINT256_MAX, 1, UINT256_MAX, expenditureId, true), "expenditure-utils-nothing-to-slash" ); }); it("can reclaim the stake by cancelling the expenditure", async () => { - await expenditureUtils.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); + await stakedExpenditure.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); const expenditureId = await colony.getExpenditureCount(); await colony.cancelExpenditure(expenditureId); - await expenditureUtils.reclaimStake(expenditureId); + await stakedExpenditure.reclaimStake(expenditureId); const obligation = await tokenLocking.getObligation(USER0, token.address, colony.address); expect(obligation).to.be.zero; @@ -341,10 +341,10 @@ contract("ExpenditureUtils", (accounts) => { }); it("can cancel and reclaim the stake in one transaction", async () => { - await expenditureUtils.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); + await stakedExpenditure.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); const expenditureId = await colony.getExpenditureCount(); - await expenditureUtils.cancelAndReclaimStake(1, UINT256_MAX, expenditureId); + await stakedExpenditure.cancelAndReclaimStake(1, UINT256_MAX, expenditureId); const obligation = await tokenLocking.getObligation(USER0, token.address, colony.address); expect(obligation).to.be.zero; @@ -354,22 +354,22 @@ contract("ExpenditureUtils", (accounts) => { }); it("cannot cancel and reclaim the stake in one transaction if not owner", async () => { - await expenditureUtils.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); + await stakedExpenditure.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); const expenditureId = await colony.getExpenditureCount(); await checkErrorRevert( - expenditureUtils.cancelAndReclaimStake(1, UINT256_MAX, expenditureId, { from: USER1 }), + stakedExpenditure.cancelAndReclaimStake(1, UINT256_MAX, expenditureId, { from: USER1 }), "expenditure-utils-must-be-owner" ); }); it("can reclaim the stake by finalizing the expenditure", async () => { - await expenditureUtils.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); + await stakedExpenditure.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); const expenditureId = await colony.getExpenditureCount(); await colony.finalizeExpenditure(expenditureId); - await expenditureUtils.reclaimStake(expenditureId); + await stakedExpenditure.reclaimStake(expenditureId); const obligation = await tokenLocking.getObligation(USER0, token.address, colony.address); expect(obligation).to.be.zero; @@ -379,14 +379,14 @@ contract("ExpenditureUtils", (accounts) => { }); it("cannot reclaim the stake while the expenditure is in progress", async () => { - await expenditureUtils.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); + await stakedExpenditure.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); const expenditureId = await colony.getExpenditureCount(); - await checkErrorRevert(expenditureUtils.reclaimStake(expenditureId), "expenditure-utils-expenditure-invalid-state"); + await checkErrorRevert(stakedExpenditure.reclaimStake(expenditureId), "expenditure-utils-expenditure-invalid-state"); }); it("cannot reclaim a nonexistent stake", async () => { - await checkErrorRevert(expenditureUtils.reclaimStake(0), "expenditure-utils-nothing-to-claim"); + await checkErrorRevert(stakedExpenditure.reclaimStake(0), "expenditure-utils-nothing-to-claim"); }); }); }); From 18592d2d8f0113b2cfd1a8b45f486b8982ed1bf5 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Thu, 23 Jun 2022 14:12:55 -0700 Subject: [PATCH 34/59] Respond to review comments II --- contracts/colony/ColonyDataTypes.sol | 7 ++++++ contracts/colony/ColonyExpenditure.sol | 10 ++++++--- contracts/colony/ColonyFunding.sol | 1 + contracts/colony/IColony.sol | 2 +- contracts/extensions/StakedExpenditure.sol | 26 +++++++++++++--------- 5 files changed, 32 insertions(+), 14 deletions(-) diff --git a/contracts/colony/ColonyDataTypes.sol b/contracts/colony/ColonyDataTypes.sol index 3921f453eb..66ea76e9e3 100755 --- a/contracts/colony/ColonyDataTypes.sol +++ b/contracts/colony/ColonyDataTypes.sol @@ -157,6 +157,13 @@ 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 slot Memory slot being set + /// @param value Value being set in the slot + event ExpenditureStateChanged(address agent, uint256 indexed expenditureId, bytes32 slot, 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 fd9fed4f60..237499be0d 100644 --- a/contracts/colony/ColonyExpenditure.sol +++ b/contracts/colony/ColonyExpenditure.sol @@ -337,7 +337,7 @@ contract ColonyExpenditure is ColonyStorage { require(false, "colony-expenditure-bad-slot"); } - executeStateChange(keccak256(abi.encode(_id, _storageSlot)), _mask, _keys, _value); + executeStateChange(_id, _storageSlot, _mask, _keys, _value); } // Public view functions @@ -360,6 +360,7 @@ contract ColonyExpenditure is ColonyStorage { // Internal functions + // Used to get around the stack depth limit function setExpenditurePayouts( uint256 _id, uint256[][] memory _payoutSlots, @@ -376,7 +377,8 @@ contract ColonyExpenditure is ColonyStorage { uint256 constant MAX_ARRAY = 1024; // Prevent writing arbitrary slots function executeStateChange( - bytes32 _slot, + uint256 _id, + uint256 _storageSlot, bool[] memory _mask, bytes32[] memory _keys, bytes32 _value @@ -386,7 +388,7 @@ contract ColonyExpenditure is ColonyStorage { require(_keys.length == _mask.length, "colony-expenditure-bad-mask"); bytes32 value = _value; - bytes32 slot = _slot; + bytes32 slot = keccak256(abi.encode(_id, _storageSlot)); // See https://solidity.readthedocs.io/en/v0.5.14/miscellaneous.html for (uint256 i; i < _keys.length; i++) { @@ -413,5 +415,7 @@ contract ColonyExpenditure is ColonyStorage { assembly { sstore(slot, value) // ignore-swc-124 } + + emit ExpenditureStateChanged(msgSender(), _id, slot, value); } } diff --git a/contracts/colony/ColonyFunding.sol b/contracts/colony/ColonyFunding.sol index 75901c75f9..85980a368f 100755 --- a/contracts/colony/ColonyFunding.sol +++ b/contracts/colony/ColonyFunding.sol @@ -183,6 +183,7 @@ contract ColonyFunding is ColonyStorage { // ignore-swc-123 } } + /// @notice @deprecated function setExpenditurePayouts( uint256 _id, uint256[] memory _slots, diff --git a/contracts/colony/IColony.sol b/contracts/colony/IColony.sol index 3057c58d1e..a3d595ad25 100644 --- a/contracts/colony/IColony.sol +++ b/contracts/colony/IColony.sol @@ -436,6 +436,7 @@ interface IColony is ColonyDataTypes, IRecovery, IBasicMetaTransaction { /// @param _amount Payout amount function setExpenditurePayout(uint256 _id, uint256 _slot, address _token, uint256 _amount) external; + /// @notice @deprecated /// @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 @@ -444,7 +445,6 @@ interface IColony is ColonyDataTypes, IRecovery, IBasicMetaTransaction { /// @param _amounts Payout amounts function setExpenditurePayouts(uint256 _id, uint256[] memory _slots, address _token, uint256[] memory _amounts) external; - /// @notice @deprecated /// @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 diff --git a/contracts/extensions/StakedExpenditure.sol b/contracts/extensions/StakedExpenditure.sol index 1bf5ec3a87..ccbbb7c12a 100644 --- a/contracts/extensions/StakedExpenditure.sol +++ b/contracts/extensions/StakedExpenditure.sol @@ -119,6 +119,10 @@ contract StakedExpenditure is ColonyExtensionMeta, PatriciaTreeProofs { Stake storage stake = stakes[_expenditureId]; require(stake.amount > 0, "expenditure-utils-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 || @@ -126,10 +130,7 @@ contract StakedExpenditure is ColonyExtensionMeta, PatriciaTreeProofs { "expenditure-utils-expenditure-invalid-state" ); - colony.deobligateStake(stake.creator, expenditure.domainId, stake.amount); - - // slither-disable-next-line reentrancy-no-eth - delete stakes[_expenditureId]; + colony.deobligateStake(stakeCreator, expenditure.domainId, stakeAmount); } function cancelAndReclaimStake( @@ -175,6 +176,10 @@ contract StakedExpenditure is ColonyExtensionMeta, PatriciaTreeProofs { ), "expenditure-utils-caller-not-arbitration" ); + require( + expenditure.status != ColonyDataTypes.ExpenditureStatus.Cancelled, + "expenditure-utils-expenditure-already-cancelled" + ); require( expenditure.status != ColonyDataTypes.ExpenditureStatus.Draft, @@ -185,18 +190,19 @@ contract StakedExpenditure is ColonyExtensionMeta, PatriciaTreeProofs { Stake storage stake = stakes[_expenditureId]; require(stake.amount > 0, "expenditure-utils-nothing-to-slash"); - colony.transferStake(_permissionDomainId, _childSkillIndex, address(this), stake.creator, expenditure.domainId, stake.amount, address(0x0)); + uint256 stakeAmount = stake.amount; + address stakeCreator = stake.creator; + delete stakes[_expenditureId]; + + colony.transferStake(_permissionDomainId, _childSkillIndex, address(this), stakeCreator, expenditure.domainId, stakeAmount, address(0x0)); colony.emitDomainReputationPenalty( _permissionDomainId, _childSkillIndex, expenditure.domainId, - stake.creator, - -int256(stake.amount) + stakeCreator, + -int256(stakeAmount) ); - - // slither-disable-next-line reentrancy-no-eth - delete stakes[_expenditureId]; } cancelExpenditure(_permissionDomainId, _childSkillIndex, _expenditureId, expenditure.owner); From 6ebbf3df6aaca8b83ec4c3f099fa7654e8d06a44 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Thu, 23 Jun 2022 14:15:51 -0700 Subject: [PATCH 35/59] Update smoke tests --- test-smoke/colony-storage-consistent.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-smoke/colony-storage-consistent.js b/test-smoke/colony-storage-consistent.js index 2245605230..3ddf95d0b8 100644 --- a/test-smoke/colony-storage-consistent.js +++ b/test-smoke/colony-storage-consistent.js @@ -149,7 +149,7 @@ contract("Contract Storage", (accounts) => { console.log("miningCycleStateHash:", miningCycleStateHash); console.log("tokenLockingStateHash:", tokenLockingStateHash); - expect(colonyNetworkStateHash).to.equal("0x9f926099fdf7b8fbd8e8c6d246b0956ae6e51546b85be4a016bcf58911b98f4b"); + expect(colonyNetworkStateHash).to.equal("0xbbd8e4df4e66a2bf51a32f33a60b619eb06c8cc8ea6c4c7e15b3a0c835930352"); expect(colonyStateHash).to.equal("0x82cdec79eb889c8860763d07f4e7c5ad4cde875c966b1e904d75e951b128315f"); expect(metaColonyStateHash).to.equal("0x97b3fe37d2143132be6760eda0dcb4cff66a1fc9f31f29377d20664c0b7406ed"); expect(miningCycleStateHash).to.equal("0x63723c9783b0f0c20e8640fc383ed40dbc228a93cc73a18fa6c6e5f0c38e8694"); From e4ef614ce8c53e3f4b87af81d387808f6a9ca51d Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Thu, 23 Jun 2022 16:55:13 -0700 Subject: [PATCH 36/59] Satisfy slither --- contracts/extensions/StakedExpenditure.sol | 30 ++++++++++++---------- test/extensions/staked-expenditure.js | 22 ++++++++-------- 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/contracts/extensions/StakedExpenditure.sol b/contracts/extensions/StakedExpenditure.sol index ccbbb7c12a..4ae45476c7 100644 --- a/contracts/extensions/StakedExpenditure.sol +++ b/contracts/extensions/StakedExpenditure.sol @@ -48,7 +48,7 @@ contract StakedExpenditure is ColonyExtensionMeta, PatriciaTreeProofs { // Modifiers modifier onlyRoot() { - require(colony.hasUserRole(msgSender(), 1, ColonyDataTypes.ColonyRole.Root), "expenditure-utils-caller-not-root"); + require(colony.hasUserRole(msgSender(), 1, ColonyDataTypes.ColonyRole.Root), "staked-expenditure-caller-not-root"); _; } @@ -88,7 +88,7 @@ contract StakedExpenditure is ColonyExtensionMeta, PatriciaTreeProofs { // Public function setStakeFraction(uint256 _stakeFraction) public onlyRoot { - require(_stakeFraction <= WAD, "expenditure-utils-value-too-large"); + require(_stakeFraction <= WAD, "staked-expenditure-value-too-large"); stakeFraction = _stakeFraction; } @@ -117,7 +117,7 @@ contract StakedExpenditure is ColonyExtensionMeta, PatriciaTreeProofs { function reclaimStake(uint256 _expenditureId) public { Stake storage stake = stakes[_expenditureId]; - require(stake.amount > 0, "expenditure-utils-nothing-to-claim"); + require(stake.amount > 0, "staked-expenditure-nothing-to-claim"); uint256 stakeAmount = stake.amount; address stakeCreator = stake.creator; @@ -127,7 +127,7 @@ contract StakedExpenditure is ColonyExtensionMeta, PatriciaTreeProofs { require( expenditure.status == ColonyDataTypes.ExpenditureStatus.Cancelled || expenditure.status == ColonyDataTypes.ExpenditureStatus.Finalized, - "expenditure-utils-expenditure-invalid-state" + "staked-expenditure-expenditure-invalid-state" ); colony.deobligateStake(stakeCreator, expenditure.domainId, stakeAmount); @@ -143,14 +143,16 @@ contract StakedExpenditure is ColonyExtensionMeta, PatriciaTreeProofs { Stake storage stake = stakes[_expenditureId]; ColonyDataTypes.Expenditure memory expenditure = colony.getExpenditure(_expenditureId); - require(expenditure.owner == msgSender(), "expenditure-utils-must-be-owner"); + require(expenditure.owner == msgSender(), "staked-expenditure-must-be-owner"); require( expenditure.status == ColonyDataTypes.ExpenditureStatus.Draft, - "expenditure-utils-expenditure-not-draft" + "staked-expenditure-expenditure-not-draft" ); cancelExpenditure(_permissionDomainId, _childSkillIndex, _expenditureId, expenditure.owner); + + // slither-disable-next-line reentrancy-no-eth reclaimStake(_expenditureId); } @@ -174,21 +176,21 @@ contract StakedExpenditure is ColonyExtensionMeta, PatriciaTreeProofs { _callerChildSkillIndex, expenditure.domainId ), - "expenditure-utils-caller-not-arbitration" + "staked-expenditure-caller-not-arbitration" ); require( expenditure.status != ColonyDataTypes.ExpenditureStatus.Cancelled, - "expenditure-utils-expenditure-already-cancelled" + "staked-expenditure-expenditure-already-cancelled" ); require( expenditure.status != ColonyDataTypes.ExpenditureStatus.Draft, - "expenditure-utils-expenditure-still-draft" + "staked-expenditure-expenditure-still-draft" ); if (_punish) { Stake storage stake = stakes[_expenditureId]; - require(stake.amount > 0, "expenditure-utils-nothing-to-slash"); + require(stake.amount > 0, "staked-expenditure-nothing-to-slash"); uint256 stakeAmount = stake.amount; address stakeCreator = stake.creator; @@ -267,7 +269,7 @@ contract StakedExpenditure is ColonyExtensionMeta, PatriciaTreeProofs { { bytes32 rootHash = IColonyNetwork(colony.getColonyNetwork()).getReputationRootHash(); bytes32 impliedRoot = getImpliedRootHashKey(_key, _value, _branchMask, _siblings); - require(rootHash == impliedRoot, "expenditure-utils-invalid-root-hash"); + require(rootHash == impliedRoot, "staked-expenditure-invalid-root-hash"); uint256 reputationValue; @@ -282,9 +284,9 @@ contract StakedExpenditure is ColonyExtensionMeta, PatriciaTreeProofs { keyUserAddress := mload(add(_key, 72)) } - require(keyColonyAddress == address(colony), "expenditure-utils-invalid-colony-address"); - require(keySkillId == colony.getDomain(_domainId).skillId, "expenditure-utils-invalid-skill-id"); - require(keyUserAddress == address(0x0), "expenditure-utils-invalid-user-address"); + 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/test/extensions/staked-expenditure.js b/test/extensions/staked-expenditure.js index 2773dcab25..014ef52b76 100644 --- a/test/extensions/staked-expenditure.js +++ b/test/extensions/staked-expenditure.js @@ -167,10 +167,10 @@ contract("StakedExpenditure", (accounts) => { expect(stakeFraction).to.eq.BN(WAD); // But not if not root! - await checkErrorRevert(stakedExpenditure.setStakeFraction(WAD, { from: USER1 }), "expenditure-utils-caller-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 }), "expenditure-utils-value-too-large"); + await checkErrorRevert(stakedExpenditure.setStakeFraction(WAD.addn(1), { from: USER0 }), "staked-expenditure-value-too-large"); }); it("can create an expenditure by submitting a stake", async () => { @@ -201,7 +201,7 @@ contract("StakedExpenditure", (accounts) => { [mask, siblings] = await reputationTree.getProof(key); await checkErrorRevert( stakedExpenditure.makeExpenditureWithStake(1, UINT256_MAX, 1, key, value, mask, siblings), - "expenditure-utils-invalid-root-hash" + "staked-expenditure-invalid-root-hash" ); key = makeReputationKey(ADDRESS_ZERO, domain1.skillId); @@ -209,7 +209,7 @@ contract("StakedExpenditure", (accounts) => { [mask, siblings] = await reputationTree.getProof(key); await checkErrorRevert( stakedExpenditure.makeExpenditureWithStake(1, UINT256_MAX, 1, key, value, mask, siblings), - "expenditure-utils-invalid-colony-address" + "staked-expenditure-invalid-colony-address" ); key = makeReputationKey(colony.address, 100); @@ -217,7 +217,7 @@ contract("StakedExpenditure", (accounts) => { [mask, siblings] = await reputationTree.getProof(key); await checkErrorRevert( stakedExpenditure.makeExpenditureWithStake(1, UINT256_MAX, 1, key, value, mask, siblings), - "expenditure-utils-invalid-skill-id" + "staked-expenditure-invalid-skill-id" ); key = makeReputationKey(colony.address, domain1.skillId, USER0); @@ -225,7 +225,7 @@ contract("StakedExpenditure", (accounts) => { [mask, siblings] = await reputationTree.getProof(key); await checkErrorRevert( stakedExpenditure.makeExpenditureWithStake(1, UINT256_MAX, 1, key, value, mask, siblings), - "expenditure-utils-invalid-user-address" + "staked-expenditure-invalid-user-address" ); }); @@ -265,7 +265,7 @@ contract("StakedExpenditure", (accounts) => { await checkErrorRevert( stakedExpenditure.cancelAndPunish(1, UINT256_MAX, 1, UINT256_MAX, expenditureId, true, { from: USER1 }), - "expenditure-utils-caller-not-arbitration" + "staked-expenditure-caller-not-arbitration" ); }); @@ -321,7 +321,7 @@ contract("StakedExpenditure", (accounts) => { await checkErrorRevert( stakedExpenditure.cancelAndPunish(1, UINT256_MAX, 1, UINT256_MAX, expenditureId, true), - "expenditure-utils-nothing-to-slash" + "staked-expenditure-nothing-to-slash" ); }); @@ -359,7 +359,7 @@ contract("StakedExpenditure", (accounts) => { await checkErrorRevert( stakedExpenditure.cancelAndReclaimStake(1, UINT256_MAX, expenditureId, { from: USER1 }), - "expenditure-utils-must-be-owner" + "staked-expenditure-must-be-owner" ); }); @@ -382,11 +382,11 @@ contract("StakedExpenditure", (accounts) => { await stakedExpenditure.makeExpenditureWithStake(1, UINT256_MAX, 1, domain1Key, domain1Value, domain1Mask, domain1Siblings, { from: USER0 }); const expenditureId = await colony.getExpenditureCount(); - await checkErrorRevert(stakedExpenditure.reclaimStake(expenditureId), "expenditure-utils-expenditure-invalid-state"); + await checkErrorRevert(stakedExpenditure.reclaimStake(expenditureId), "staked-expenditure-expenditure-invalid-state"); }); it("cannot reclaim a nonexistent stake", async () => { - await checkErrorRevert(stakedExpenditure.reclaimStake(0), "expenditure-utils-nothing-to-claim"); + await checkErrorRevert(stakedExpenditure.reclaimStake(0), "staked-expenditure-nothing-to-claim"); }); }); }); From 947637b2c7ee59c55c5587a01ce5d12c2ed0af0f Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Thu, 30 Jun 2022 10:36:13 -0700 Subject: [PATCH 37/59] Add two events to StakedExpenditure --- contracts/extensions/StakedExpenditure.sol | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/contracts/extensions/StakedExpenditure.sol b/contracts/extensions/StakedExpenditure.sol index 4ae45476c7..4c9438fd2a 100644 --- a/contracts/extensions/StakedExpenditure.sol +++ b/contracts/extensions/StakedExpenditure.sol @@ -31,6 +31,8 @@ contract StakedExpenditure is ColonyExtensionMeta, PatriciaTreeProofs { // Events event ExpenditureMadeViaStake(address indexed creator, uint256 expenditureId, uint256 stake); + event ExpenditureCancelled(uint256 expenditureId); + event StakeReclaimed(uint256 expenditureId); // Datatypes @@ -131,6 +133,8 @@ contract StakedExpenditure is ColonyExtensionMeta, PatriciaTreeProofs { ); colony.deobligateStake(stakeCreator, expenditure.domainId, stakeAmount); + + emit StakeReclaimed(_expenditureId); } function cancelAndReclaimStake( @@ -254,6 +258,8 @@ contract StakedExpenditure is ColonyExtensionMeta, PatriciaTreeProofs { keys, value ); + + emit ExpenditureCancelled(_expenditureId); } function getReputationFromProof( From d66348ec4b980e33d165bfb224ccc56ac3e35a2b Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Thu, 30 Jun 2022 15:33:13 -0700 Subject: [PATCH 38/59] Remove expenditureExists modifier --- contracts/colony/ColonyExpenditure.sol | 47 +++++--------------- contracts/colony/ColonyFunding.sol | 8 +--- contracts/colony/ColonyStorage.sol | 14 +++--- test/contracts-network/colony-expenditure.js | 6 +-- 4 files changed, 24 insertions(+), 51 deletions(-) diff --git a/contracts/colony/ColonyExpenditure.sol b/contracts/colony/ColonyExpenditure.sol index 237499be0d..42fb2bdb8e 100644 --- a/contracts/colony/ColonyExpenditure.sol +++ b/contracts/colony/ColonyExpenditure.sol @@ -68,9 +68,8 @@ contract ColonyExpenditure is ColonyStorage { function transferExpenditure(uint256 _id, address _newOwner) public stoppable - expenditureExists(_id) expenditureDraftOrLocked(_id) - expenditureSelfOrOwner(_id) + expenditureOwnerOrSelf(_id) { expenditures[_id].owner = _newOwner; @@ -86,7 +85,6 @@ contract ColonyExpenditure is ColonyStorage { ) public stoppable - expenditureExists(_id) expenditureDraftOrLocked(_id) authDomain(_permissionDomainId, _childSkillIndex, expenditures[_id].domainId) { @@ -98,9 +96,8 @@ contract ColonyExpenditure is ColonyStorage { function cancelExpenditure(uint256 _id) public stoppable - expenditureExists(_id) expenditureDraft(_id) - expenditureSelfOrOwner(_id) + expenditureOwnerOrSelf(_id) { expenditures[_id].status = ExpenditureStatus.Cancelled; @@ -110,9 +107,8 @@ contract ColonyExpenditure is ColonyStorage { function lockExpenditure(uint256 _id) public stoppable - expenditureExists(_id) expenditureDraft(_id) - expenditureSelfOrOwner(_id) + expenditureOwnerOrSelf(_id) { expenditures[_id].status = ExpenditureStatus.Locked; @@ -122,9 +118,8 @@ contract ColonyExpenditure is ColonyStorage { function finalizeExpenditure(uint256 _id) public stoppable - expenditureExists(_id) expenditureDraftOrLocked(_id) - expenditureSelfOrOwner(_id) + expenditureOwnerOrSelf(_id) { FundingPot storage fundingPot = fundingPots[expenditures[_id].fundingPotId]; require(fundingPot.payoutsWeCannotMake == 0, "colony-expenditure-not-funded"); @@ -138,9 +133,8 @@ contract ColonyExpenditure is ColonyStorage { function setExpenditureMetadata(uint256 _id, string memory _metadata) public stoppable - expenditureExists(_id) expenditureDraft(_id) - expenditureSelfOrOwner(_id) + expenditureOwnerOrSelf(_id) { emit ExpenditureMetadataSet(msgSender(), _id, _metadata); } @@ -153,7 +147,6 @@ contract ColonyExpenditure is ColonyStorage { ) public stoppable - expenditureExists(_id) authDomain(_permissionDomainId, _childSkillIndex, expenditures[_id].domainId) { emit ExpenditureMetadataSet(msgSender(), _id, _metadata); @@ -162,9 +155,8 @@ contract ColonyExpenditure is ColonyStorage { function setExpenditureRecipients(uint256 _id, uint256[] memory _slots, address payable[] memory _recipients) public stoppable - expenditureExists(_id) expenditureDraft(_id) - expenditureSelfOrOwner(_id) + expenditureOwnerOrSelf(_id) { require(_slots.length == _recipients.length, "colony-expenditure-bad-slots"); @@ -178,9 +170,8 @@ contract ColonyExpenditure is ColonyStorage { function setExpenditureSkills(uint256 _id, uint256[] memory _slots, uint256[] memory _skillIds) public stoppable - expenditureExists(_id) expenditureDraft(_id) - expenditureSelfOrOwner(_id) + expenditureOwnerOrSelf(_id) { require(_slots.length == _skillIds.length, "colony-expenditure-bad-slots"); IColonyNetwork colonyNetworkContract = IColonyNetwork(colonyNetworkAddress); @@ -201,9 +192,8 @@ contract ColonyExpenditure is ColonyStorage { function setExpenditureClaimDelays(uint256 _id, uint256[] memory _slots, uint256[] memory _claimDelays) public stoppable - expenditureExists(_id) expenditureDraft(_id) - expenditureSelfOrOwner(_id) + expenditureOwnerOrSelf(_id) { require(_slots.length == _claimDelays.length, "colony-expenditure-bad-slots"); @@ -217,9 +207,8 @@ contract ColonyExpenditure is ColonyStorage { function setExpenditurePayoutModifiers(uint256 _id, uint256[] memory _slots, int256[] memory _payoutModifiers) public stoppable - expenditureExists(_id) expenditureDraft(_id) - expenditureSelfOrOwner(_id) + expenditureOwnerOrSelf(_id) { require(_slots.length == _payoutModifiers.length, "colony-expenditure-bad-slots"); @@ -246,15 +235,14 @@ contract ColonyExpenditure is ColonyStorage { ) public stoppable - expenditureExists(_id) expenditureDraft(_id) - expenditureSelfOrOwner(_id) + expenditureOwnerOrSelf(_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, _payoutSlots, _payoutTokens, _payoutValues); } + if (_payoutTokens.length > 0) { IColony(address(this)).setExpenditurePayouts(_id, _payoutSlots, _payoutTokens, _payoutValues); } } // Deprecated @@ -307,7 +295,6 @@ contract ColonyExpenditure is ColonyStorage { ) public stoppable - expenditureExists(_id) authDomain(_permissionDomainId, _childSkillIndex, expenditures[_id].domainId) { // Only allow editing expenditure status, owner, finalizedTimestamp, and globalClaimDelay @@ -360,18 +347,6 @@ contract ColonyExpenditure is ColonyStorage { // Internal functions - // Used to get around the stack depth limit - function setExpenditurePayouts( - uint256 _id, - uint256[][] memory _payoutSlots, - address[] memory _payoutTokens, - uint256[][] memory _payoutValues - ) - internal - { - IColony(address(this)).setExpenditurePayouts(_id, _payoutSlots, _payoutTokens, _payoutValues); - } - 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 85980a368f..9aee6c0ebb 100755 --- a/contracts/colony/ColonyFunding.sol +++ b/contracts/colony/ColonyFunding.sol @@ -174,9 +174,8 @@ contract ColonyFunding is ColonyStorage { // ignore-swc-123 ) public stoppable - expenditureExists(_id) expenditureDraft(_id) - expenditureSelfOrOwner(_id) + expenditureOwnerOrSelf(_id) { for (uint256 i; i < _tokens.length; i++) { setExpenditurePayoutsInternal(_id, _slots[i], _tokens[i], _amounts[i]); @@ -192,9 +191,8 @@ contract ColonyFunding is ColonyStorage { // ignore-swc-123 ) public stoppable - expenditureExists(_id) expenditureDraft(_id) - expenditureSelfOrOwner(_id) + expenditureOwnerOrSelf(_id) { setExpenditurePayoutsInternal(_id, _slots, _token, _amounts); } @@ -209,7 +207,6 @@ contract ColonyFunding is ColonyStorage { // ignore-swc-123 ) public stoppable - expenditureExists(_id) authDomain(_permissionDomainId, _childSkillIndex, expenditures[_id].domainId) { setExpenditurePayoutsInternal(_id, _slots, _token, _amounts); @@ -231,7 +228,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]; diff --git a/contracts/colony/ColonyStorage.sol b/contracts/colony/ColonyStorage.sol index 62aa2f15c0..b438b3bf3b 100755 --- a/contracts/colony/ColonyStorage.sol +++ b/contracts/colony/ColonyStorage.sol @@ -169,17 +169,14 @@ contract ColonyStorage is ColonyDataTypes, ColonyNetworkDataTypes, DSMath, Commo _; } - modifier expenditureExists(uint256 _id) { - require(_id > 0 && _id <= expenditureCount, "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,11 +186,12 @@ 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"); _; } - modifier expenditureSelfOrOwner(uint256 _id) { + modifier expenditureOwnerOrSelf(uint256 _id) { require(expenditures[_id].owner == msgSender() || address(this) == msgSender(), "colony-expenditure-not-self-or-owner"); _; } @@ -330,6 +328,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/test/contracts-network/colony-expenditure.js b/test/contracts-network/colony-expenditure.js index 6839cf1975..ceb9ff83d7 100644 --- a/test/contracts-network/colony-expenditure.js +++ b/test/contracts-network/colony-expenditure.js @@ -178,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"); @@ -190,7 +190,7 @@ contract("Colony Expenditure", (accounts) => { await checkErrorRevert(colony.setExpenditureMetadata(100, ""), "colony-expenditure-does-not-exist"); await checkErrorRevert( colony.methods["setExpenditureMetadata(uint256,uint256,uint256,string)"](0, 0, 100, ""), - "colony-expenditure-does-not-exist" + "ds-auth-permission-domain-does-not-exist" ); await checkErrorRevert(colony.setExpenditureRecipients(100, [], []), "colony-expenditure-does-not-exist"); await checkErrorRevert(colony.setExpenditureClaimDelays(100, [], []), "colony-expenditure-does-not-exist"); From 21f4b7fac63fe6ef0cab3ab2e7f0a913051ec93e Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Fri, 1 Jul 2022 11:42:20 -0700 Subject: [PATCH 39/59] Respond to review comments III --- contracts/colony/ColonyExpenditure.sol | 2 ++ contracts/colony/ColonyStorage.sol | 5 ++++ contracts/extensions/StakedExpenditure.sol | 6 ++++- test/contracts-network/colony-expenditure.js | 2 +- test/extensions/staked-expenditure.js | 25 +++++++++++++------- 5 files changed, 30 insertions(+), 10 deletions(-) diff --git a/contracts/colony/ColonyExpenditure.sol b/contracts/colony/ColonyExpenditure.sol index 42fb2bdb8e..9be932fc15 100644 --- a/contracts/colony/ColonyExpenditure.sol +++ b/contracts/colony/ColonyExpenditure.sol @@ -147,6 +147,7 @@ contract ColonyExpenditure is ColonyStorage { ) public stoppable + validExpenditure(_id) authDomain(_permissionDomainId, _childSkillIndex, expenditures[_id].domainId) { emit ExpenditureMetadataSet(msgSender(), _id, _metadata); @@ -295,6 +296,7 @@ contract ColonyExpenditure is ColonyStorage { ) public stoppable + validExpenditure(_id) authDomain(_permissionDomainId, _childSkillIndex, expenditures[_id].domainId) { // Only allow editing expenditure status, owner, finalizedTimestamp, and globalClaimDelay diff --git a/contracts/colony/ColonyStorage.sol b/contracts/colony/ColonyStorage.sol index b438b3bf3b..931fd6f944 100755 --- a/contracts/colony/ColonyStorage.sol +++ b/contracts/colony/ColonyStorage.sol @@ -169,6 +169,11 @@ contract ColonyStorage is ColonyDataTypes, ColonyNetworkDataTypes, DSMath, Commo _; } + 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"); diff --git a/contracts/extensions/StakedExpenditure.sol b/contracts/extensions/StakedExpenditure.sol index 4c9438fd2a..2145a51242 100644 --- a/contracts/extensions/StakedExpenditure.sol +++ b/contracts/extensions/StakedExpenditure.sol @@ -33,6 +33,7 @@ contract StakedExpenditure is ColonyExtensionMeta, PatriciaTreeProofs { event ExpenditureMadeViaStake(address indexed creator, uint256 expenditureId, uint256 stake); event ExpenditureCancelled(uint256 expenditureId); event StakeReclaimed(uint256 expenditureId); + event StakeFractionSet(uint256 stakeFraction); // Datatypes @@ -92,6 +93,8 @@ contract StakedExpenditure is ColonyExtensionMeta, PatriciaTreeProofs { function setStakeFraction(uint256 _stakeFraction) public onlyRoot { require(_stakeFraction <= WAD, "staked-expenditure-value-too-large"); stakeFraction = _stakeFraction; + + emit StakeFractionSet(_stakeFraction); } function makeExpenditureWithStake( @@ -104,6 +107,7 @@ contract StakedExpenditure is ColonyExtensionMeta, PatriciaTreeProofs { bytes32[] memory _siblings ) public + notDeprecated { uint256 domainRep = getReputationFromProof(_domainId, _key, _value, _branchMask, _siblings); uint256 stakeAmount = wmul(domainRep, stakeFraction); @@ -119,7 +123,7 @@ contract StakedExpenditure is ColonyExtensionMeta, PatriciaTreeProofs { function reclaimStake(uint256 _expenditureId) public { Stake storage stake = stakes[_expenditureId]; - require(stake.amount > 0, "staked-expenditure-nothing-to-claim"); + require(stake.creator != address(0x0), "staked-expenditure-nothing-to-claim"); uint256 stakeAmount = stake.amount; address stakeCreator = stake.creator; diff --git a/test/contracts-network/colony-expenditure.js b/test/contracts-network/colony-expenditure.js index ceb9ff83d7..ffaf947e49 100644 --- a/test/contracts-network/colony-expenditure.js +++ b/test/contracts-network/colony-expenditure.js @@ -190,7 +190,7 @@ contract("Colony Expenditure", (accounts) => { await checkErrorRevert(colony.setExpenditureMetadata(100, ""), "colony-expenditure-does-not-exist"); await checkErrorRevert( colony.methods["setExpenditureMetadata(uint256,uint256,uint256,string)"](0, 0, 100, ""), - "ds-auth-permission-domain-does-not-exist" + "colony-expenditure-does-not-exist" ); await checkErrorRevert(colony.setExpenditureRecipients(100, [], []), "colony-expenditure-does-not-exist"); await checkErrorRevert(colony.setExpenditureClaimDelays(100, [], []), "colony-expenditure-does-not-exist"); diff --git a/test/extensions/staked-expenditure.js b/test/extensions/staked-expenditure.js index 014ef52b76..ba57f92138 100644 --- a/test/extensions/staked-expenditure.js +++ b/test/extensions/staked-expenditure.js @@ -27,7 +27,7 @@ const EtherRouter = artifacts.require("EtherRouter"); const StakedExpenditure = artifacts.require("StakedExpenditure"); const IReputationMiningCycle = artifacts.require("IReputationMiningCycle"); -const EXPENDITURE_UTILS = soliditySha3("StakedExpenditure"); +const STAKED_EXPENDITURE = soliditySha3("StakedExpenditure"); contract("StakedExpenditure", (accounts) => { let colonyNetwork; @@ -65,9 +65,9 @@ contract("StakedExpenditure", (accounts) => { beforeEach(async () => { ({ colony, token } = await setupRandomColony(colonyNetwork)); - await colony.installExtension(EXPENDITURE_UTILS, version); + await colony.installExtension(STAKED_EXPENDITURE, version); - const stakedExpenditureAddress = await colonyNetwork.getExtensionInstallation(EXPENDITURE_UTILS, colony.address); + const stakedExpenditureAddress = await colonyNetwork.getExtensionInstallation(STAKED_EXPENDITURE, colony.address); stakedExpenditure = await StakedExpenditure.at(stakedExpenditureAddress); await colony.setArbitrationRole(1, UINT256_MAX, stakedExpenditure.address, 1, true); @@ -115,7 +115,7 @@ contract("StakedExpenditure", (accounts) => { await checkErrorRevert(stakedExpenditure.install(colony.address), "extension-already-installed"); const identifier = await stakedExpenditure.identifier(); - expect(identifier).to.equal(EXPENDITURE_UTILS); + expect(identifier).to.equal(STAKED_EXPENDITURE); const capabilityRoles = await stakedExpenditure.getCapabilityRoles("0x0"); expect(capabilityRoles).to.equal(ethers.constants.HashZero); @@ -137,12 +137,12 @@ contract("StakedExpenditure", (accounts) => { it("can install the extension with the extension manager", async () => { ({ colony } = await setupRandomColony(colonyNetwork)); - await colony.installExtension(EXPENDITURE_UTILS, version, { from: USER0 }); + await colony.installExtension(STAKED_EXPENDITURE, version, { from: USER0 }); - await checkErrorRevert(colony.installExtension(EXPENDITURE_UTILS, version, { from: USER0 }), "colony-network-extension-already-installed"); - await checkErrorRevert(colony.uninstallExtension(EXPENDITURE_UTILS, { from: USER1 }), "ds-auth-unauthorized"); + 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(EXPENDITURE_UTILS, { from: USER0 }); + await colony.uninstallExtension(STAKED_EXPENDITURE, { from: USER0 }); }); }); @@ -229,6 +229,15 @@ contract("StakedExpenditure", (accounts) => { ); }); + 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(); From 98b7979115606e79a96084ad770141f3eb4295b6 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Wed, 6 Jul 2022 14:44:55 -0700 Subject: [PATCH 40/59] Update yarn.lock --- test/contracts-network/colony-expenditure.js | 1 - yarn.lock | 70 +++++++++++++++++++- 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/test/contracts-network/colony-expenditure.js b/test/contracts-network/colony-expenditure.js index ffaf947e49..fad8df22bf 100644 --- a/test/contracts-network/colony-expenditure.js +++ b/test/contracts-network/colony-expenditure.js @@ -512,7 +512,6 @@ contract("Colony Expenditure", (accounts) => { let slot; slot = await colony.getExpenditureSlot(expenditureId, SLOT0); - console.log(slot); expect(slot.recipient).to.equal(ADDRESS_ZERO); expect(slot.skills[0]).to.be.zero; expect(slot.claimDelay).to.be.zero; diff --git a/yarn.lock b/yarn.lock index 8deaa55af2..3c1f40e89a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -241,6 +241,14 @@ core-js "^2.6.5" regenerator-runtime "^0.13.4" +"@babel/polyfill@^7.0.0": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/polyfill/-/polyfill-7.12.1.tgz#1f2d6371d1261bbd961f3c5d5909150e12d0bd96" + integrity sha512-X0pi0V6gxLi6lFZpGmeNa4zxtwEmCs42isWLNjZZDE0Y8yVfgu0T2OAHlzBbdYlqbW/YXVvoBHpATEM+goCj8g== + dependencies: + core-js "^2.6.5" + regenerator-runtime "^0.13.4" + "@babel/runtime@^7.6.3", "@babel/runtime@^7.9.2": version "7.18.6" resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.6.tgz" @@ -1549,6 +1557,56 @@ ethereumjs-tx "^1.3.7" strip-hex-prefix "^1.0.0" +"@umaprotocol/truffle-ledger-provider@^1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@umaprotocol/truffle-ledger-provider/-/truffle-ledger-provider-1.0.5.tgz#e30025c4ecc2f2540825c46788ef1291474080be" + integrity sha512-RPkhftL0GIrkX6QB7IsvsUx8dl6NUXs4kv2U1TtnLXkq6EsfEhmZeLm8jrtBKcY05Uuq4BqyfxcM5o0C3Oljlw== + optionalDependencies: + "@babel/polyfill" "^7.0.0" + "@ledgerhq/hw-transport-node-hid" "^4.32.0" + "@umaprotocol/web3-provider-engine" "^15.0.4" + "@umaprotocol/web3-subprovider" "^4.74.3" + ethereumjs-wallet "^0.6.0" + +"@umaprotocol/web3-provider-engine@^15.0.4", "@umaprotocol/web3-provider-engine@^15.0.5": + version "15.0.5" + resolved "https://registry.yarnpkg.com/@umaprotocol/web3-provider-engine/-/web3-provider-engine-15.0.5.tgz#7edd5e60edde1b0453174ee20d336c3b5eabe0f6" + integrity sha512-bygswdrRMZ3z+fSMi9aOx5T/Mm4geIopcj3NLQjd56Ekek/PlXw8L5OLlwb0VLePyknFn4jwT4hgZOg/Tu7uWw== + dependencies: + async "^2.5.0" + backoff "^2.5.0" + clone "^2.0.0" + cross-fetch "^2.1.0" + eth-block-tracker "^3.0.0" + eth-json-rpc-infura "^3.1.0" + eth-sig-util "^1.4.2" + ethereumjs-block "^1.2.2" + ethereumjs-tx "^1.2.0" + ethereumjs-util "^5.1.5" + ethereumjs-vm "^2.3.4" + json-rpc-error "^2.0.0" + json-stable-stringify "^1.0.1" + promise-to-callback "^1.0.0" + readable-stream "^2.2.9" + request "^2.85.0" + semaphore "^1.0.3" + tape "^4.4.0" + ws "^5.1.1" + xhr "^2.2.0" + xtend "^4.0.1" + +"@umaprotocol/web3-subprovider@^4.74.3": + version "4.74.5" + resolved "https://registry.yarnpkg.com/@umaprotocol/web3-subprovider/-/web3-subprovider-4.74.5.tgz#b755fe4df015bfae1e38b5fcada91638294e1347" + integrity sha512-ISZQtpPRqrMTaSYLwKpMsbRdrQI3JPo0hp68BW7snKLFkzEYGNX0NgSAtBwA/RgUlnD1oL0PBZ+wEvkVNdXM0Q== + dependencies: + "@ledgerhq/hw-app-eth" "^4.74.2" + "@ledgerhq/hw-transport" "^4.74.2" + "@ledgerhq/hw-transport-u2f" "^4.74.2" + "@umaprotocol/web3-provider-engine" "^15.0.5" + ethereumjs-tx "^1.3.7" + strip-hex-prefix "^1.0.0" + "@ungap/promise-all-settled@1.1.2": version "1.1.2" resolved "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz" @@ -10758,7 +10816,17 @@ 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@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0: + version "5.7.1" + resolved "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + +semver@^6.0.0, semver@^6.3.0: + version "6.3.0" + resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +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== From edbc3386202be866b8fb49f96c62fc921962474f Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Tue, 5 Jul 2022 13:52:27 -0700 Subject: [PATCH 41/59] Allow for setting multiple payouts in a single expenditure slot --- contracts/colony/ColonyAuthority.sol | 1 + contracts/colony/ColonyFunding.sol | 22 ++++++++++++++++- contracts/colony/IColony.sol | 17 +++++++++++++ contracts/extensions/VotingReputation.sol | 22 ++++++++++------- test/contracts-network/colony-expenditure.js | 25 ++++++++++++++++++++ 5 files changed, 77 insertions(+), 10 deletions(-) diff --git a/contracts/colony/ColonyAuthority.sol b/contracts/colony/ColonyAuthority.sol index 2bdc29f8e2..42a06f400d 100644 --- a/contracts/colony/ColonyAuthority.sol +++ b/contracts/colony/ColonyAuthority.sol @@ -127,6 +127,7 @@ contract ColonyAuthority is CommonAuthority { // Added in colony v10 (ginger-lwss) addRoleCapability(ARBITRATION_ROLE, "setExpenditurePayouts(uint256,uint256,uint256,uint256[],address,uint256[])"); + addRoleCapability(ARBITRATION_ROLE, "setExpenditureSlotPayouts(uint256,uint256,uint256,uint256,address[],uint256[])"); addRoleCapability(FUNDING_ROLE, "moveFundsBetweenPots(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256[],address[])"); } diff --git a/contracts/colony/ColonyFunding.sol b/contracts/colony/ColonyFunding.sol index 9aee6c0ebb..22f9a05f2f 100755 --- a/contracts/colony/ColonyFunding.sol +++ b/contracts/colony/ColonyFunding.sol @@ -207,11 +207,31 @@ contract ColonyFunding is ColonyStorage { // ignore-swc-123 ) public stoppable + validExpenditure(_id) authDomain(_permissionDomainId, _childSkillIndex, expenditures[_id].domainId) { setExpenditurePayoutsInternal(_id, _slots, _token, _amounts); } + function setExpenditureSlotPayouts( + uint256 _permissionDomainId, + uint256 _childSkillIndex, + uint256 _id, + uint256 _slot, + address[] memory _tokens, + uint256[] memory _amounts + ) + public + stoppable + validExpenditure(_id) + authDomain(_permissionDomainId, _childSkillIndex, expenditures[_id].domainId) + { + require(_tokens.length == _amounts.length, "colony-funding-mismatched-arguments"); + for (uint256 i; i < _tokens.length; i++) { + setExpenditurePayout(_id, _slot, _tokens[i], _amounts[i]); + } + } + function setExpenditurePayout(uint256 _id, uint256 _slot, address _token, uint256 _amount) public stoppable @@ -220,7 +240,7 @@ contract ColonyFunding is ColonyStorage { // ignore-swc-123 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); diff --git a/contracts/colony/IColony.sol b/contracts/colony/IColony.sol index a3d595ad25..83a15ee0a2 100644 --- a/contracts/colony/IColony.sol +++ b/contracts/colony/IColony.sol @@ -453,6 +453,23 @@ interface IColony is ColonyDataTypes, IRecovery, IBasicMetaTransaction { /// @param _amounts 2D array of payout amounts function setExpenditurePayouts(uint256 _id, uint256[][] memory _slots, address[] memory _tokens, uint256[][] memory _amounts) external; + /// @notice Set the token payouts in given expenditure slot. Can only be called by expenditure owner. + /// @dev Can only be called while expenditure is in draft state. + /// @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 Slot to set payouts + /// @param _tokens Array of token addresses, `0x0` value indicates Ether + /// @param _amounts Array of payout amounts + function setExpenditureSlotPayouts( + uint256 _permissionDomainId, + uint256 _childSkillIndex, + uint256 _id, + uint256 _slot, + address[] memory _tokens, + uint256[] memory _amounts + ) external; + /// @notice Set the token payouts in given expenditure slots. 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` diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index 671d574a27..1d199291ce 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -56,14 +56,18 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs, BasicMetaTrans bytes32(uint256(1)) << uint8(ColonyDataTypes.ColonyRole.Root) ); - bytes4 constant CHANGE_FUNCTION_SIG_1 = bytes4(keccak256( + bytes4 constant SET_EXPENDITURE_STATE = bytes4(keccak256( "setExpenditureState(uint256,uint256,uint256,uint256,bool[],bytes32[],bytes32)" )); - bytes4 constant CHANGE_FUNCTION_SIG_2 = bytes4(keccak256( + bytes4 constant SET_EXPENDITURE_PAYOUTS = bytes4(keccak256( "setExpenditurePayouts(uint256,uint256,uint256,uint256[],address,uint256[])" )); + bytes4 constant SET_EXPENDITURE_SLOT_PAYOUTS = bytes4(keccak256( + "setExpenditureSlotPayouts(uint256,uint256,uint256,uint256,address[],uint256[])" + )); + bytes4 constant OLD_MOVE_FUNDS_SIG = bytes4(keccak256( "moveFundsBetweenPots(uint256,uint256,uint256,uint256,uint256,uint256,address)" )); @@ -398,7 +402,7 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs, BasicMetaTrans _vote == YAY && !motion.escalated && motion.stakes[YAY] == requiredStake && - (getSig(motion.action) == CHANGE_FUNCTION_SIG_1 || getSig(motion.action) == CHANGE_FUNCTION_SIG_2) && + (getSig(motion.action) == SET_EXPENDITURE_STATE || getSig(motion.action) == SET_EXPENDITURE_PAYOUTS) && motion.altTarget == address(0x0) ) { bytes32 structHash = hashExpenditureActionStruct(motion.action); @@ -594,7 +598,7 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs, BasicMetaTrans ); if ( - (getSig(motion.action) == CHANGE_FUNCTION_SIG_1 || getSig(motion.action) == CHANGE_FUNCTION_SIG_2) && + (getSig(motion.action) == SET_EXPENDITURE_STATE || getSig(motion.action) == SET_EXPENDITURE_PAYOUTS) && getTarget(motion.altTarget) == address(colony) ) { bytes32 structHash = hashExpenditureActionStruct(motion.action); @@ -1055,7 +1059,7 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs, BasicMetaTrans } function hashExpenditureActionStruct(bytes memory action) internal returns (bytes32 hash) { - assert(getSig(action) == CHANGE_FUNCTION_SIG_1 || getSig(action) == CHANGE_FUNCTION_SIG_2); + assert(getSig(action) == SET_EXPENDITURE_STATE || getSig(action) == SET_EXPENDITURE_PAYOUTS); uint256 expenditureId; uint256 storageSlot; @@ -1086,7 +1090,7 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs, BasicMetaTrans // of 0x20 represents advancing one byte, 4 is the function signature. // So: 0x[length][sig][args...] - bytes4 functionSignature = CHANGE_FUNCTION_SIG_1; + bytes4 functionSignature = SET_EXPENDITURE_STATE; uint256 permissionDomainId; uint256 childSkillIndex; @@ -1100,8 +1104,8 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs, BasicMetaTrans storageSlot := mload(add(action, 0x84)) } - // If we are editing the main expenditure struct - if (storageSlot == 25 || getSig(action) == CHANGE_FUNCTION_SIG_2) { + // If we are editing the main expenditure struct, or setting payouts on multiple slots + if (storageSlot == 25 || getSig(action) == SET_EXPENDITURE_PAYOUTS) { bytes memory mainClaimDelayAction = new bytes(4 + 32 * 11); // 356 bytes assembly { @@ -1120,7 +1124,7 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs, BasicMetaTrans } return mainClaimDelayAction; - // If we are editing an expenditure slot + // If we are editing an expenditure slot or setting payouts for one slot } else { bytes memory slotClaimDelayAction = new bytes(4 + 32 * 13); // 420 bytes diff --git a/test/contracts-network/colony-expenditure.js b/test/contracts-network/colony-expenditure.js index fad8df22bf..7f7670bd57 100644 --- a/test/contracts-network/colony-expenditure.js +++ b/test/contracts-network/colony-expenditure.js @@ -373,6 +373,31 @@ contract("Colony Expenditure", (accounts) => { expect(payout).to.eq.BN(20); }); + it("should allow owners to update payouts at once in one slot", async () => { + await colony.setExpenditureSlotPayouts( + 1, + UINT256_MAX, + expenditureId, + SLOT0, + [token.address, otherToken.address], + [10, 20], + { from: ARBITRATOR } + ); + + let payout; + payout = await colony.getExpenditureSlotPayout(expenditureId, SLOT0, token.address); + expect(payout).to.eq.BN(10); + payout = await colony.getExpenditureSlotPayout(expenditureId, SLOT0, otherToken.address); + expect(payout).to.eq.BN(20); + }); + + it("should not allow owners to update payouts at once in one slot with mismatched arguments", async () => { + await checkErrorRevert( + colony.setExpenditureSlotPayouts(1, UINT256_MAX, expenditureId, SLOT0, [token.address, otherToken.address], [10], { from: ARBITRATOR }), + "colony-funding-mismatched-arguments" + ); + }); + it("should not allow owners to update many slot payouts with mismatched arguments", async () => { const setExpenditurePayouts = colony.methods["setExpenditurePayouts(uint256,uint256[],address,uint256[])"]; From e5c290818740ded7dc73394e4b982a830a031f8a Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Thu, 7 Jul 2022 18:10:47 -0700 Subject: [PATCH 42/59] Respond to review comments IV --- contracts/colony/Colony.sol | 4 ++ contracts/colony/IColony.sol | 3 +- contracts/extensions/VotingReputation.sol | 38 +++++++++---- test/extensions/voting-rep.js | 65 ++++++++++++++++++++++- 4 files changed, 96 insertions(+), 14 deletions(-) diff --git a/contracts/colony/Colony.sol b/contracts/colony/Colony.sol index 72f22f3ebb..534f16e1c0 100755 --- a/contracts/colony/Colony.sol +++ b/contracts/colony/Colony.sol @@ -315,6 +315,10 @@ contract Colony is BasicMetaTransaction, ColonyStorage, PatriciaTreeProofs { sig = bytes4(keccak256("setExpenditurePayouts(uint256,uint256,uint256,uint256[],address,uint256[])")); colonyAuthority.setRoleCapability(uint8(ColonyRole.Arbitration), address(this), sig, true); + sig = bytes4(keccak256("setExpenditureSlotPayouts(uint256,uint256,uint256,uint256,address[],uint256[])")); + colonyAuthority.setRoleCapability(uint8(ColonyRole.Arbitration), address(this), sig, true); + + sig = bytes4(keccak256("moveFundsBetweenPots(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256[],address[])")); colonyAuthority.setRoleCapability(uint8(ColonyRole.Funding), address(this), sig, true); } diff --git a/contracts/colony/IColony.sol b/contracts/colony/IColony.sol index 83a15ee0a2..5c7d8bed04 100644 --- a/contracts/colony/IColony.sol +++ b/contracts/colony/IColony.sol @@ -453,8 +453,7 @@ interface IColony is ColonyDataTypes, IRecovery, IBasicMetaTransaction { /// @param _amounts 2D array of payout amounts function setExpenditurePayouts(uint256 _id, uint256[][] memory _slots, address[] memory _tokens, uint256[][] memory _amounts) external; - /// @notice Set the token payouts in given expenditure slot. Can only be called by expenditure owner. - /// @dev Can only be called while expenditure is in draft state. + /// @notice Set the token payouts in 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 diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index 1d199291ce..1ce8860f36 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -401,9 +401,11 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs, BasicMetaTrans if ( _vote == YAY && !motion.escalated && - motion.stakes[YAY] == requiredStake && - (getSig(motion.action) == SET_EXPENDITURE_STATE || getSig(motion.action) == SET_EXPENDITURE_PAYOUTS) && - motion.altTarget == address(0x0) + motion.stakes[YAY] == requiredStake && ( + getSig(motion.action) == SET_EXPENDITURE_STATE || + getSig(motion.action) == SET_EXPENDITURE_PAYOUTS || + getSig(motion.action) == SET_EXPENDITURE_SLOT_PAYOUTS + ) && motion.altTarget == address(0x0) ) { bytes32 structHash = hashExpenditureActionStruct(motion.action); expenditureMotionCounts[structHash] = add(expenditureMotionCounts[structHash], 1); @@ -597,9 +599,11 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs, BasicMetaTrans motion.votes[NAY] < motion.votes[YAY] ); - if ( - (getSig(motion.action) == SET_EXPENDITURE_STATE || getSig(motion.action) == SET_EXPENDITURE_PAYOUTS) && - getTarget(motion.altTarget) == address(colony) + if (( + getSig(motion.action) == SET_EXPENDITURE_STATE || + getSig(motion.action) == SET_EXPENDITURE_PAYOUTS || + getSig(motion.action) == SET_EXPENDITURE_SLOT_PAYOUTS + ) && getTarget(motion.altTarget) == address(colony) ) { bytes32 structHash = hashExpenditureActionStruct(motion.action); expenditureMotionCounts[structHash] = sub(expenditureMotionCounts[structHash], 1); @@ -1059,7 +1063,11 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs, BasicMetaTrans } function hashExpenditureActionStruct(bytes memory action) internal returns (bytes32 hash) { - assert(getSig(action) == SET_EXPENDITURE_STATE || getSig(action) == SET_EXPENDITURE_PAYOUTS); + assert( + getSig(action) == SET_EXPENDITURE_STATE || + getSig(action) == SET_EXPENDITURE_PAYOUTS || + getSig(action) == SET_EXPENDITURE_SLOT_PAYOUTS + ); uint256 expenditureId; uint256 storageSlot; @@ -1105,7 +1113,10 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs, BasicMetaTrans } // If we are editing the main expenditure struct, or setting payouts on multiple slots - if (storageSlot == 25 || getSig(action) == SET_EXPENDITURE_PAYOUTS) { + if ( + (getSig(action) == SET_EXPENDITURE_STATE && storageSlot == 25) || + (getSig(action) == SET_EXPENDITURE_PAYOUTS) + ) { bytes memory mainClaimDelayAction = new bytes(4 + 32 * 11); // 356 bytes assembly { @@ -1126,13 +1137,20 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs, BasicMetaTrans // If we are editing an expenditure slot or setting payouts for one slot } else { - bytes memory slotClaimDelayAction = new bytes(4 + 32 * 13); // 420 bytes uint256 expenditureSlot; - assembly { + if (getSig(action) == SET_EXPENDITURE_SLOT_PAYOUTS) { + assembly { + expenditureSlot := mload(add(action, 0x84)) + } + } else { + assembly { expenditureSlot := mload(add(action, 0x184)) + } + } + assembly { mstore(add(slotClaimDelayAction, 0x20), functionSignature) mstore(add(slotClaimDelayAction, 0x24), permissionDomainId) mstore(add(slotClaimDelayAction, 0x44), childSkillIndex) diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index e79e30c0a3..c30959e2b2 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -683,10 +683,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,6 +700,15 @@ 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 globalClaimDelay if voting on expenditure payout states", async () => { @@ -713,10 +723,11 @@ contract("Voting Reputation", (accounts) => { motionId = await voting.getMotionCount(); let expenditureMotionCount; + let expenditure; + expenditureMotionCount = await voting.getExpenditureMotionCount(soliditySha3(expenditureId, 0)); expect(expenditureMotionCount).to.be.zero; - let expenditure; expenditure = await colony.getExpenditure(expenditureId); expect(expenditure.globalClaimDelay).to.be.zero; @@ -729,6 +740,56 @@ contract("Voting Reputation", (accounts) => { expect(expenditure.globalClaimDelay).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; + + expenditure = await colony.getExpenditure(expenditureId); + expect(expenditure.globalClaimDelay).to.be.zero; + }); + + it("can update the expenditure slot claimDelay if voting on expenditure slot 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, "setExpenditureSlotPayouts", [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; + + expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); + expect(expenditureSlot.claimDelay).to.be.zero; + + await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); + + expenditureMotionCount = await voting.getExpenditureMotionCount(soliditySha3(expenditureId, 0)); + expect(expenditureMotionCount).to.eq.BN(1); + + expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); + expect(expenditureSlot.claimDelay).to.eq.BN(UINT256_MAX.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 states", async () => { From d53ecb0eff50bdb7cf531edd864be01e1b78a667 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Thu, 7 Jul 2022 18:16:13 -0700 Subject: [PATCH 43/59] Update smoke tests --- test-smoke/colony-storage-consistent.js | 10 +++++----- test/contracts-network/colony-expenditure.js | 12 +++--------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/test-smoke/colony-storage-consistent.js b/test-smoke/colony-storage-consistent.js index 3ddf95d0b8..b9fc8c1490 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("0xbbd8e4df4e66a2bf51a32f33a60b619eb06c8cc8ea6c4c7e15b3a0c835930352"); - expect(colonyStateHash).to.equal("0x82cdec79eb889c8860763d07f4e7c5ad4cde875c966b1e904d75e951b128315f"); - expect(metaColonyStateHash).to.equal("0x97b3fe37d2143132be6760eda0dcb4cff66a1fc9f31f29377d20664c0b7406ed"); - expect(miningCycleStateHash).to.equal("0x63723c9783b0f0c20e8640fc383ed40dbc228a93cc73a18fa6c6e5f0c38e8694"); - expect(tokenLockingStateHash).to.equal("0xa4dd9734ad2f6b18ec82a5d10be671a5082d6400173d47ac1401580f57cd1638"); + expect(colonyNetworkStateHash).to.equal("0x29097499f8e5f6c3203ab16f919abace364d7baf0c5686346ffd22133cd4db6a"); + expect(colonyStateHash).to.equal("0x46e36a62ba5cc6c071e5f7ab3e76921f5211f32f8125fd02fed52e078f02364d"); + expect(metaColonyStateHash).to.equal("0x86fc4659e140aa1fc3edcd30ebd438198dc787f6abc4be7c730bb9741d1ad049"); + expect(miningCycleStateHash).to.equal("0x632d459a2197708bd2dbde87e8275c47dddcdf16d59e3efd21dcef9acb2a7366"); + expect(tokenLockingStateHash).to.equal("0x30fbcbfbe589329fe20288101faabe1f60a4610ae0c0effb15526c6b390a8e07"); }); }); }); diff --git a/test/contracts-network/colony-expenditure.js b/test/contracts-network/colony-expenditure.js index 7f7670bd57..6869175a3a 100644 --- a/test/contracts-network/colony-expenditure.js +++ b/test/contracts-network/colony-expenditure.js @@ -374,15 +374,9 @@ contract("Colony Expenditure", (accounts) => { }); it("should allow owners to update payouts at once in one slot", async () => { - await colony.setExpenditureSlotPayouts( - 1, - UINT256_MAX, - expenditureId, - SLOT0, - [token.address, otherToken.address], - [10, 20], - { from: ARBITRATOR } - ); + await colony.setExpenditureSlotPayouts(1, UINT256_MAX, expenditureId, SLOT0, [token.address, otherToken.address], [10, 20], { + from: ARBITRATOR, + }); let payout; payout = await colony.getExpenditureSlotPayout(expenditureId, SLOT0, token.address); From f03b5b65596f37bb98c7ab4d047343e16eb7b088 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Wed, 13 Jul 2022 08:44:52 -0700 Subject: [PATCH 44/59] Respond to review comments V --- contracts/colony/ColonyExpenditure.sol | 20 ++++----- contracts/colony/ColonyFunding.sol | 8 ++-- contracts/colony/ColonyStorage.sol | 11 +++-- contracts/colony/IColony.sol | 2 +- contracts/extensions/FundingQueue.sol | 6 +-- contracts/extensions/StakedExpenditure.sol | 29 ++++++++++++ docs/_Interface_IColony.md | 6 +-- test/contracts-network/colony-expenditure.js | 39 +++++++++++------ test/contracts-network/colony-funding.js | 46 ++++++++++++++++++++ test/contracts-network/colony-permissions.js | 8 ++-- test/contracts-network/colony-recovery.js | 14 +++++- test/extensions/funding-queue.js | 6 +-- 12 files changed, 149 insertions(+), 46 deletions(-) diff --git a/contracts/colony/ColonyExpenditure.sol b/contracts/colony/ColonyExpenditure.sol index 9be932fc15..e017044125 100644 --- a/contracts/colony/ColonyExpenditure.sol +++ b/contracts/colony/ColonyExpenditure.sol @@ -69,7 +69,7 @@ contract ColonyExpenditure is ColonyStorage { public stoppable expenditureDraftOrLocked(_id) - expenditureOwnerOrSelf(_id) + expenditureOwner(_id) { expenditures[_id].owner = _newOwner; @@ -97,7 +97,7 @@ contract ColonyExpenditure is ColonyStorage { public stoppable expenditureDraft(_id) - expenditureOwnerOrSelf(_id) + expenditureOwner(_id) { expenditures[_id].status = ExpenditureStatus.Cancelled; @@ -108,7 +108,7 @@ contract ColonyExpenditure is ColonyStorage { public stoppable expenditureDraft(_id) - expenditureOwnerOrSelf(_id) + expenditureOwner(_id) { expenditures[_id].status = ExpenditureStatus.Locked; @@ -119,7 +119,7 @@ contract ColonyExpenditure is ColonyStorage { public stoppable expenditureDraftOrLocked(_id) - expenditureOwnerOrSelf(_id) + expenditureOwner(_id) { FundingPot storage fundingPot = fundingPots[expenditures[_id].fundingPotId]; require(fundingPot.payoutsWeCannotMake == 0, "colony-expenditure-not-funded"); @@ -134,7 +134,7 @@ contract ColonyExpenditure is ColonyStorage { public stoppable expenditureDraft(_id) - expenditureOwnerOrSelf(_id) + expenditureOwner(_id) { emit ExpenditureMetadataSet(msgSender(), _id, _metadata); } @@ -157,7 +157,7 @@ contract ColonyExpenditure is ColonyStorage { public stoppable expenditureDraft(_id) - expenditureOwnerOrSelf(_id) + expenditureOwner(_id) { require(_slots.length == _recipients.length, "colony-expenditure-bad-slots"); @@ -172,7 +172,7 @@ contract ColonyExpenditure is ColonyStorage { public stoppable expenditureDraft(_id) - expenditureOwnerOrSelf(_id) + expenditureOwner(_id) { require(_slots.length == _skillIds.length, "colony-expenditure-bad-slots"); IColonyNetwork colonyNetworkContract = IColonyNetwork(colonyNetworkAddress); @@ -194,7 +194,7 @@ contract ColonyExpenditure is ColonyStorage { public stoppable expenditureDraft(_id) - expenditureOwnerOrSelf(_id) + expenditureOwner(_id) { require(_slots.length == _claimDelays.length, "colony-expenditure-bad-slots"); @@ -209,7 +209,7 @@ contract ColonyExpenditure is ColonyStorage { public stoppable expenditureDraft(_id) - expenditureOwnerOrSelf(_id) + expenditureOwner(_id) { require(_slots.length == _payoutModifiers.length, "colony-expenditure-bad-slots"); @@ -237,7 +237,7 @@ contract ColonyExpenditure is ColonyStorage { public stoppable expenditureDraft(_id) - expenditureOwnerOrSelf(_id) + expenditureOwner(_id) { if (_recipients.length > 0) { setExpenditureRecipients(_id, _recipientSlots, _recipients); } if (_skillIds.length > 0) { setExpenditureSkills(_id, _skillIdSlots, _skillIds); } diff --git a/contracts/colony/ColonyFunding.sol b/contracts/colony/ColonyFunding.sol index 22f9a05f2f..58e3f1bf30 100755 --- a/contracts/colony/ColonyFunding.sol +++ b/contracts/colony/ColonyFunding.sol @@ -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"); require(_amounts.length == _tokens.length, "colony-invalid-arguments"); for (uint256 i; i < _amounts.length; i++) { @@ -70,8 +70,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); } diff --git a/contracts/colony/ColonyStorage.sol b/contracts/colony/ColonyStorage.sol index 931fd6f944..bcd61f1e3b 100755 --- a/contracts/colony/ColonyStorage.sol +++ b/contracts/colony/ColonyStorage.sol @@ -197,7 +197,12 @@ contract ColonyStorage is ColonyDataTypes, ColonyNetworkDataTypes, DSMath, Commo } modifier expenditureOwnerOrSelf(uint256 _id) { - require(expenditures[_id].owner == msgSender() || address(this) == msgSender(), "colony-expenditure-not-self-or-owner"); + require(expenditures[_id].owner == msgSender() || address(this) == msgSender(), "colony-expenditure-not-owner-or-self"); + _; + } + + modifier expenditureOwner(uint256 _id) { + require(expenditures[_id].owner == msgSender(), "colony-expenditure-not-owner"); _; } @@ -233,7 +238,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"); _; } @@ -257,7 +262,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"); _; } diff --git a/contracts/colony/IColony.sol b/contracts/colony/IColony.sol index 5c7d8bed04..e58167456a 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 diff --git a/contracts/extensions/FundingQueue.sol b/contracts/extensions/FundingQueue.sol index f70d05644f..4ddabfb13a 100644 --- a/contracts/extensions/FundingQueue.sol +++ b/contracts/extensions/FundingQueue.sol @@ -90,7 +90,7 @@ contract FundingQueue is ColonyExtension, PatriciaTreeProofs, BasicMetaTransacti /// @notice Returns the version of the extension function version() public override pure returns (uint256) { - return 3; + return 4; } /// @notice Configures the extension @@ -141,12 +141,12 @@ contract FundingQueue is ColonyExtension, PatriciaTreeProofs, BasicMetaTransacti require( (domainSkillId == fromSkillId && _fromChildSkillIndex == UINT256_MAX) || fromSkillId == colonyNetwork.getChildSkillId(domainSkillId, _fromChildSkillIndex), - "funding-queue-bad-inheritence-from" + "funding-queue-bad-inheritance-from" ); require( (domainSkillId == toSkillId && _toChildSkillIndex == UINT256_MAX) || toSkillId == colonyNetwork.getChildSkillId(domainSkillId, _toChildSkillIndex), - "funding-queue-bad-inheritence-to" + "funding-queue-bad-inheritance-to" ); proposalCount++; diff --git a/contracts/extensions/StakedExpenditure.sol b/contracts/extensions/StakedExpenditure.sol index 2145a51242..d2df0f84fa 100644 --- a/contracts/extensions/StakedExpenditure.sol +++ b/contracts/extensions/StakedExpenditure.sol @@ -90,6 +90,8 @@ contract StakedExpenditure is ColonyExtensionMeta, PatriciaTreeProofs { // 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; @@ -97,6 +99,14 @@ contract StakedExpenditure is ColonyExtensionMeta, PatriciaTreeProofs { 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, @@ -121,6 +131,8 @@ contract StakedExpenditure is ColonyExtensionMeta, PatriciaTreeProofs { 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"); @@ -141,6 +153,11 @@ contract StakedExpenditure is ColonyExtensionMeta, PatriciaTreeProofs { 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, @@ -164,6 +181,14 @@ contract StakedExpenditure is ColonyExtensionMeta, PatriciaTreeProofs { 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, @@ -220,10 +245,14 @@ contract StakedExpenditure is ColonyExtensionMeta, PatriciaTreeProofs { // 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]; } 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/test/contracts-network/colony-expenditure.js b/test/contracts-network/colony-expenditure.js index 6869175a3a..55f4e36478 100644 --- a/test/contracts-network/colony-expenditure.js +++ b/test/contracts-network/colony-expenditure.js @@ -111,7 +111,7 @@ contract("Colony Expenditure", (accounts) => { let expenditure = await colony.getExpenditure(expenditureId); expect(expenditure.status).to.eq.BN(DRAFT); - await checkErrorRevert(colony.cancelExpenditure(expenditureId, { from: USER }), "colony-expenditure-not-self-or-owner"); + await checkErrorRevert(colony.cancelExpenditure(expenditureId, { from: USER }), "colony-expenditure-not-owner"); await colony.cancelExpenditure(expenditureId, { from: ADMIN }); expenditure = await colony.getExpenditure(expenditureId); @@ -126,7 +126,7 @@ contract("Colony Expenditure", (accounts) => { let expenditure = await colony.getExpenditure(expenditureId); expect(expenditure.owner).to.equal(ADMIN); - await checkErrorRevert(colony.transferExpenditure(expenditureId, USER), "colony-expenditure-not-self-or-owner"); + await checkErrorRevert(colony.transferExpenditure(expenditureId, USER), "colony-expenditure-not-owner"); await colony.transferExpenditure(expenditureId, USER, { from: ADMIN }); expenditure = await colony.getExpenditure(expenditureId); @@ -205,7 +205,7 @@ contract("Colony Expenditure", (accounts) => { await expectEvent(tx, "ExpenditureMetadataSet", [ADMIN, expenditureId, IPFS_HASH]); - await checkErrorRevert(setExpenditureMetadata(expenditureId, IPFS_HASH, { from: USER }), "colony-expenditure-not-self-or-owner"); + await checkErrorRevert(setExpenditureMetadata(expenditureId, IPFS_HASH, { from: USER }), "colony-expenditure-not-owner"); }); it("should allow arbitrators to update the metadata", async () => { @@ -232,7 +232,7 @@ contract("Colony Expenditure", (accounts) => { it("should allow only owners to update many slot recipients at once", async () => { await checkErrorRevert( colony.setExpenditureRecipients(expenditureId, [SLOT1, SLOT2], [RECIPIENT, USER], { from: USER }), - "colony-expenditure-not-self-or-owner" + "colony-expenditure-not-owner" ); await colony.setExpenditureRecipients(expenditureId, [SLOT1, SLOT2], [RECIPIENT, USER], { from: ADMIN }); @@ -311,10 +311,7 @@ contract("Colony Expenditure", (accounts) => { }); it("should allow only owners to update a slot claim delay", async () => { - await checkErrorRevert( - colony.setExpenditureClaimDelay(expenditureId, SLOT0, SECONDS_PER_DAY, { from: USER }), - "colony-expenditure-not-self-or-owner" - ); + await checkErrorRevert(colony.setExpenditureClaimDelay(expenditureId, SLOT0, SECONDS_PER_DAY, { from: USER }), "colony-expenditure-not-owner"); await colony.setExpenditureClaimDelay(expenditureId, SLOT0, SECONDS_PER_DAY, { from: ADMIN }); @@ -340,7 +337,7 @@ contract("Colony Expenditure", (accounts) => { it("should allow only owners to update many slot payout modifiers at once", async () => { await checkErrorRevert( colony.setExpenditurePayoutModifiers(expenditureId, [SLOT1, SLOT2], [WAD.divn(2), WAD], { from: USER }), - "colony-expenditure-not-self-or-owner" + "colony-expenditure-not-owner" ); await colony.setExpenditurePayoutModifiers(expenditureId, [SLOT1, SLOT2], [WAD.divn(2), WAD], { from: ADMIN }); @@ -412,8 +409,8 @@ contract("Colony Expenditure", (accounts) => { }); it("should not allow non-owners to update skills or payouts", async () => { - await checkErrorRevert(colony.setExpenditureSkill(expenditureId, SLOT0, GLOBAL_SKILL_ID), "colony-expenditure-not-self-or-owner"); - await checkErrorRevert(colony.setExpenditurePayout(expenditureId, SLOT0, token.address, WAD), "colony-expenditure-not-self-or-owner"); + await checkErrorRevert(colony.setExpenditureSkill(expenditureId, SLOT0, GLOBAL_SKILL_ID), "colony-expenditure-not-owner"); + await checkErrorRevert(colony.setExpenditurePayout(expenditureId, SLOT0, token.address, WAD), "colony-expenditure-not-owner"); }); it("should allow owners to add a slot payout", async () => { @@ -526,6 +523,22 @@ contract("Colony Expenditure", (accounts) => { expect(payout).to.eq.BN(WAD.muln(40)); }); + 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, [], [], [], [], [], [], [], [], [], [[], []], [[], []], { from: ADMIN }); @@ -582,7 +595,7 @@ contract("Colony Expenditure", (accounts) => { let expenditure = await colony.getExpenditure(expenditureId); expect(expenditure.status).to.eq.BN(DRAFT); - await checkErrorRevert(colony.lockExpenditure(expenditureId, { from: USER }), "colony-expenditure-not-self-or-owner"); + await checkErrorRevert(colony.lockExpenditure(expenditureId, { from: USER }), "colony-expenditure-not-owner"); await colony.lockExpenditure(expenditureId, { from: ADMIN }); expenditure = await colony.getExpenditure(expenditureId); @@ -635,7 +648,7 @@ contract("Colony Expenditure", (accounts) => { let expenditure = await colony.getExpenditure(expenditureId); expect(expenditure.status).to.eq.BN(DRAFT); - await checkErrorRevert(colony.finalizeExpenditure(expenditureId, { from: USER }), "colony-expenditure-not-self-or-owner"); + await checkErrorRevert(colony.finalizeExpenditure(expenditureId, { from: USER }), "colony-expenditure-not-owner"); const tx = await colony.finalizeExpenditure(expenditureId, { from: ADMIN }); const currTime = await getBlockTime(tx.receipt.blockNumber); diff --git a/test/contracts-network/colony-funding.js b/test/contracts-network/colony-funding.js index 333a3821f3..d2f163a5a3 100755 --- a/test/contracts-network/colony-funding.js +++ b/test/contracts-network/colony-funding.js @@ -123,6 +123,52 @@ contract("Colony Funding", (accounts) => { expect(pot2OtherTokenBalance).to.eq.BN(100); }); + it("should not be able to move multiple tokens from a deprecated domain", async () => { + // Create domain 2 and deprecate it + await colony.addDomain(1, UINT256_MAX, 1); + await colony.deprecateDomain(1, 0, 2, true); + + const sig = "moveFundsBetweenPots(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256[],address[])"; + const moveFundsBetweenPots = colony.methods[sig]; + + await checkErrorRevert(moveFundsBetweenPots(1, UINT256_MAX, 1, UINT256_MAX, 0, 1, 2, [], []), "colony-domain-deprecated"); + }); + + it("should not be able to move multiple tokens with invalid funding pots", async () => { + const sig = "moveFundsBetweenPots(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256[],address[])"; + const moveFundsBetweenPots = colony.methods[sig]; + + await checkErrorRevert( + moveFundsBetweenPots(1, UINT256_MAX, 1, UINT256_MAX, 0, 1, 1, [], []), + "colony-funding-cannot-move-funds-between-the-same-pot" + ); + await checkErrorRevert( + moveFundsBetweenPots(1, UINT256_MAX, 1, UINT256_MAX, 0, 0, 1, [], []), + "colony-funding-cannot-move-funds-from-rewards-pot" + ); + }); + + it("should not be able to move multiple tokens with an invalid domain proof", async () => { + const sig = "moveFundsBetweenPots(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256[],address[])"; + const moveFundsBetweenPots = colony.methods[sig]; + + // Create domain 2 + await colony.addDomain(1, UINT256_MAX, 1); + + await checkErrorRevert(moveFundsBetweenPots(1, 0, 1, UINT256_MAX, 0, 1, 2, [], []), "ds-auth-invalid-domain-inheritance"); + await checkErrorRevert(moveFundsBetweenPots(1, UINT256_MAX, 1, 0, 0, 1, 2, [], []), "colony-invalid-domain-inheritance"); + await checkErrorRevert(moveFundsBetweenPots(1, UINT256_MAX, 1, UINT256_MAX, UINT256_MAX, 1, 2, [], []), "colony-invalid-domain-inheritance"); + }); + + it("should not be able to move multiple tokens with invalid arguments", async () => { + await colony.addDomain(1, UINT256_MAX, 1); + + const sig = "moveFundsBetweenPots(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256[],address[])"; + const moveFundsBetweenPots = colony.methods[sig]; + + await checkErrorRevert(moveFundsBetweenPots(1, UINT256_MAX, 1, UINT256_MAX, 0, 1, 2, [10], []), "colony-invalid-arguments"); + }); + it("when moving tokens between pots, should respect permission inheritance", async () => { await removeSubdomainLimit(colonyNetwork); // Temporary for tests until we allow subdomain depth > 1 await fundColonyWithTokens(colony, otherToken, 100); 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..ba5b768026 100644 --- a/test/contracts-network/colony-recovery.js +++ b/test/contracts-network/colony-recovery.js @@ -137,6 +137,14 @@ contract("Colony Recovery", (accounts) => { const metaColony = await IMetaColony.at(metaColonyAddress); await metaColony.enterRecoveryMode(); + let sig; + sig = "moveFundsBetweenPots(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256[],address[])"; + const moveFundsBetweenPots1 = metaColony.methods[sig]; + sig = "moveFundsBetweenPots(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,address)"; + const moveFundsBetweenPots2 = metaColony.methods[sig]; + sig = "moveFundsBetweenPots(uint256,uint256,uint256,uint256,uint256,uint256,address)"; + const moveFundsBetweenPots3 = metaColony.methods[sig]; + await checkErrorRevert(colony.initialiseColony(ethers.constants.AddressZero, ethers.constants.AddressZero), "colony-in-recovery-mode"); await checkErrorRevert(colony.mintTokens(1000), "colony-in-recovery-mode"); await checkErrorRevert(metaColony.addGlobalSkill(), "colony-in-recovery-mode"); @@ -189,6 +197,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"); @@ -219,8 +228,9 @@ contract("Colony Recovery", (accounts) => { await checkErrorRevert(metaColony.claimExpenditurePayout(0, 0, ADDRESS_ZERO), "colony-in-recovery-mode"); await checkErrorRevert(metaColony.claimPayment(0, ADDRESS_ZERO), "colony-in-recovery-mode"); await checkErrorRevert(metaColony.setPaymentPayout(0, 0, 0, ADDRESS_ZERO, 0), "colony-in-recovery-mode"); - await checkErrorRevert(metaColony.moveFundsBetweenPots(0, 0, 0, 0, 0, 0, 0, 0, ADDRESS_ZERO), "colony-in-recovery-mode"); - await checkErrorRevert(metaColony.moveFundsBetweenPots(0, 0, 0, 0, 0, 0, ADDRESS_ZERO), "colony-in-recovery-mode"); + await checkErrorRevert(moveFundsBetweenPots1(0, 0, 0, 0, 0, 0, 0, [], []), "colony-in-recovery-mode"); + await checkErrorRevert(moveFundsBetweenPots2(0, 0, 0, 0, 0, 0, 0, 0, ADDRESS_ZERO), "colony-in-recovery-mode"); + await checkErrorRevert(moveFundsBetweenPots3(0, 0, 0, 0, 0, 0, ADDRESS_ZERO), "colony-in-recovery-mode"); await checkErrorRevert(metaColony.claimColonyFunds(ADDRESS_ZERO), "colony-in-recovery-mode"); await checkErrorRevert(metaColony.startNextRewardPayout(ADDRESS_ZERO, HASHZERO, HASHZERO, 0, []), "colony-in-recovery-mode"); await checkErrorRevert(metaColony.claimRewardPayout(0, [0, 0, 0, 0, 0, 0, 0], HASHZERO, HASHZERO, 0, []), "colony-in-recovery-mode"); diff --git a/test/extensions/funding-queue.js b/test/extensions/funding-queue.js index e45874ab73..d3b59f984f 100644 --- a/test/extensions/funding-queue.js +++ b/test/extensions/funding-queue.js @@ -254,9 +254,9 @@ contract("Funding Queues", (accounts) => { expect(deprecated).to.equal(true); }); - it("cannot create a basic proposal with bad inheritence", async () => { - await checkErrorRevert(fundingQueue.createProposal(1, 0, 1, 1, 3, WAD, token.address, { from: USER0 }), "funding-queue-bad-inheritence-from"); - await checkErrorRevert(fundingQueue.createProposal(1, 1, 0, 3, 1, WAD, token.address, { from: USER0 }), "funding-queue-bad-inheritence-to"); + it("cannot create a basic proposal with bad inheritance", async () => { + await checkErrorRevert(fundingQueue.createProposal(1, 0, 1, 1, 3, WAD, token.address, { from: USER0 }), "funding-queue-bad-inheritance-from"); + await checkErrorRevert(fundingQueue.createProposal(1, 1, 0, 3, 1, WAD, token.address, { from: USER0 }), "funding-queue-bad-inheritance-to"); }); it("can stake a proposal", async () => { From ad15861e5bf38683b44713fbc6ba7b66aeb574e1 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Thu, 14 Jul 2022 09:59:15 -0700 Subject: [PATCH 45/59] Respond to review comments VI --- contracts/colony/ColonyExpenditure.sol | 20 ++++++++++---------- contracts/colony/ColonyFunding.sol | 10 +++++++++- contracts/colony/ColonyStorage.sol | 2 +- test/contracts-network/colony-expenditure.js | 17 +++++++++++++++-- 4 files changed, 35 insertions(+), 14 deletions(-) diff --git a/contracts/colony/ColonyExpenditure.sol b/contracts/colony/ColonyExpenditure.sol index e017044125..2bf0e3981f 100644 --- a/contracts/colony/ColonyExpenditure.sol +++ b/contracts/colony/ColonyExpenditure.sol @@ -69,7 +69,7 @@ contract ColonyExpenditure is ColonyStorage { public stoppable expenditureDraftOrLocked(_id) - expenditureOwner(_id) + expenditureOnlyOwner(_id) { expenditures[_id].owner = _newOwner; @@ -97,7 +97,7 @@ contract ColonyExpenditure is ColonyStorage { public stoppable expenditureDraft(_id) - expenditureOwner(_id) + expenditureOnlyOwner(_id) { expenditures[_id].status = ExpenditureStatus.Cancelled; @@ -108,7 +108,7 @@ contract ColonyExpenditure is ColonyStorage { public stoppable expenditureDraft(_id) - expenditureOwner(_id) + expenditureOnlyOwner(_id) { expenditures[_id].status = ExpenditureStatus.Locked; @@ -119,7 +119,7 @@ contract ColonyExpenditure is ColonyStorage { public stoppable expenditureDraftOrLocked(_id) - expenditureOwner(_id) + expenditureOnlyOwner(_id) { FundingPot storage fundingPot = fundingPots[expenditures[_id].fundingPotId]; require(fundingPot.payoutsWeCannotMake == 0, "colony-expenditure-not-funded"); @@ -134,7 +134,7 @@ contract ColonyExpenditure is ColonyStorage { public stoppable expenditureDraft(_id) - expenditureOwner(_id) + expenditureOnlyOwner(_id) { emit ExpenditureMetadataSet(msgSender(), _id, _metadata); } @@ -157,7 +157,7 @@ contract ColonyExpenditure is ColonyStorage { public stoppable expenditureDraft(_id) - expenditureOwner(_id) + expenditureOnlyOwner(_id) { require(_slots.length == _recipients.length, "colony-expenditure-bad-slots"); @@ -172,7 +172,7 @@ contract ColonyExpenditure is ColonyStorage { public stoppable expenditureDraft(_id) - expenditureOwner(_id) + expenditureOnlyOwner(_id) { require(_slots.length == _skillIds.length, "colony-expenditure-bad-slots"); IColonyNetwork colonyNetworkContract = IColonyNetwork(colonyNetworkAddress); @@ -194,7 +194,7 @@ contract ColonyExpenditure is ColonyStorage { public stoppable expenditureDraft(_id) - expenditureOwner(_id) + expenditureOnlyOwner(_id) { require(_slots.length == _claimDelays.length, "colony-expenditure-bad-slots"); @@ -209,7 +209,7 @@ contract ColonyExpenditure is ColonyStorage { public stoppable expenditureDraft(_id) - expenditureOwner(_id) + expenditureOnlyOwner(_id) { require(_slots.length == _payoutModifiers.length, "colony-expenditure-bad-slots"); @@ -237,7 +237,7 @@ contract ColonyExpenditure is ColonyStorage { public stoppable expenditureDraft(_id) - expenditureOwner(_id) + expenditureOnlyOwner(_id) { if (_recipients.length > 0) { setExpenditureRecipients(_id, _recipientSlots, _recipients); } if (_skillIds.length > 0) { setExpenditureSkills(_id, _skillIdSlots, _skillIds); } diff --git a/contracts/colony/ColonyFunding.sol b/contracts/colony/ColonyFunding.sol index 58e3f1bf30..c9da8fe0f2 100755 --- a/contracts/colony/ColonyFunding.sol +++ b/contracts/colony/ColonyFunding.sol @@ -227,14 +227,22 @@ contract ColonyFunding is ColonyStorage { // ignore-swc-123 authDomain(_permissionDomainId, _childSkillIndex, expenditures[_id].domainId) { require(_tokens.length == _amounts.length, "colony-funding-mismatched-arguments"); + + uint256[] memory slots = new uint256[](1); + slots[0] = _slot; + uint256[] memory amounts = new uint256[](1); + for (uint256 i; i < _tokens.length; i++) { - setExpenditurePayout(_id, _slot, _tokens[i], _amounts[i]); + amounts[0] = _amounts[i]; + setExpenditurePayoutsInternal(_id, slots, _tokens[i], amounts); } } function setExpenditurePayout(uint256 _id, uint256 _slot, address _token, uint256 _amount) public stoppable + expenditureDraft(_id) + expenditureOwnerOrSelf(_id) { uint256[] memory slots = new uint256[](1); slots[0] = _slot; diff --git a/contracts/colony/ColonyStorage.sol b/contracts/colony/ColonyStorage.sol index bcd61f1e3b..204d2db8bd 100755 --- a/contracts/colony/ColonyStorage.sol +++ b/contracts/colony/ColonyStorage.sol @@ -201,7 +201,7 @@ contract ColonyStorage is ColonyDataTypes, ColonyNetworkDataTypes, DSMath, Commo _; } - modifier expenditureOwner(uint256 _id) { + modifier expenditureOnlyOwner(uint256 _id) { require(expenditures[_id].owner == msgSender(), "colony-expenditure-not-owner"); _; } diff --git a/test/contracts-network/colony-expenditure.js b/test/contracts-network/colony-expenditure.js index 55f4e36478..98d5641dca 100644 --- a/test/contracts-network/colony-expenditure.js +++ b/test/contracts-network/colony-expenditure.js @@ -370,7 +370,7 @@ contract("Colony Expenditure", (accounts) => { expect(payout).to.eq.BN(20); }); - it("should allow owners to update payouts at once in one slot", async () => { + it("should allow arbitrators to update payouts at once in one slot", async () => { await colony.setExpenditureSlotPayouts(1, UINT256_MAX, expenditureId, SLOT0, [token.address, otherToken.address], [10, 20], { from: ARBITRATOR, }); @@ -410,7 +410,7 @@ contract("Colony Expenditure", (accounts) => { it("should not allow non-owners to update skills or payouts", async () => { await checkErrorRevert(colony.setExpenditureSkill(expenditureId, SLOT0, GLOBAL_SKILL_ID), "colony-expenditure-not-owner"); - await checkErrorRevert(colony.setExpenditurePayout(expenditureId, SLOT0, token.address, WAD), "colony-expenditure-not-owner"); + await checkErrorRevert(colony.setExpenditurePayout(expenditureId, SLOT0, token.address, WAD), "colony-expenditure-not-owner-or-self"); }); it("should allow owners to add a slot payout", async () => { @@ -465,6 +465,19 @@ contract("Colony Expenditure", (accounts) => { 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-or-self" + ); + }); + it("should allow owners to update many values simultaneously", async () => { await colony.setExpenditureValues( expenditureId, From cbee88f369efa95786739094806e4a9b1a0fdbb6 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Thu, 14 Jul 2022 17:22:19 -0700 Subject: [PATCH 46/59] Update smoke tests --- test-smoke/colony-storage-consistent.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-smoke/colony-storage-consistent.js b/test-smoke/colony-storage-consistent.js index b9fc8c1490..409d48bde8 100644 --- a/test-smoke/colony-storage-consistent.js +++ b/test-smoke/colony-storage-consistent.js @@ -149,7 +149,7 @@ contract("Contract Storage", (accounts) => { console.log("miningCycleStateHash:", miningCycleStateHash); console.log("tokenLockingStateHash:", tokenLockingStateHash); - expect(colonyNetworkStateHash).to.equal("0x29097499f8e5f6c3203ab16f919abace364d7baf0c5686346ffd22133cd4db6a"); + expect(colonyNetworkStateHash).to.equal("0x850f123f81d395bfc6041976d5127f1c142e29132043180b86351275d8940e9a"); expect(colonyStateHash).to.equal("0x46e36a62ba5cc6c071e5f7ab3e76921f5211f32f8125fd02fed52e078f02364d"); expect(metaColonyStateHash).to.equal("0x86fc4659e140aa1fc3edcd30ebd438198dc787f6abc4be7c730bb9741d1ad049"); expect(miningCycleStateHash).to.equal("0x632d459a2197708bd2dbde87e8275c47dddcdf16d59e3efd21dcef9acb2a7366"); From fb49de4c3a516173e0bb377f3658ebea6a1811a1 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Fri, 15 Jul 2022 13:53:50 -0700 Subject: [PATCH 47/59] Respond to review comments VII --- contracts/extensions/StakedExpenditure.sol | 35 +++++++++++++--------- contracts/extensions/VotingReputation.sol | 2 +- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/contracts/extensions/StakedExpenditure.sol b/contracts/extensions/StakedExpenditure.sol index d2df0f84fa..cdb998fe8e 100644 --- a/contracts/extensions/StakedExpenditure.sol +++ b/contracts/extensions/StakedExpenditure.sol @@ -222,22 +222,29 @@ contract StakedExpenditure is ColonyExtensionMeta, PatriciaTreeProofs { ); if (_punish) { - Stake storage stake = stakes[_expenditureId]; - require(stake.amount > 0, "staked-expenditure-nothing-to-slash"); - - uint256 stakeAmount = stake.amount; - address stakeCreator = stake.creator; + uint256 stakeAmount = stakes[_expenditureId].amount; + address stakeCreator = stakes[_expenditureId].creator; delete stakes[_expenditureId]; - colony.transferStake(_permissionDomainId, _childSkillIndex, address(this), stakeCreator, expenditure.domainId, stakeAmount, address(0x0)); - - colony.emitDomainReputationPenalty( - _permissionDomainId, - _childSkillIndex, - expenditure.domainId, - stakeCreator, - -int256(stakeAmount) - ); + 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); diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index 1ce8860f36..f539c4c86d 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -1103,7 +1103,7 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs, BasicMetaTrans uint256 permissionDomainId; uint256 childSkillIndex; uint256 expenditureId; - uint256 storageSlot; + uint256 storageSlot; // This value is only used if getSig(action) == SET_EXPENDITURE_STATE assembly { permissionDomainId := mload(add(action, 0x24)) From 2be192bd0126ae3b1191cda8718b47d9c070b5ab Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Tue, 19 Jul 2022 10:21:29 -0700 Subject: [PATCH 48/59] Update ExpenditureStateChanged event --- contracts/colony/ColonyDataTypes.sol | 13 +++++++++++-- contracts/colony/ColonyExpenditure.sol | 4 ++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/contracts/colony/ColonyDataTypes.sol b/contracts/colony/ColonyDataTypes.sol index 66ea76e9e3..c12dfc0919 100755 --- a/contracts/colony/ColonyDataTypes.sol +++ b/contracts/colony/ColonyDataTypes.sol @@ -160,9 +160,18 @@ interface ColonyDataTypes { /// @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 slot Memory slot being set + /// @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, bytes32 slot, bytes32 value); + 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 diff --git a/contracts/colony/ColonyExpenditure.sol b/contracts/colony/ColonyExpenditure.sol index 2bf0e3981f..48575691a1 100644 --- a/contracts/colony/ColonyExpenditure.sol +++ b/contracts/colony/ColonyExpenditure.sol @@ -327,6 +327,8 @@ contract ColonyExpenditure is ColonyStorage { } executeStateChange(_id, _storageSlot, _mask, _keys, _value); + + emit ExpenditureStateChanged(msgSender(), _id, _storageSlot, _mask, _keys, _value); } // Public view functions @@ -392,7 +394,5 @@ contract ColonyExpenditure is ColonyStorage { assembly { sstore(slot, value) // ignore-swc-124 } - - emit ExpenditureStateChanged(msgSender(), _id, slot, value); } } From 4362e443afb8e0eeee50dedda1ccea24ababe37b Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Tue, 19 Jul 2022 18:34:12 -0700 Subject: [PATCH 49/59] Improve no-op test --- test/contracts-network/colony-expenditure.js | 49 ++++++++++++++------ 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/test/contracts-network/colony-expenditure.js b/test/contracts-network/colony-expenditure.js index 98d5641dca..f537e77731 100644 --- a/test/contracts-network/colony-expenditure.js +++ b/test/contracts-network/colony-expenditure.js @@ -553,41 +553,64 @@ contract("Colony Expenditure", (accounts) => { }); 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(ADDRESS_ZERO); + expect(slot.recipient).to.equal(RECIPIENT); expect(slot.skills[0]).to.be.zero; - expect(slot.claimDelay).to.be.zero; - expect(slot.payoutModifier).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(ADDRESS_ZERO); - expect(slot.skills[0]).to.be.zero; - expect(slot.claimDelay).to.be.zero; + 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(ADDRESS_ZERO); - expect(slot.skills[0]).to.be.zero; + 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.be.zero; + expect(slot.payoutModifier).to.eq.BN(WAD.divn(2)); let payout; payout = await colony.getExpenditureSlotPayout(expenditureId, SLOT0, token.address); - expect(payout).to.be.zero; + expect(payout).to.eq.BN(WAD.muln(10)); payout = await colony.getExpenditureSlotPayout(expenditureId, SLOT1, token.address); - expect(payout).to.be.zero; + 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.be.zero; + expect(payout).to.eq.BN(WAD.muln(30)); payout = await colony.getExpenditureSlotPayout(expenditureId, SLOT2, otherToken.address); - expect(payout).to.be.zero; + expect(payout).to.eq.BN(WAD.muln(40)); }); }); From 190a66a170f87430a2e26a080248a2c6cf106693 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Mon, 25 Jul 2022 14:51:51 -0400 Subject: [PATCH 50/59] Update yarn.lock --- yarn.lock | 70 +------------------------------------------------------ 1 file changed, 1 insertion(+), 69 deletions(-) diff --git a/yarn.lock b/yarn.lock index 3c1f40e89a..73ba3934e7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -241,14 +241,6 @@ core-js "^2.6.5" regenerator-runtime "^0.13.4" -"@babel/polyfill@^7.0.0": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/polyfill/-/polyfill-7.12.1.tgz#1f2d6371d1261bbd961f3c5d5909150e12d0bd96" - integrity sha512-X0pi0V6gxLi6lFZpGmeNa4zxtwEmCs42isWLNjZZDE0Y8yVfgu0T2OAHlzBbdYlqbW/YXVvoBHpATEM+goCj8g== - dependencies: - core-js "^2.6.5" - regenerator-runtime "^0.13.4" - "@babel/runtime@^7.6.3", "@babel/runtime@^7.9.2": version "7.18.6" resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.6.tgz" @@ -1557,56 +1549,6 @@ ethereumjs-tx "^1.3.7" strip-hex-prefix "^1.0.0" -"@umaprotocol/truffle-ledger-provider@^1.0.5": - version "1.0.5" - resolved "https://registry.yarnpkg.com/@umaprotocol/truffle-ledger-provider/-/truffle-ledger-provider-1.0.5.tgz#e30025c4ecc2f2540825c46788ef1291474080be" - integrity sha512-RPkhftL0GIrkX6QB7IsvsUx8dl6NUXs4kv2U1TtnLXkq6EsfEhmZeLm8jrtBKcY05Uuq4BqyfxcM5o0C3Oljlw== - optionalDependencies: - "@babel/polyfill" "^7.0.0" - "@ledgerhq/hw-transport-node-hid" "^4.32.0" - "@umaprotocol/web3-provider-engine" "^15.0.4" - "@umaprotocol/web3-subprovider" "^4.74.3" - ethereumjs-wallet "^0.6.0" - -"@umaprotocol/web3-provider-engine@^15.0.4", "@umaprotocol/web3-provider-engine@^15.0.5": - version "15.0.5" - resolved "https://registry.yarnpkg.com/@umaprotocol/web3-provider-engine/-/web3-provider-engine-15.0.5.tgz#7edd5e60edde1b0453174ee20d336c3b5eabe0f6" - integrity sha512-bygswdrRMZ3z+fSMi9aOx5T/Mm4geIopcj3NLQjd56Ekek/PlXw8L5OLlwb0VLePyknFn4jwT4hgZOg/Tu7uWw== - dependencies: - async "^2.5.0" - backoff "^2.5.0" - clone "^2.0.0" - cross-fetch "^2.1.0" - eth-block-tracker "^3.0.0" - eth-json-rpc-infura "^3.1.0" - eth-sig-util "^1.4.2" - ethereumjs-block "^1.2.2" - ethereumjs-tx "^1.2.0" - ethereumjs-util "^5.1.5" - ethereumjs-vm "^2.3.4" - json-rpc-error "^2.0.0" - json-stable-stringify "^1.0.1" - promise-to-callback "^1.0.0" - readable-stream "^2.2.9" - request "^2.85.0" - semaphore "^1.0.3" - tape "^4.4.0" - ws "^5.1.1" - xhr "^2.2.0" - xtend "^4.0.1" - -"@umaprotocol/web3-subprovider@^4.74.3": - version "4.74.5" - resolved "https://registry.yarnpkg.com/@umaprotocol/web3-subprovider/-/web3-subprovider-4.74.5.tgz#b755fe4df015bfae1e38b5fcada91638294e1347" - integrity sha512-ISZQtpPRqrMTaSYLwKpMsbRdrQI3JPo0hp68BW7snKLFkzEYGNX0NgSAtBwA/RgUlnD1oL0PBZ+wEvkVNdXM0Q== - dependencies: - "@ledgerhq/hw-app-eth" "^4.74.2" - "@ledgerhq/hw-transport" "^4.74.2" - "@ledgerhq/hw-transport-u2f" "^4.74.2" - "@umaprotocol/web3-provider-engine" "^15.0.5" - ethereumjs-tx "^1.3.7" - strip-hex-prefix "^1.0.0" - "@ungap/promise-all-settled@1.1.2": version "1.1.2" resolved "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz" @@ -10816,17 +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@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0: - version "5.7.1" - resolved "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - -semver@^6.0.0, semver@^6.3.0: - version "6.3.0" - resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== - -semver@^7.3.4, 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== From 11d4e8c629aefd56e3823f20401dd29c4d8488ed Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Thu, 21 Jul 2022 14:25:27 -0700 Subject: [PATCH 51/59] Remove extraneous utility functions --- contracts/colony/Colony.sol | 10 ++--- contracts/colony/ColonyAuthority.sol | 3 +- contracts/colony/ColonyExpenditure.sol | 7 ++-- contracts/colony/ColonyFunding.sol | 29 ++------------ contracts/colony/IColony.sol | 16 -------- contracts/extensions/VotingReputation.sol | 31 +++++---------- test-smoke/colony-storage-consistent.js | 10 ++--- test/contracts-network/colony-expenditure.js | 19 --------- test/extensions/voting-rep.js | 41 -------------------- 9 files changed, 26 insertions(+), 140 deletions(-) diff --git a/contracts/colony/Colony.sol b/contracts/colony/Colony.sol index 534f16e1c0..7b7f184934 100755 --- a/contracts/colony/Colony.sol +++ b/contracts/colony/Colony.sol @@ -312,15 +312,11 @@ contract Colony is BasicMetaTransaction, ColonyStorage, PatriciaTreeProofs { ColonyAuthority colonyAuthority = ColonyAuthority(address(authority)); bytes4 sig; - sig = bytes4(keccak256("setExpenditurePayouts(uint256,uint256,uint256,uint256[],address,uint256[])")); - colonyAuthority.setRoleCapability(uint8(ColonyRole.Arbitration), address(this), sig, true); - - sig = bytes4(keccak256("setExpenditureSlotPayouts(uint256,uint256,uint256,uint256,address[],uint256[])")); - colonyAuthority.setRoleCapability(uint8(ColonyRole.Arbitration), address(this), sig, true); - - sig = bytes4(keccak256("moveFundsBetweenPots(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256[],address[])")); colonyAuthority.setRoleCapability(uint8(ColonyRole.Funding), address(this), sig, true); + + sig = bytes4(keccak256("setExpenditurePayouts(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 42a06f400d..6ba331e4e5 100644 --- a/contracts/colony/ColonyAuthority.sol +++ b/contracts/colony/ColonyAuthority.sol @@ -126,9 +126,8 @@ contract ColonyAuthority is CommonAuthority { addRoleCapability(ROOT_ROLE, "editColonyByDelta(string)"); // Added in colony v10 (ginger-lwss) - addRoleCapability(ARBITRATION_ROLE, "setExpenditurePayouts(uint256,uint256,uint256,uint256[],address,uint256[])"); - addRoleCapability(ARBITRATION_ROLE, "setExpenditureSlotPayouts(uint256,uint256,uint256,uint256,address[],uint256[])"); addRoleCapability(FUNDING_ROLE, "moveFundsBetweenPots(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256[],address[])"); + addRoleCapability(ARBITRATION_ROLE, "setExpenditurePayouts(uint256,uint256,uint256,uint256[],address,uint256[])"); } function addRoleCapability(uint8 role, bytes memory sig) private { diff --git a/contracts/colony/ColonyExpenditure.sol b/contracts/colony/ColonyExpenditure.sol index 48575691a1..88a0526f77 100644 --- a/contracts/colony/ColonyExpenditure.sol +++ b/contracts/colony/ColonyExpenditure.sol @@ -326,7 +326,7 @@ contract ColonyExpenditure is ColonyStorage { require(false, "colony-expenditure-bad-slot"); } - executeStateChange(_id, _storageSlot, _mask, _keys, _value); + executeStateChange(keccak256(abi.encode(_id, _storageSlot)), _mask, _keys, _value); emit ExpenditureStateChanged(msgSender(), _id, _storageSlot, _mask, _keys, _value); } @@ -356,8 +356,7 @@ contract ColonyExpenditure is ColonyStorage { uint256 constant MAX_ARRAY = 1024; // Prevent writing arbitrary slots function executeStateChange( - uint256 _id, - uint256 _storageSlot, + bytes32 _slot, bool[] memory _mask, bytes32[] memory _keys, bytes32 _value @@ -367,7 +366,7 @@ contract ColonyExpenditure is ColonyStorage { require(_keys.length == _mask.length, "colony-expenditure-bad-mask"); bytes32 value = _value; - bytes32 slot = keccak256(abi.encode(_id, _storageSlot)); + bytes32 slot = _slot; // See https://solidity.readthedocs.io/en/v0.5.14/miscellaneous.html for (uint256 i; i < _keys.length; i++) { diff --git a/contracts/colony/ColonyFunding.sol b/contracts/colony/ColonyFunding.sol index c9da8fe0f2..4b1bd8f729 100755 --- a/contracts/colony/ColonyFunding.sol +++ b/contracts/colony/ColonyFunding.sol @@ -166,6 +166,7 @@ contract ColonyFunding is ColonyStorage { // ignore-swc-123 } } + /// @notice For owners to update payouts with many tokens and many slots function setExpenditurePayouts( uint256 _id, uint256[][] memory _slots, @@ -183,6 +184,7 @@ contract ColonyFunding is ColonyStorage { // ignore-swc-123 } /// @notice @deprecated + /// @notice For owners to update payouts with one token and many slots function setExpenditurePayouts( uint256 _id, uint256[] memory _slots, @@ -197,6 +199,7 @@ contract ColonyFunding is ColonyStorage { // ignore-swc-123 setExpenditurePayoutsInternal(_id, _slots, _token, _amounts); } + /// @notice For arbitrators to update payouts with one token and many slots function setExpenditurePayouts( uint256 _permissionDomainId, uint256 _childSkillIndex, @@ -213,31 +216,7 @@ contract ColonyFunding is ColonyStorage { // ignore-swc-123 setExpenditurePayoutsInternal(_id, _slots, _token, _amounts); } - function setExpenditureSlotPayouts( - uint256 _permissionDomainId, - uint256 _childSkillIndex, - uint256 _id, - uint256 _slot, - address[] memory _tokens, - uint256[] memory _amounts - ) - public - stoppable - validExpenditure(_id) - authDomain(_permissionDomainId, _childSkillIndex, expenditures[_id].domainId) - { - require(_tokens.length == _amounts.length, "colony-funding-mismatched-arguments"); - - uint256[] memory slots = new uint256[](1); - slots[0] = _slot; - uint256[] memory amounts = new uint256[](1); - - for (uint256 i; i < _tokens.length; i++) { - amounts[0] = _amounts[i]; - setExpenditurePayoutsInternal(_id, slots, _tokens[i], 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 diff --git a/contracts/colony/IColony.sol b/contracts/colony/IColony.sol index e58167456a..6193b1852e 100644 --- a/contracts/colony/IColony.sol +++ b/contracts/colony/IColony.sol @@ -453,22 +453,6 @@ interface IColony is ColonyDataTypes, IRecovery, IBasicMetaTransaction { /// @param _amounts 2D array of payout amounts function setExpenditurePayouts(uint256 _id, uint256[][] memory _slots, address[] memory _tokens, uint256[][] memory _amounts) external; - /// @notice Set the token payouts in 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 Slot to set payouts - /// @param _tokens Array of token addresses, `0x0` value indicates Ether - /// @param _amounts Array of payout amounts - function setExpenditureSlotPayouts( - uint256 _permissionDomainId, - uint256 _childSkillIndex, - uint256 _id, - uint256 _slot, - address[] memory _tokens, - uint256[] memory _amounts - ) external; - /// @notice Set the token payouts in given expenditure slots. 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` diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index f539c4c86d..6d79dacc84 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -64,10 +64,6 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs, BasicMetaTrans "setExpenditurePayouts(uint256,uint256,uint256,uint256[],address,uint256[])" )); - bytes4 constant SET_EXPENDITURE_SLOT_PAYOUTS = bytes4(keccak256( - "setExpenditureSlotPayouts(uint256,uint256,uint256,uint256,address[],uint256[])" - )); - bytes4 constant OLD_MOVE_FUNDS_SIG = bytes4(keccak256( "moveFundsBetweenPots(uint256,uint256,uint256,uint256,uint256,uint256,address)" )); @@ -403,8 +399,7 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs, BasicMetaTrans !motion.escalated && motion.stakes[YAY] == requiredStake && ( getSig(motion.action) == SET_EXPENDITURE_STATE || - getSig(motion.action) == SET_EXPENDITURE_PAYOUTS || - getSig(motion.action) == SET_EXPENDITURE_SLOT_PAYOUTS + getSig(motion.action) == SET_EXPENDITURE_PAYOUTS ) && motion.altTarget == address(0x0) ) { bytes32 structHash = hashExpenditureActionStruct(motion.action); @@ -601,8 +596,7 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs, BasicMetaTrans if (( getSig(motion.action) == SET_EXPENDITURE_STATE || - getSig(motion.action) == SET_EXPENDITURE_PAYOUTS || - getSig(motion.action) == SET_EXPENDITURE_SLOT_PAYOUTS + getSig(motion.action) == SET_EXPENDITURE_PAYOUTS ) && getTarget(motion.altTarget) == address(colony) ) { bytes32 structHash = hashExpenditureActionStruct(motion.action); @@ -1065,8 +1059,7 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs, BasicMetaTrans function hashExpenditureActionStruct(bytes memory action) internal returns (bytes32 hash) { assert( getSig(action) == SET_EXPENDITURE_STATE || - getSig(action) == SET_EXPENDITURE_PAYOUTS || - getSig(action) == SET_EXPENDITURE_SLOT_PAYOUTS + getSig(action) == SET_EXPENDITURE_PAYOUTS ); uint256 expenditureId; @@ -1119,6 +1112,7 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs, BasicMetaTrans ) { bytes memory mainClaimDelayAction = new bytes(4 + 32 * 11); // 356 bytes + assembly { mstore(add(mainClaimDelayAction, 0x20), functionSignature) mstore(add(mainClaimDelayAction, 0x24), permissionDomainId) @@ -1133,24 +1127,18 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs, BasicMetaTrans mstore(add(mainClaimDelayAction, 0x144), 1) // keys length mstore(add(mainClaimDelayAction, 0x164), 4) // globalClaimDelay offset } + return mainClaimDelayAction; - // If we are editing an expenditure slot or setting payouts for one slot + // If we are editing an expenditure slot } else { - bytes memory slotClaimDelayAction = new bytes(4 + 32 * 13); // 420 bytes + uint256 expenditureSlot; + bytes memory slotClaimDelayAction = new bytes(4 + 32 * 13); // 420 bytes - if (getSig(action) == SET_EXPENDITURE_SLOT_PAYOUTS) { - assembly { - expenditureSlot := mload(add(action, 0x84)) - } - } else { - assembly { + assembly { expenditureSlot := mload(add(action, 0x184)) - } - } - assembly { mstore(add(slotClaimDelayAction, 0x20), functionSignature) mstore(add(slotClaimDelayAction, 0x24), permissionDomainId) mstore(add(slotClaimDelayAction, 0x44), childSkillIndex) @@ -1166,6 +1154,7 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs, BasicMetaTrans mstore(add(slotClaimDelayAction, 0x184), expenditureSlot) mstore(add(slotClaimDelayAction, 0x1a4), 1) // claimDelay offset } + return slotClaimDelayAction; } diff --git a/test-smoke/colony-storage-consistent.js b/test-smoke/colony-storage-consistent.js index 409d48bde8..c36a5c18d2 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("0x850f123f81d395bfc6041976d5127f1c142e29132043180b86351275d8940e9a"); - expect(colonyStateHash).to.equal("0x46e36a62ba5cc6c071e5f7ab3e76921f5211f32f8125fd02fed52e078f02364d"); - expect(metaColonyStateHash).to.equal("0x86fc4659e140aa1fc3edcd30ebd438198dc787f6abc4be7c730bb9741d1ad049"); - expect(miningCycleStateHash).to.equal("0x632d459a2197708bd2dbde87e8275c47dddcdf16d59e3efd21dcef9acb2a7366"); - expect(tokenLockingStateHash).to.equal("0x30fbcbfbe589329fe20288101faabe1f60a4610ae0c0effb15526c6b390a8e07"); + expect(colonyNetworkStateHash).to.equal("0x706e2d895a31bfeba0356c84dec58b011e98570fbe56c8c59a109f6dc38093b8"); + expect(colonyStateHash).to.equal("0x82cdec79eb889c8860763d07f4e7c5ad4cde875c966b1e904d75e951b128315f"); + expect(metaColonyStateHash).to.equal("0x97b3fe37d2143132be6760eda0dcb4cff66a1fc9f31f29377d20664c0b7406ed"); + expect(miningCycleStateHash).to.equal("0x63723c9783b0f0c20e8640fc383ed40dbc228a93cc73a18fa6c6e5f0c38e8694"); + expect(tokenLockingStateHash).to.equal("0xa4dd9734ad2f6b18ec82a5d10be671a5082d6400173d47ac1401580f57cd1638"); }); }); }); diff --git a/test/contracts-network/colony-expenditure.js b/test/contracts-network/colony-expenditure.js index f537e77731..d39988e365 100644 --- a/test/contracts-network/colony-expenditure.js +++ b/test/contracts-network/colony-expenditure.js @@ -370,25 +370,6 @@ contract("Colony Expenditure", (accounts) => { expect(payout).to.eq.BN(20); }); - it("should allow arbitrators to update payouts at once in one slot", async () => { - await colony.setExpenditureSlotPayouts(1, UINT256_MAX, expenditureId, SLOT0, [token.address, otherToken.address], [10, 20], { - from: ARBITRATOR, - }); - - let payout; - payout = await colony.getExpenditureSlotPayout(expenditureId, SLOT0, token.address); - expect(payout).to.eq.BN(10); - payout = await colony.getExpenditureSlotPayout(expenditureId, SLOT0, otherToken.address); - expect(payout).to.eq.BN(20); - }); - - it("should not allow owners to update payouts at once in one slot with mismatched arguments", async () => { - await checkErrorRevert( - colony.setExpenditureSlotPayouts(1, UINT256_MAX, expenditureId, SLOT0, [token.address, otherToken.address], [10], { from: ARBITRATOR }), - "colony-funding-mismatched-arguments" - ); - }); - it("should not allow owners to update many slot payouts with mismatched arguments", async () => { const setExpenditurePayouts = colony.methods["setExpenditurePayouts(uint256,uint256[],address,uint256[])"]; diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index c30959e2b2..e44e7b649e 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -751,47 +751,6 @@ contract("Voting Reputation", (accounts) => { expect(expenditure.globalClaimDelay).to.be.zero; }); - it("can update the expenditure slot claimDelay if voting on expenditure slot 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, "setExpenditureSlotPayouts", [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; - - expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); - expect(expenditureSlot.claimDelay).to.be.zero; - - await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - - expenditureMotionCount = await voting.getExpenditureMotionCount(soliditySha3(expenditureId, 0)); - expect(expenditureMotionCount).to.eq.BN(1); - - expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); - expect(expenditureSlot.claimDelay).to.eq.BN(UINT256_MAX.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 states", async () => { await colony.makeExpenditure(1, UINT256_MAX, 1); const expenditureId = await colony.getExpenditureCount(); From 96dc77d0b3fed087385bccf08bb17a0dfb7dcd63 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Wed, 27 Jul 2022 10:10:21 +0100 Subject: [PATCH 52/59] Introduce setExpenditurePayout for arbitrators --- contracts/colony/Colony.sol | 6 +- contracts/colony/ColonyAuthority.sol | 2 +- contracts/colony/ColonyFunding.sol | 14 ++- contracts/colony/IColony.sol | 12 +- contracts/extensions/VotingReputation.sol | 116 ++++++++++--------- test/contracts-network/colony-expenditure.js | 10 +- test/contracts-network/colony-recovery.js | 1 + test/extensions/voting-rep.js | 100 ++++++++++++++-- 8 files changed, 177 insertions(+), 84 deletions(-) diff --git a/contracts/colony/Colony.sol b/contracts/colony/Colony.sol index 7b7f184934..344bcec8d1 100755 --- a/contracts/colony/Colony.sol +++ b/contracts/colony/Colony.sol @@ -312,11 +312,11 @@ contract Colony is BasicMetaTransaction, ColonyStorage, PatriciaTreeProofs { 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); + sig = bytes4(keccak256("moveFundsBetweenPots(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256[],address[])")); colonyAuthority.setRoleCapability(uint8(ColonyRole.Funding), address(this), sig, true); - - sig = bytes4(keccak256("setExpenditurePayouts(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 6ba331e4e5..825ad2dd6c 100644 --- a/contracts/colony/ColonyAuthority.sol +++ b/contracts/colony/ColonyAuthority.sol @@ -126,8 +126,8 @@ contract ColonyAuthority is CommonAuthority { addRoleCapability(ROOT_ROLE, "editColonyByDelta(string)"); // Added in colony v10 (ginger-lwss) + addRoleCapability(ARBITRATION_ROLE, "setExpenditurePayout(uint256,uint256,uint256,uint256,address,uint256)"); addRoleCapability(FUNDING_ROLE, "moveFundsBetweenPots(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256[],address[])"); - addRoleCapability(ARBITRATION_ROLE, "setExpenditurePayouts(uint256,uint256,uint256,uint256[],address,uint256[])"); } function addRoleCapability(uint8 role, bytes memory sig) private { diff --git a/contracts/colony/ColonyFunding.sol b/contracts/colony/ColonyFunding.sol index 4b1bd8f729..fefee80aba 100755 --- a/contracts/colony/ColonyFunding.sol +++ b/contracts/colony/ColonyFunding.sol @@ -199,21 +199,25 @@ contract ColonyFunding is ColonyStorage { // ignore-swc-123 setExpenditurePayoutsInternal(_id, _slots, _token, _amounts); } - /// @notice For arbitrators to update payouts with one token and many slots - function setExpenditurePayouts( + /// @notice For arbitrators to update payouts with one token and one slot + function setExpenditurePayout( uint256 _permissionDomainId, uint256 _childSkillIndex, uint256 _id, - uint256[] memory _slots, + uint256 _slot, address _token, - uint256[] memory _amounts + uint256 _amount ) public stoppable validExpenditure(_id) authDomain(_permissionDomainId, _childSkillIndex, expenditures[_id].domainId) { - setExpenditurePayoutsInternal(_id, _slots, _token, _amounts); + 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 diff --git a/contracts/colony/IColony.sol b/contracts/colony/IColony.sol index 6193b1852e..ca1bbe1d01 100644 --- a/contracts/colony/IColony.sol +++ b/contracts/colony/IColony.sol @@ -453,20 +453,20 @@ interface IColony is ColonyDataTypes, IRecovery, IBasicMetaTransaction { /// @param _amounts 2D array of payout amounts function setExpenditurePayouts(uint256 _id, uint256[][] memory _slots, address[] memory _tokens, uint256[][] memory _amounts) external; - /// @notice Set the token payouts in given expenditure slots. Can only be called by an Arbitration user. + /// @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 _slots Array of slots to set payouts + /// @param _slot The slot to set the payout /// @param _token Address of the token, `0x0` value indicates Ether - /// @param _amounts Payout amounts - function setExpenditurePayouts( + /// @param _amount Payout amount + function setExpenditurePayout( uint256 _permissionDomainId, uint256 _childSkillIndex, uint256 _id, - uint256[] memory _slots, + uint256 _slot, address _token, - uint256[] memory _amounts + uint256 _amount ) external; /// @notice Deprecated diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index 6d79dacc84..955bbaf9fa 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -60,8 +60,8 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs, BasicMetaTrans "setExpenditureState(uint256,uint256,uint256,uint256,bool[],bytes32[],bytes32)" )); - bytes4 constant SET_EXPENDITURE_PAYOUTS = bytes4(keccak256( - "setExpenditurePayouts(uint256,uint256,uint256,uint256[],address,uint256[])" + bytes4 constant SET_EXPENDITURE_PAYOUT = bytes4(keccak256( + "setExpenditurePayout(uint256,uint256,uint256,uint256,address,uint256)" )); bytes4 constant OLD_MOVE_FUNDS_SIG = bytes4(keccak256( @@ -399,7 +399,7 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs, BasicMetaTrans !motion.escalated && motion.stakes[YAY] == requiredStake && ( getSig(motion.action) == SET_EXPENDITURE_STATE || - getSig(motion.action) == SET_EXPENDITURE_PAYOUTS + getSig(motion.action) == SET_EXPENDITURE_PAYOUT ) && motion.altTarget == address(0x0) ) { bytes32 structHash = hashExpenditureActionStruct(motion.action); @@ -596,7 +596,7 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs, BasicMetaTrans if (( getSig(motion.action) == SET_EXPENDITURE_STATE || - getSig(motion.action) == SET_EXPENDITURE_PAYOUTS + getSig(motion.action) == SET_EXPENDITURE_PAYOUT ) && getTarget(motion.altTarget) == address(colony) ) { bytes32 structHash = hashExpenditureActionStruct(motion.action); @@ -1044,37 +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) == SET_EXPENDITURE_STATE || - getSig(action) == SET_EXPENDITURE_PAYOUTS - ); + 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)); } } @@ -1091,12 +1098,16 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs, BasicMetaTrans // of 0x20 represents advancing one byte, 4 is the function signature. // So: 0x[length][sig][args...] + 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; // This value is only used if getSig(action) == SET_EXPENDITURE_STATE + uint256 storageSlot; // This value is only used if (sig == SET_EXPENDITURE_STATE) + uint256 expenditureSlot; assembly { permissionDomainId := mload(add(action, 0x24)) @@ -1105,54 +1116,49 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs, BasicMetaTrans storageSlot := mload(add(action, 0x84)) } - // If we are editing the main expenditure struct, or setting payouts on multiple slots - if ( - (getSig(action) == SET_EXPENDITURE_STATE && storageSlot == 25) || - (getSig(action) == SET_EXPENDITURE_PAYOUTS) - ) { - + // If we are editing the main expenditure struct + 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 { - - uint256 expenditureSlot; bytes memory slotClaimDelayAction = new bytes(4 + 32 * 13); // 420 bytes + 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/test/contracts-network/colony-expenditure.js b/test/contracts-network/colony-expenditure.js index d39988e365..b7be77a6eb 100644 --- a/test/contracts-network/colony-expenditure.js +++ b/test/contracts-network/colony-expenditure.js @@ -1286,12 +1286,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 setExpenditurePayouts = colony.methods["setExpenditurePayouts(uint256,uint256,uint256,uint256[],address,uint256[])"]; - await setExpenditurePayouts(1, UINT256_MAX, expenditureId, [0], token.address, [100], { from: ARBITRATOR }); + 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 }); - 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 () => { diff --git a/test/contracts-network/colony-recovery.js b/test/contracts-network/colony-recovery.js index ba5b768026..4a8790917e 100644 --- a/test/contracts-network/colony-recovery.js +++ b/test/contracts-network/colony-recovery.js @@ -237,6 +237,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/extensions/voting-rep.js b/test/extensions/voting-rep.js index e44e7b649e..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"); @@ -711,33 +713,33 @@ contract("Voting Reputation", (accounts) => { expect(expenditureSlot.claimDelay).to.be.zero; }); - it("can update the expenditure globalClaimDelay if voting on expenditure payout states", 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, "setExpenditurePayouts", [1, UINT256_MAX, expenditureId, [0], token.address, [WAD]]); + 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 expenditure; + let expenditureSlot; expenditureMotionCount = await voting.getExpenditureMotionCount(soliditySha3(expenditureId, 0)); expect(expenditureMotionCount).to.be.zero; - expenditure = await colony.getExpenditure(expenditureId); - expect(expenditure.globalClaimDelay).to.be.zero; + expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); + expect(expenditureSlot.claimDelay).to.be.zero; await voting.stakeMotion(motionId, 1, UINT256_MAX, YAY, REQUIRED_STAKE, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); expenditureMotionCount = await voting.getExpenditureMotionCount(soliditySha3(expenditureId, 0)); expect(expenditureMotionCount).to.eq.BN(1); - expenditure = await colony.getExpenditure(expenditureId); - expect(expenditure.globalClaimDelay).to.eq.BN(UINT256_MAX.divn(3)); + expenditureSlot = await colony.getExpenditureSlot(expenditureId, 0); + expect(expenditureSlot.claimDelay).to.eq.BN(UINT256_MAX.divn(3)); await checkErrorRevert(colony.claimExpenditurePayout(expenditureId, 0, token.address), "colony-expenditure-cannot-claim"); @@ -747,8 +749,88 @@ contract("Voting Reputation", (accounts) => { expenditureMotionCount = await voting.getExpenditureMotionCount(soliditySha3(expenditureId, 0)); expect(expenditureMotionCount).to.be.zero; - expenditure = await colony.getExpenditure(expenditureId); - expect(expenditure.globalClaimDelay).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 () => { From 60731d16e23cc15c2a5aeac5abaecee172061cdf Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Wed, 27 Jul 2022 11:01:34 +0100 Subject: [PATCH 53/59] Don't bump version frivolously --- contracts/extensions/FundingQueue.sol | 6 +++--- test/extensions/funding-queue.js | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/contracts/extensions/FundingQueue.sol b/contracts/extensions/FundingQueue.sol index 4ddabfb13a..f70d05644f 100644 --- a/contracts/extensions/FundingQueue.sol +++ b/contracts/extensions/FundingQueue.sol @@ -90,7 +90,7 @@ contract FundingQueue is ColonyExtension, PatriciaTreeProofs, BasicMetaTransacti /// @notice Returns the version of the extension function version() public override pure returns (uint256) { - return 4; + return 3; } /// @notice Configures the extension @@ -141,12 +141,12 @@ contract FundingQueue is ColonyExtension, PatriciaTreeProofs, BasicMetaTransacti require( (domainSkillId == fromSkillId && _fromChildSkillIndex == UINT256_MAX) || fromSkillId == colonyNetwork.getChildSkillId(domainSkillId, _fromChildSkillIndex), - "funding-queue-bad-inheritance-from" + "funding-queue-bad-inheritence-from" ); require( (domainSkillId == toSkillId && _toChildSkillIndex == UINT256_MAX) || toSkillId == colonyNetwork.getChildSkillId(domainSkillId, _toChildSkillIndex), - "funding-queue-bad-inheritance-to" + "funding-queue-bad-inheritence-to" ); proposalCount++; diff --git a/test/extensions/funding-queue.js b/test/extensions/funding-queue.js index d3b59f984f..e45874ab73 100644 --- a/test/extensions/funding-queue.js +++ b/test/extensions/funding-queue.js @@ -254,9 +254,9 @@ contract("Funding Queues", (accounts) => { expect(deprecated).to.equal(true); }); - it("cannot create a basic proposal with bad inheritance", async () => { - await checkErrorRevert(fundingQueue.createProposal(1, 0, 1, 1, 3, WAD, token.address, { from: USER0 }), "funding-queue-bad-inheritance-from"); - await checkErrorRevert(fundingQueue.createProposal(1, 1, 0, 3, 1, WAD, token.address, { from: USER0 }), "funding-queue-bad-inheritance-to"); + it("cannot create a basic proposal with bad inheritence", async () => { + await checkErrorRevert(fundingQueue.createProposal(1, 0, 1, 1, 3, WAD, token.address, { from: USER0 }), "funding-queue-bad-inheritence-from"); + await checkErrorRevert(fundingQueue.createProposal(1, 1, 0, 3, 1, WAD, token.address, { from: USER0 }), "funding-queue-bad-inheritence-to"); }); it("can stake a proposal", async () => { From 7d61d43ab1b5d80f7220dba24b163336ac1f46d1 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Wed, 27 Jul 2022 10:41:41 +0100 Subject: [PATCH 54/59] Remove additional extraneous utility functions --- contracts/colony/Colony.sol | 3 - contracts/colony/ColonyAuthority.sol | 1 - contracts/colony/ColonyExpenditure.sol | 16 ++++- contracts/colony/ColonyFunding.sol | 46 ------------ contracts/colony/IColony.sol | 33 +-------- test/contracts-network/colony-expenditure.js | 14 ++-- test/contracts-network/colony-funding.js | 73 -------------------- test/contracts-network/colony-recovery.js | 13 +--- 8 files changed, 24 insertions(+), 175 deletions(-) diff --git a/contracts/colony/Colony.sol b/contracts/colony/Colony.sol index 344bcec8d1..9659b1dade 100755 --- a/contracts/colony/Colony.sol +++ b/contracts/colony/Colony.sol @@ -314,9 +314,6 @@ contract Colony is BasicMetaTransaction, ColonyStorage, PatriciaTreeProofs { sig = bytes4(keccak256("setExpenditurePayout(uint256,uint256,uint256,uint256,address,uint256)")); colonyAuthority.setRoleCapability(uint8(ColonyRole.Arbitration), address(this), sig, true); - - sig = bytes4(keccak256("moveFundsBetweenPots(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256[],address[])")); - colonyAuthority.setRoleCapability(uint8(ColonyRole.Funding), 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 825ad2dd6c..e62d3b2cf0 100644 --- a/contracts/colony/ColonyAuthority.sol +++ b/contracts/colony/ColonyAuthority.sol @@ -127,7 +127,6 @@ contract ColonyAuthority is CommonAuthority { // Added in colony v10 (ginger-lwss) addRoleCapability(ARBITRATION_ROLE, "setExpenditurePayout(uint256,uint256,uint256,uint256,address,uint256)"); - addRoleCapability(FUNDING_ROLE, "moveFundsBetweenPots(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256[],address[])"); } function addRoleCapability(uint8 role, bytes memory sig) private { diff --git a/contracts/colony/ColonyExpenditure.sol b/contracts/colony/ColonyExpenditure.sol index 88a0526f77..07f3799fed 100644 --- a/contracts/colony/ColonyExpenditure.sol +++ b/contracts/colony/ColonyExpenditure.sol @@ -243,7 +243,7 @@ contract ColonyExpenditure is ColonyStorage { 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) { IColony(address(this)).setExpenditurePayouts(_id, _payoutSlots, _payoutTokens, _payoutValues); } + if (_payoutTokens.length > 0) { setExpenditurePayouts(_id, _payoutTokens, _payoutSlots, _payoutValues); } } // Deprecated @@ -351,6 +351,20 @@ 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++) { + IColony(address(this)).setExpenditurePayouts(_id, _slots[i], _tokens[i], _values[i]); + } + } + 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 fefee80aba..909f5dde52 100755 --- a/contracts/colony/ColonyFunding.sol +++ b/contracts/colony/ColonyFunding.sol @@ -26,33 +26,6 @@ contract ColonyFunding is ColonyStorage { // ignore-swc-123 // Public - function moveFundsBetweenPots( - uint256 _permissionDomainId, - uint256 _childSkillIndex, - uint256 _domainId, - uint256 _fromChildSkillIndex, - uint256 _toChildSkillIndex, - uint256 _fromPot, - uint256 _toPot, - uint256[] memory _amounts, - address[] memory _tokens - ) - public - stoppable - domainNotDeprecated(getDomainFromFundingPot(_toPot)) - authDomain(_permissionDomainId, _childSkillIndex, _domainId) - validFundingTransfer(_fromPot, _toPot) - { - require(validateDomainInheritance(_domainId, _fromChildSkillIndex, getDomainFromFundingPot(_fromPot)), "colony-invalid-domain-inheritance"); - require(validateDomainInheritance(_domainId, _toChildSkillIndex, getDomainFromFundingPot(_toPot)), "colony-invalid-domain-inheritance"); - require(_amounts.length == _tokens.length, "colony-invalid-arguments"); - - for (uint256 i; i < _amounts.length; i++) { - moveFundsBetweenPotsFunctionality(_fromPot, _toPot, _amounts[i], _tokens[i]); - } - } - - /// @notice @deprecated function moveFundsBetweenPots( uint256 _permissionDomainId, uint256 _childSkillIndex, @@ -76,7 +49,6 @@ contract ColonyFunding is ColonyStorage { // ignore-swc-123 moveFundsBetweenPotsFunctionality(_fromPot, _toPot, _amount, _token); } - /// @notice @deprecated function moveFundsBetweenPots( uint256 _permissionDomainId, uint256 _fromChildSkillIndex, @@ -166,24 +138,6 @@ contract ColonyFunding is ColonyStorage { // ignore-swc-123 } } - /// @notice For owners to update payouts with many tokens and many slots - function setExpenditurePayouts( - uint256 _id, - uint256[][] memory _slots, - address[] memory _tokens, - uint256[][] memory _amounts - ) - public - stoppable - expenditureDraft(_id) - expenditureOwnerOrSelf(_id) - { - for (uint256 i; i < _tokens.length; i++) { - setExpenditurePayoutsInternal(_id, _slots[i], _tokens[i], _amounts[i]); - } - } - - /// @notice @deprecated /// @notice For owners to update payouts with one token and many slots function setExpenditurePayouts( uint256 _id, diff --git a/contracts/colony/IColony.sol b/contracts/colony/IColony.sol index ca1bbe1d01..e3a9eaa163 100644 --- a/contracts/colony/IColony.sol +++ b/contracts/colony/IColony.sol @@ -436,7 +436,6 @@ interface IColony is ColonyDataTypes, IRecovery, IBasicMetaTransaction { /// @param _amount Payout amount function setExpenditurePayout(uint256 _id, uint256 _slot, address _token, uint256 _amount) external; - /// @notice @deprecated /// @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 @@ -445,14 +444,6 @@ interface IColony is ColonyDataTypes, IRecovery, IBasicMetaTransaction { /// @param _amounts Payout amounts function setExpenditurePayouts(uint256 _id, uint256[] memory _slots, address _token, uint256[] memory _amounts) 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 2D array of slots to set payouts - /// @param _tokens Array of token addresses, `0x0` value indicates Ether - /// @param _amounts 2D array of payout amounts - function setExpenditurePayouts(uint256 _id, uint256[][] memory _slots, address[] memory _tokens, 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` @@ -469,7 +460,7 @@ interface IColony is ColonyDataTypes, IRecovery, IBasicMetaTransaction { uint256 _amount ) external; - /// @notice Deprecated + /// @notice @deprecated /// @notice Sets the skill on an expenditure slot. Can only be called by expenditure owner. /// @param _id Expenditure identifier /// @param _slot Number of the slot @@ -987,28 +978,6 @@ interface IColony is ColonyDataTypes, IRecovery, IBasicMetaTransaction { /// @return payout Funding pot payout amount function getFundingPotPayout(uint256 _potId, address _token) external view returns (uint256 payout); - /// @notice Move a given amount: `_amount` of `_token` funds from funding pot with id `_fromPot` to one with id `_toPot`. - /// @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 _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 - /// @param _amounts An array of the amounts of funds - /// @param _tokens An array of the addresses of the tokens, `0x0` value indicates Ether - function moveFundsBetweenPots( - uint256 _permissionDomainId, - uint256 _childSkillIndex, - uint256 _domainId, - uint256 _fromChildSkillIndex, - uint256 _toChildSkillIndex, - uint256 _fromPot, - uint256 _toPot, - uint256[] memory _amounts, - address[] memory _tokens - ) external; - /// @notice Move a given amount: `_amount` of `_token` funds from funding pot with id `_fromPot` to one with id `_toPot`. /// @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 diff --git a/test/contracts-network/colony-expenditure.js b/test/contracts-network/colony-expenditure.js index b7be77a6eb..d3c55acadd 100644 --- a/test/contracts-network/colony-expenditure.js +++ b/test/contracts-network/colony-expenditure.js @@ -358,8 +358,7 @@ contract("Colony Expenditure", (accounts) => { }); it("should allow owners to update many slot payouts at once", async () => { - const setExpenditurePayouts = colony.methods["setExpenditurePayouts(uint256,uint256[],address,uint256[])"]; - await setExpenditurePayouts(expenditureId, [SLOT1, SLOT2], token.address, [10, 20], { from: ADMIN }); + await colony.setExpenditurePayouts(expenditureId, [SLOT1, SLOT2], token.address, [10, 20], { from: ADMIN }); let payout; payout = await colony.getExpenditureSlotPayout(expenditureId, SLOT0, token.address); @@ -371,10 +370,8 @@ contract("Colony Expenditure", (accounts) => { }); it("should not allow owners to update many slot payouts with mismatched arguments", async () => { - const setExpenditurePayouts = colony.methods["setExpenditurePayouts(uint256,uint256[],address,uint256[])"]; - await checkErrorRevert( - setExpenditurePayouts(expenditureId, [SLOT0, SLOT1], token.address, [WAD], { from: ADMIN }), + colony.setExpenditurePayouts(expenditureId, [SLOT0, SLOT1], token.address, [WAD], { from: ADMIN }), "colony-expenditure-bad-slots" ); }); @@ -647,9 +644,10 @@ contract("Colony Expenditure", (accounts) => { }); it("should not allow the owner to set payouts", async () => { - const setExpenditurePayouts = colony.methods["setExpenditurePayouts(uint256,uint256[],address,uint256[])"]; - - await checkErrorRevert(setExpenditurePayouts(expenditureId, [SLOT0], token.address, [WAD], { from: ADMIN }), "colony-expenditure-not-draft"); + await checkErrorRevert( + colony.setExpenditurePayouts(expenditureId, [SLOT0], token.address, [WAD], { from: ADMIN }), + "colony-expenditure-not-draft" + ); }); }); diff --git a/test/contracts-network/colony-funding.js b/test/contracts-network/colony-funding.js index d2f163a5a3..fc47e13651 100755 --- a/test/contracts-network/colony-funding.js +++ b/test/contracts-network/colony-funding.js @@ -96,79 +96,6 @@ contract("Colony Funding", (accounts) => { expect(pot2Balance).to.eq.BN(51); }); - it("should let multiple tokens be moved between funding pots at once", async () => { - await fundColonyWithTokens(colony, token, 100); - await fundColonyWithTokens(colony, otherToken, 200); - - await colony.addDomain(1, UINT256_MAX, 1); - - const sig = "moveFundsBetweenPots(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256[],address[])"; - const moveFundsBetweenPots = colony.methods[sig]; - await moveFundsBetweenPots(1, UINT256_MAX, 1, UINT256_MAX, 0, 1, 2, [50, 100], [token.address, otherToken.address]); - - const colonyTokenBalance = await token.balanceOf(colony.address); - const colonyOtherTokenBalance = await otherToken.balanceOf(colony.address); - - const potTokenBalance = await colony.getFundingPotBalance(1, token.address); - const potOtherTokenBalance = await colony.getFundingPotBalance(1, otherToken.address); - - const pot2TokenBalance = await colony.getFundingPotBalance(2, token.address); - const pot2OtherTokenBalance = await colony.getFundingPotBalance(2, otherToken.address); - - expect(colonyTokenBalance).to.eq.BN(100); - expect(colonyOtherTokenBalance).to.eq.BN(200); - expect(potTokenBalance).to.eq.BN(49); - expect(potOtherTokenBalance).to.eq.BN(98); - expect(pot2TokenBalance).to.eq.BN(50); - expect(pot2OtherTokenBalance).to.eq.BN(100); - }); - - it("should not be able to move multiple tokens from a deprecated domain", async () => { - // Create domain 2 and deprecate it - await colony.addDomain(1, UINT256_MAX, 1); - await colony.deprecateDomain(1, 0, 2, true); - - const sig = "moveFundsBetweenPots(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256[],address[])"; - const moveFundsBetweenPots = colony.methods[sig]; - - await checkErrorRevert(moveFundsBetweenPots(1, UINT256_MAX, 1, UINT256_MAX, 0, 1, 2, [], []), "colony-domain-deprecated"); - }); - - it("should not be able to move multiple tokens with invalid funding pots", async () => { - const sig = "moveFundsBetweenPots(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256[],address[])"; - const moveFundsBetweenPots = colony.methods[sig]; - - await checkErrorRevert( - moveFundsBetweenPots(1, UINT256_MAX, 1, UINT256_MAX, 0, 1, 1, [], []), - "colony-funding-cannot-move-funds-between-the-same-pot" - ); - await checkErrorRevert( - moveFundsBetweenPots(1, UINT256_MAX, 1, UINT256_MAX, 0, 0, 1, [], []), - "colony-funding-cannot-move-funds-from-rewards-pot" - ); - }); - - it("should not be able to move multiple tokens with an invalid domain proof", async () => { - const sig = "moveFundsBetweenPots(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256[],address[])"; - const moveFundsBetweenPots = colony.methods[sig]; - - // Create domain 2 - await colony.addDomain(1, UINT256_MAX, 1); - - await checkErrorRevert(moveFundsBetweenPots(1, 0, 1, UINT256_MAX, 0, 1, 2, [], []), "ds-auth-invalid-domain-inheritance"); - await checkErrorRevert(moveFundsBetweenPots(1, UINT256_MAX, 1, 0, 0, 1, 2, [], []), "colony-invalid-domain-inheritance"); - await checkErrorRevert(moveFundsBetweenPots(1, UINT256_MAX, 1, UINT256_MAX, UINT256_MAX, 1, 2, [], []), "colony-invalid-domain-inheritance"); - }); - - it("should not be able to move multiple tokens with invalid arguments", async () => { - await colony.addDomain(1, UINT256_MAX, 1); - - const sig = "moveFundsBetweenPots(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256[],address[])"; - const moveFundsBetweenPots = colony.methods[sig]; - - await checkErrorRevert(moveFundsBetweenPots(1, UINT256_MAX, 1, UINT256_MAX, 0, 1, 2, [10], []), "colony-invalid-arguments"); - }); - it("when moving tokens between pots, should respect permission inheritance", async () => { await removeSubdomainLimit(colonyNetwork); // Temporary for tests until we allow subdomain depth > 1 await fundColonyWithTokens(colony, otherToken, 100); diff --git a/test/contracts-network/colony-recovery.js b/test/contracts-network/colony-recovery.js index 4a8790917e..06f6fa351b 100644 --- a/test/contracts-network/colony-recovery.js +++ b/test/contracts-network/colony-recovery.js @@ -137,14 +137,6 @@ contract("Colony Recovery", (accounts) => { const metaColony = await IMetaColony.at(metaColonyAddress); await metaColony.enterRecoveryMode(); - let sig; - sig = "moveFundsBetweenPots(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256[],address[])"; - const moveFundsBetweenPots1 = metaColony.methods[sig]; - sig = "moveFundsBetweenPots(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,address)"; - const moveFundsBetweenPots2 = metaColony.methods[sig]; - sig = "moveFundsBetweenPots(uint256,uint256,uint256,uint256,uint256,uint256,address)"; - const moveFundsBetweenPots3 = metaColony.methods[sig]; - await checkErrorRevert(colony.initialiseColony(ethers.constants.AddressZero, ethers.constants.AddressZero), "colony-in-recovery-mode"); await checkErrorRevert(colony.mintTokens(1000), "colony-in-recovery-mode"); await checkErrorRevert(metaColony.addGlobalSkill(), "colony-in-recovery-mode"); @@ -228,9 +220,8 @@ contract("Colony Recovery", (accounts) => { await checkErrorRevert(metaColony.claimExpenditurePayout(0, 0, ADDRESS_ZERO), "colony-in-recovery-mode"); await checkErrorRevert(metaColony.claimPayment(0, ADDRESS_ZERO), "colony-in-recovery-mode"); await checkErrorRevert(metaColony.setPaymentPayout(0, 0, 0, ADDRESS_ZERO, 0), "colony-in-recovery-mode"); - await checkErrorRevert(moveFundsBetweenPots1(0, 0, 0, 0, 0, 0, 0, [], []), "colony-in-recovery-mode"); - await checkErrorRevert(moveFundsBetweenPots2(0, 0, 0, 0, 0, 0, 0, 0, ADDRESS_ZERO), "colony-in-recovery-mode"); - await checkErrorRevert(moveFundsBetweenPots3(0, 0, 0, 0, 0, 0, ADDRESS_ZERO), "colony-in-recovery-mode"); + await checkErrorRevert(metaColony.moveFundsBetweenPots(0, 0, 0, 0, 0, 0, 0, 0, ADDRESS_ZERO), "colony-in-recovery-mode"); + await checkErrorRevert(metaColony.moveFundsBetweenPots(0, 0, 0, 0, 0, 0, ADDRESS_ZERO), "colony-in-recovery-mode"); await checkErrorRevert(metaColony.claimColonyFunds(ADDRESS_ZERO), "colony-in-recovery-mode"); await checkErrorRevert(metaColony.startNextRewardPayout(ADDRESS_ZERO, HASHZERO, HASHZERO, 0, []), "colony-in-recovery-mode"); await checkErrorRevert(metaColony.claimRewardPayout(0, [0, 0, 0, 0, 0, 0, 0], HASHZERO, HASHZERO, 0, []), "colony-in-recovery-mode"); From 1e9490f7dd987c2ef6e96d0913dfe54e9f0e85e4 Mon Sep 17 00:00:00 2001 From: Alex Rea Date: Mon, 1 Aug 2022 15:18:51 +0100 Subject: [PATCH 55/59] Quieten smoke tests again --- truffle.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/truffle.js b/truffle.js index c1a704569f..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 = { From 3607f4cb734c5e9045ffcd00af6d4199d4ee50f1 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Mon, 1 Aug 2022 15:51:04 +0100 Subject: [PATCH 56/59] Update smoke tests --- test-smoke/colony-storage-consistent.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test-smoke/colony-storage-consistent.js b/test-smoke/colony-storage-consistent.js index c36a5c18d2..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("0x706e2d895a31bfeba0356c84dec58b011e98570fbe56c8c59a109f6dc38093b8"); - expect(colonyStateHash).to.equal("0x82cdec79eb889c8860763d07f4e7c5ad4cde875c966b1e904d75e951b128315f"); - expect(metaColonyStateHash).to.equal("0x97b3fe37d2143132be6760eda0dcb4cff66a1fc9f31f29377d20664c0b7406ed"); - expect(miningCycleStateHash).to.equal("0x63723c9783b0f0c20e8640fc383ed40dbc228a93cc73a18fa6c6e5f0c38e8694"); - expect(tokenLockingStateHash).to.equal("0xa4dd9734ad2f6b18ec82a5d10be671a5082d6400173d47ac1401580f57cd1638"); + 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"); }); }); }); From fc62b9a1158faeb4eac4c40cf84b9b4a7d226fdf Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Mon, 1 Aug 2022 16:18:48 +0100 Subject: [PATCH 57/59] Remove special burn handling (for now) --- contracts/tokenLocking/TokenLocking.sol | 9 +-------- test/contracts-network/colony-staking.js | 4 ++-- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/contracts/tokenLocking/TokenLocking.sol b/contracts/tokenLocking/TokenLocking.sol index 168988f850..20fdacf9d2 100644 --- a/contracts/tokenLocking/TokenLocking.sol +++ b/contracts/tokenLocking/TokenLocking.sol @@ -195,14 +195,7 @@ contract TokenLocking is TokenLockingStorage, DSMath, BasicMetaTransaction { // Lock storage userLock = userLocks[_token][_user]; userLock.balance = sub(userLock.balance, _amount); - if (_recipient == address(0x0)) { - // If the burn fails, transfer to 0x0 - try ERC20Extended(_token).burn(_amount) {} catch { - require(ERC20Extended(_token).transfer(address(0x0), _amount), "colony-token-locking-burn-failed"); - } - } else { - makeConditionalDeposit(_token, _amount, _recipient); - } + makeConditionalDeposit(_token, _amount, _recipient); emit StakeTransferred(_token, msgSender(), _user, _recipient, _amount); } diff --git a/test/contracts-network/colony-staking.js b/test/contracts-network/colony-staking.js index b919bf06d9..8c1f00f27e 100644 --- a/test/contracts-network/colony-staking.js +++ b/test/contracts-network/colony-staking.js @@ -238,7 +238,7 @@ contract("Colony Staking", (accounts) => { expect(balance).to.eq.BN(WAD); }); - it("should burn the stake if sent to address(0x0) and the token supports burning", async () => { + 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 }); @@ -249,7 +249,7 @@ contract("Colony Staking", (accounts) => { expect(supplyBefore.sub(supplyAfter)).to.eq.BN(WAD); }); - it("should send the stake to address(0x0) if the token does not support burning", async () => { + 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); From 4cff39d5206def06c504b519a3f27f326ab1028a Mon Sep 17 00:00:00 2001 From: Alex Rea Date: Wed, 3 Aug 2022 17:20:17 +0100 Subject: [PATCH 58/59] Delegatecall to preserve msgSender() --- contracts/colony/ColonyExpenditure.sol | 8 ++++- contracts/colony/ColonyFunding.sol | 4 +-- contracts/colony/ColonyStorage.sol | 5 ---- test/contracts-network/colony-expenditure.js | 31 ++++++++++++++++---- 4 files changed, 35 insertions(+), 13 deletions(-) diff --git a/contracts/colony/ColonyExpenditure.sol b/contracts/colony/ColonyExpenditure.sol index 07f3799fed..da51133a48 100644 --- a/contracts/colony/ColonyExpenditure.sol +++ b/contracts/colony/ColonyExpenditure.sol @@ -361,7 +361,13 @@ contract ColonyExpenditure is ColonyStorage { internal { for (uint256 i; i < _tokens.length; i++) { - IColony(address(this)).setExpenditurePayouts(_id, _slots[i], _tokens[i], _values[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)) + } + } } } diff --git a/contracts/colony/ColonyFunding.sol b/contracts/colony/ColonyFunding.sol index 909f5dde52..3dc5d506b0 100755 --- a/contracts/colony/ColonyFunding.sol +++ b/contracts/colony/ColonyFunding.sol @@ -148,7 +148,7 @@ contract ColonyFunding is ColonyStorage { // ignore-swc-123 public stoppable expenditureDraft(_id) - expenditureOwnerOrSelf(_id) + expenditureOnlyOwner(_id) { setExpenditurePayoutsInternal(_id, _slots, _token, _amounts); } @@ -179,7 +179,7 @@ contract ColonyFunding is ColonyStorage { // ignore-swc-123 public stoppable expenditureDraft(_id) - expenditureOwnerOrSelf(_id) + expenditureOnlyOwner(_id) { uint256[] memory slots = new uint256[](1); slots[0] = _slot; diff --git a/contracts/colony/ColonyStorage.sol b/contracts/colony/ColonyStorage.sol index 204d2db8bd..db6f8540f3 100755 --- a/contracts/colony/ColonyStorage.sol +++ b/contracts/colony/ColonyStorage.sol @@ -196,11 +196,6 @@ contract ColonyStorage is ColonyDataTypes, ColonyNetworkDataTypes, DSMath, Commo _; } - modifier expenditureOwnerOrSelf(uint256 _id) { - require(expenditures[_id].owner == msgSender() || address(this) == msgSender(), "colony-expenditure-not-owner-or-self"); - _; - } - modifier expenditureOnlyOwner(uint256 _id) { require(expenditures[_id].owner == msgSender(), "colony-expenditure-not-owner"); _; diff --git a/test/contracts-network/colony-expenditure.js b/test/contracts-network/colony-expenditure.js index d3c55acadd..9cfe2de1b8 100644 --- a/test/contracts-network/colony-expenditure.js +++ b/test/contracts-network/colony-expenditure.js @@ -388,7 +388,7 @@ contract("Colony Expenditure", (accounts) => { it("should not allow non-owners to update skills or payouts", async () => { await checkErrorRevert(colony.setExpenditureSkill(expenditureId, SLOT0, GLOBAL_SKILL_ID), "colony-expenditure-not-owner"); - await checkErrorRevert(colony.setExpenditurePayout(expenditureId, SLOT0, token.address, WAD), "colony-expenditure-not-owner-or-self"); + await checkErrorRevert(colony.setExpenditurePayout(expenditureId, SLOT0, token.address, WAD), "colony-expenditure-not-owner"); }); it("should allow owners to add a slot payout", async () => { @@ -450,10 +450,7 @@ contract("Colony Expenditure", (accounts) => { }); 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-or-self" - ); + 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 () => { @@ -514,6 +511,30 @@ contract("Colony Expenditure", (accounts) => { 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 }), From b79d9b1ce4983b797903cc6ea0ad73e8e7070ea8 Mon Sep 17 00:00:00 2001 From: Alex Rea Date: Thu, 4 Aug 2022 15:13:30 +0100 Subject: [PATCH 59/59] Put abi encoding on own line for clarity --- contracts/colony/ColonyExpenditure.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/contracts/colony/ColonyExpenditure.sol b/contracts/colony/ColonyExpenditure.sol index da51133a48..fa1ca15fab 100644 --- a/contracts/colony/ColonyExpenditure.sol +++ b/contracts/colony/ColonyExpenditure.sol @@ -361,7 +361,9 @@ contract ColonyExpenditure is ColonyStorage { 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])); + (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 {