From 2a71b4205f3ffdb57d1c09d936e8828aa9d6fe20 Mon Sep 17 00:00:00 2001 From: Alex Rea Date: Wed, 6 Mar 2024 15:47:43 +0000 Subject: [PATCH 1/2] cancelExpenditureViaArbitration --- contracts/colony/Colony.sol | 49 +------------------- contracts/colony/ColonyAuthority.sol | 2 + contracts/colony/ColonyExpenditure.sol | 15 ++++++ contracts/colony/IColony.sol | 11 +++++ docs/interfaces/icolony.md | 14 ++++++ helpers/constants.js | 2 +- scripts/deployOldUpgradeableVersion.js | 13 ++++++ test-smoke/colony-storage-consistent.js | 10 ++-- test/contracts-network/colony-expenditure.js | 27 +++++++++++ test/contracts-network/colony.js | 7 ++- test/contracts-network/meta-colony.js | 3 ++ test/extensions/one-tx-payment.js | 5 ++ 12 files changed, 104 insertions(+), 54 deletions(-) diff --git a/contracts/colony/Colony.sol b/contracts/colony/Colony.sol index de649d66e5..ba3f1ac9a2 100755 --- a/contracts/colony/Colony.sol +++ b/contracts/colony/Colony.sol @@ -34,7 +34,7 @@ contract Colony is BasicMetaTransaction, Multicall, ColonyStorage, PatriciaTreeP // This function, exactly as defined, is used in build scripts. Take care when updating. // Version number should be upped with every change in Colony or its dependency contracts or libraries. // prettier-ignore - function version() public pure returns (uint256 colonyVersion) { return 14; } + function version() public pure returns (uint256 colonyVersion) { return 15; } function getColonyNetwork() public view returns (address) { return colonyNetworkAddress; @@ -303,57 +303,12 @@ contract Colony is BasicMetaTransaction, Multicall, ColonyStorage, PatriciaTreeP emit ColonyUpgraded(msgSender(), currentVersion, _newVersion); } - // v11 to v12 function finishUpgrade() public always { ColonyAuthority colonyAuthority = ColonyAuthority(address(authority)); bytes4 sig; - sig = bytes4(keccak256("makeArbitraryTransactions(address[],bytes[],bool)")); - colonyAuthority.setRoleCapability(uint8(ColonyRole.Root), address(this), sig, true); - - sig = bytes4(keccak256("setDefaultGlobalClaimDelay(uint256)")); - colonyAuthority.setRoleCapability(uint8(ColonyRole.Root), address(this), sig, true); - - sig = bytes4(keccak256("setExpenditureMetadata(uint256,uint256,uint256,string)")); + sig = bytes4(keccak256("cancelExpenditureViaArbitration(uint256,uint256,uint256)")); colonyAuthority.setRoleCapability(uint8(ColonyRole.Arbitration), address(this), sig, true); - - // Remove Task & Payment functions - sig = bytes4(keccak256("makeTask(uint256,uint256,bytes32,uint256,uint256,uint256)")); - colonyAuthority.setRoleCapability(uint8(ColonyRole.Administration), address(this), sig, false); - sig = bytes4(keccak256("addPayment(uint256,uint256,address,address,uint256,uint256,uint256)")); - colonyAuthority.setRoleCapability(uint8(ColonyRole.Administration), address(this), sig, false); - sig = bytes4(keccak256("setPaymentRecipient(uint256,uint256,uint256,address)")); - colonyAuthority.setRoleCapability(uint8(ColonyRole.Administration), address(this), sig, false); - sig = bytes4(keccak256("setPaymentSkill(uint256,uint256,uint256,uint256)")); - colonyAuthority.setRoleCapability(uint8(ColonyRole.Administration), address(this), sig, false); - sig = bytes4(keccak256("setPaymentPayout(uint256,uint256,uint256,address,uint256)")); - colonyAuthority.setRoleCapability(uint8(ColonyRole.Administration), address(this), sig, false); - sig = bytes4(keccak256("finalizePayment(uint256,uint256,uint256)")); - colonyAuthority.setRoleCapability(uint8(ColonyRole.Administration), address(this), sig, false); - - // Remove Global Skills functions - sig = bytes4(keccak256("addGlobalSkill()")); - colonyAuthority.setRoleCapability(uint8(ColonyRole.Root), address(this), sig, false); - sig = bytes4(keccak256("deprecateGlobalSkill(uint256)")); - colonyAuthority.setRoleCapability(uint8(ColonyRole.Root), address(this), sig, false); - - // If OneTxPayment extension is installed, add the new role and upgrade it - bytes32 ONE_TX_PAYMENT = keccak256("OneTxPayment"); - IColonyNetwork network = IColonyNetwork(colonyNetworkAddress); - address oneTxPaymentAddress = network.getExtensionInstallation(ONE_TX_PAYMENT, address(this)); - - if (oneTxPaymentAddress != address(0x0)) { - uint256 installedVersion = ColonyExtension(oneTxPaymentAddress).version(); - require(installedVersion >= 5, "colony-upgrade-one-tx-payment-to-6"); - if (installedVersion == 5) { - // If installed in root, add arbitration permission - if (colonyAuthority.hasUserRole(oneTxPaymentAddress, 1, uint8(ColonyRole.Administration))) { - colonyAuthority.setUserRole(oneTxPaymentAddress, 1, uint8(ColonyRole.Arbitration), true); - } - // Upgrade extension - network.upgradeExtension(ONE_TX_PAYMENT, 6); - } - } } function getMetatransactionNonce(address _user) public view override returns (uint256 nonce) { diff --git a/contracts/colony/ColonyAuthority.sol b/contracts/colony/ColonyAuthority.sol index 04d7567be0..7e52d1735d 100644 --- a/contracts/colony/ColonyAuthority.sol +++ b/contracts/colony/ColonyAuthority.sol @@ -128,6 +128,8 @@ contract ColonyAuthority is CommonAuthority { // Added in colony v10 (ginger-lwss) addRoleCapability(ARBITRATION_ROLE, "setExpenditurePayout(uint256,uint256,uint256,uint256,address,uint256)"); + + addRoleCapability(ARBITRATION_ROLE, "cancelExpenditureViaArbitration(uint256,uint256,uint256)"); } function addRoleCapability(uint8 role, bytes memory sig) private { diff --git a/contracts/colony/ColonyExpenditure.sol b/contracts/colony/ColonyExpenditure.sol index cfb7e4a91d..62845065ca 100644 --- a/contracts/colony/ColonyExpenditure.sol +++ b/contracts/colony/ColonyExpenditure.sol @@ -91,6 +91,21 @@ contract ColonyExpenditure is ColonyStorage { emit ExpenditureTransferred(msgSender(), _id, _newOwner); } + function cancelExpenditureViaArbitration( + uint256 _permissionDomainId, + uint256 _childSkillIndex, + uint256 _id + ) + public + stoppable + expenditureDraftOrLocked(_id) + authDomain(_permissionDomainId, _childSkillIndex, expenditures[_id].domainId) + { + expenditures[_id].status = ExpenditureStatus.Cancelled; + + emit ExpenditureCancelled(msgSender(), _id); + } + function cancelExpenditure( uint256 _id ) public stoppable expenditureDraft(_id) expenditureOnlyOwner(_id) { diff --git a/contracts/colony/IColony.sol b/contracts/colony/IColony.sol index 6f81f9f579..163a342771 100644 --- a/contracts/colony/IColony.sol +++ b/contracts/colony/IColony.sol @@ -465,6 +465,17 @@ interface IColony is ColonyDataTypes, IRecovery, IBasicMetaTransaction, IMultica address _newOwner ) external; + /// @notice Cancels the expenditure and prevents further editing. + /// @param _permissionDomainId The domainId in which I have the permission to take this action + /// @param _childSkillIndex The index that the `_domainId` is relative to `_permissionDomainId`, + /// (only used if `_permissionDomainId` is different to `_domainId`) + /// @param _id Expenditure identifier + function cancelExpenditureViaArbitration( + uint256 _permissionDomainId, + uint256 _childSkillIndex, + uint256 _id + ) external; + /// @notice Cancels the expenditure and prevents further editing. Can only be called by expenditure owner. /// @param _id Expenditure identifier function cancelExpenditure(uint256 _id) external; diff --git a/docs/interfaces/icolony.md b/docs/interfaces/icolony.md index 67737b1f67..4cb42437ae 100644 --- a/docs/interfaces/icolony.md +++ b/docs/interfaces/icolony.md @@ -128,6 +128,20 @@ Cancels the expenditure and prevents further editing. Can only be called by expe |_id|uint256|Expenditure identifier +### ▸ `cancelExpenditureViaArbitration(uint256 _permissionDomainId, uint256 _childSkillIndex, uint256 _id)` + +Cancels the expenditure and prevents further editing. + + +**Parameters** + +|Name|Type|Description| +|---|---|---| +|_permissionDomainId|uint256|The domainId in which I have the permission to take this action +|_childSkillIndex|uint256|The index that the `_domainId` is relative to `_permissionDomainId`, (only used if `_permissionDomainId` is different to `_domainId`) +|_id|uint256|Expenditure identifier + + ### ▸ `claimColonyFunds(address _token)` Move any funds received by the colony in `_token` denomination to the top-level domain pot, siphoning off a small amount to the reward pot. If called against a colony's own token, no fee is taken. diff --git a/helpers/constants.js b/helpers/constants.js index 37916a3299..80b8496db5 100644 --- a/helpers/constants.js +++ b/helpers/constants.js @@ -13,7 +13,7 @@ const INT256_MIN = new BN(2).pow(new BN(255)).mul(new BN(-1)); const INT128_MAX = new BN(2).pow(new BN(127)).sub(new BN(1)); const INT128_MIN = new BN(2).pow(new BN(127)).mul(new BN(-1)); -const CURR_VERSION = 14; +const CURR_VERSION = 15; const RECOVERY_ROLE = 0; const ROOT_ROLE = 1; diff --git a/scripts/deployOldUpgradeableVersion.js b/scripts/deployOldUpgradeableVersion.js index f90a8df011..92478a8d97 100644 --- a/scripts/deployOldUpgradeableVersion.js +++ b/scripts/deployOldUpgradeableVersion.js @@ -59,6 +59,19 @@ module.exports.deployColonyVersionGLWSS4 = (colonyNetwork) => { ); }; +module.exports.deployColonyVersionHMWSS = (colonyNetwork) => { + return module.exports.deployOldColonyVersion( + "Colony", + "IMetaColony", + [ + // eslint-disable-next-line max-len + "Colony,ColonyDomains,ColonyExpenditure,ColonyFunding,ColonyRewards,ColonyRoles,ContractRecovery,ColonyArbitraryTransaction", + ], + "hmwss", + colonyNetwork, + ); +}; + module.exports.deployColonyNetworkVersionGLWSS4 = () => { return module.exports.deployOldColonyNetworkVersion( "", diff --git a/test-smoke/colony-storage-consistent.js b/test-smoke/colony-storage-consistent.js index cba210ccea..a8d3d2a2bc 100644 --- a/test-smoke/colony-storage-consistent.js +++ b/test-smoke/colony-storage-consistent.js @@ -155,11 +155,11 @@ contract("Contract Storage", (accounts) => { console.log("miningCycleStateHash:", miningCycleStateHash); console.log("tokenLockingStateHash:", tokenLockingStateHash); - expect(colonyNetworkStateHash).to.equal("0x1cb8d09f4e7af91f61bf655aebb2677d5dd43dc4b493ccfb90f025c2c23a1406"); - expect(colonyStateHash).to.equal("0x2de353bd8d9b4785c51026b335708068760968512ccbfeff2441f182dc9e3c22"); - expect(metaColonyStateHash).to.equal("0xba451b41b29bc477b8b53f057b9252c6daedeab4ee917b5ba00d46f6d2919bfc"); - expect(miningCycleStateHash).to.equal("0xf453858c03397af01668d54e6031e5a5594cff94c8fdb618c44899cd24cb1856"); - expect(tokenLockingStateHash).to.equal("0xb0e53da184faa87011a47b92e99d95f22afcc2feba5aa009be659242df09df63"); + expect(colonyNetworkStateHash).to.equal("0x8ddc8d3b55aa9dcc332854cfdc05d470306b1352cd1c7cb463149b263e23000e"); + expect(colonyStateHash).to.equal("0x545133a18e7ed2a90179ea3661bcca3817b4d545bddbce1dad2eb7a3e1c66111"); + expect(metaColonyStateHash).to.equal("0x6c1447525a40a2d3fabea2a758043a52c9d44ee4fdf1e65f956810bdcc19e0cf"); + expect(miningCycleStateHash).to.equal("0x5f04f203ae1ca038a9e86f46cadb22f9a5f75d70732b4af7415ef627bfe153e9"); + expect(tokenLockingStateHash).to.equal("0x06cb0760dd2c02417a7577013c119523e123aeb2bbc8343d278d2b94fd2652ce"); }); }); }); diff --git a/test/contracts-network/colony-expenditure.js b/test/contracts-network/colony-expenditure.js index de9410fe65..cc9c8b09f9 100644 --- a/test/contracts-network/colony-expenditure.js +++ b/test/contracts-network/colony-expenditure.js @@ -14,6 +14,7 @@ const { downgradeColony, deployColonyNetworkVersionGLWSS4, downgradeColonyNetwork, + deployColonyVersionHMWSS, } = require("../../scripts/deployOldUpgradeableVersion"); const { expect } = chai; @@ -146,6 +147,20 @@ contract("Colony Expenditure", (accounts) => { expect(expenditure.owner).to.equal(USER); }); + it("should allow arbitration users to cancel expenditures", async () => { + await colony.makeExpenditure(1, UINT256_MAX, 1, { from: ADMIN }); + const expenditureId = await colony.getExpenditureCount(); + + let expenditure = await colony.getExpenditure(expenditureId); + expect(expenditure.status).to.eq.BN(DRAFT); + + await checkErrorRevert(colony.cancelExpenditureViaArbitration(1, UINT256_MAX, expenditureId, { from: ADMIN }), "ds-auth-unauthorized"); + await colony.cancelExpenditureViaArbitration(1, UINT256_MAX, expenditureId, { from: ARBITRATOR }); + + expenditure = await colony.getExpenditure(expenditureId); + expect(expenditure.status).to.eq.BN(CANCELLED); + }); + it("a non-root user cannot setDefaultGlobalClaimDelay", async () => { await checkErrorRevert(colony.setDefaultGlobalClaimDelay(0, { from: ADMIN }), "ds-auth-unauthorized"); }); @@ -179,6 +194,7 @@ contract("Colony Expenditure", (accounts) => { it("should error if the expenditure does not exist", async () => { await checkErrorRevert(colony.setExpenditureSkills(100, [SLOT0], [localSkillId]), "colony-expenditure-does-not-exist"); await checkErrorRevert(colony.transferExpenditure(100, USER), "colony-expenditure-does-not-exist"); + await checkErrorRevert(colony.cancelExpenditure(100, { from: ARBITRATOR }), "colony-expenditure-does-not-exist"); await checkErrorRevert( colony.transferExpenditureViaArbitration(0, UINT256_MAX, 100, USER, { from: ARBITRATOR }), "colony-expenditure-does-not-exist", @@ -301,6 +317,7 @@ contract("Colony Expenditure", (accounts) => { it("should not allow owners to set a (now defunct) global skill, either deprecated or undeprecated", async () => { const { OldInterface } = await deployColonyVersionGLWSS4(colonyNetwork); + await deployColonyVersionHMWSS(colonyNetwork); await downgradeColony(colonyNetwork, metaColony, "glwss4"); // Make the colonyNetwork the old version @@ -322,6 +339,7 @@ contract("Colony Expenditure", (accounts) => { // Upgrade to current version await colonyNetworkAsEtherRouter.setResolver(latestResolver); await metaColony.upgrade(14); + await metaColony.upgrade(15); await checkErrorRevert(colony.setExpenditureSkill(expenditureId, SLOT0, globalSkillId, { from: ADMIN }), "colony-not-valid-local-skill"); await checkErrorRevert(colony.setExpenditureSkill(expenditureId, SLOT0, globalSkillId2, { from: ADMIN }), "colony-not-valid-local-skill"); @@ -460,6 +478,15 @@ contract("Colony Expenditure", (accounts) => { await checkErrorRevert(colony.setExpenditurePayout(expenditureId, SLOT0, token.address, WAD, { from: ADMIN }), "colony-expenditure-not-draft"); }); + it("should not allow arbitration to cancel an expenditure that has been finalized", async () => { + await colony.finalizeExpenditure(expenditureId, { from: ADMIN }); + + await checkErrorRevert( + colony.cancelExpenditureViaArbitration(1, UINT256_MAX, expenditureId, { from: ARBITRATOR }), + "colony-expenditure-not-draft-or-locked", + ); + }); + 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"); }); diff --git a/test/contracts-network/colony.js b/test/contracts-network/colony.js index cc0c51c1d6..754a55558c 100755 --- a/test/contracts-network/colony.js +++ b/test/contracts-network/colony.js @@ -13,7 +13,7 @@ const { fundColonyWithTokens, setupColony, } = require("../../helpers/test-data-generator"); -const { deployColonyVersionGLWSS4 } = require("../../scripts/deployOldUpgradeableVersion"); +const { deployColonyVersionGLWSS4, deployColonyVersionHMWSS } = require("../../scripts/deployOldUpgradeableVersion"); const { expect } = chai; chai.use(bnChai(web3.utils.BN)); @@ -448,6 +448,7 @@ contract("Colony", (accounts) => { let oldColony; before(async () => { ({ OldInterface } = await deployColonyVersionGLWSS4(colonyNetwork)); + await deployColonyVersionHMWSS(colonyNetwork); }); beforeEach(async () => { @@ -465,6 +466,7 @@ contract("Colony", (accounts) => { it("should be able to query for a task", async () => { await oldColony.makeTask(1, UINT256_MAX, SPECIFICATION_HASH, 1, localSkillId, 0, { from: USER0 }); await colony.upgrade(14); + await colony.upgrade(15); const taskId = await colony.getTaskCount(); const task = await colony.getTask(taskId); @@ -486,6 +488,7 @@ contract("Colony", (accounts) => { it("should be able to query for a payment", async () => { await oldColony.addPayment(1, UINT256_MAX, USER1, token.address, WAD, 1, localSkillId, { from: USER0 }); await colony.upgrade(14); + await colony.upgrade(15); const paymentId = await colony.getPaymentCount(); const payment = await colony.getPayment(paymentId); @@ -506,6 +509,7 @@ contract("Colony", (accounts) => { // Move funds into task funding pot await colony.moveFundsBetweenPots(1, UINT256_MAX, 1, UINT256_MAX, UINT256_MAX, 1, fundingPotId, WAD, token.address); await colony.upgrade(14); + await colony.upgrade(15); // Move funds back await colony.moveFundsBetweenPots(1, UINT256_MAX, 1, UINT256_MAX, UINT256_MAX, fundingPotId, 1, WAD, token.address); }); @@ -522,6 +526,7 @@ contract("Colony", (accounts) => { // Move funds into task funding pot await colony.moveFundsBetweenPots(1, UINT256_MAX, 1, UINT256_MAX, UINT256_MAX, 1, fundingPotId, WAD, token.address); await colony.upgrade(14); + await colony.upgrade(15); // Move funds back await colony.moveFundsBetweenPots(1, UINT256_MAX, 1, UINT256_MAX, UINT256_MAX, fundingPotId, 1, WAD, token.address); }); diff --git a/test/contracts-network/meta-colony.js b/test/contracts-network/meta-colony.js index ff50781139..bdebf6d869 100644 --- a/test/contracts-network/meta-colony.js +++ b/test/contracts-network/meta-colony.js @@ -12,6 +12,7 @@ const { downgradeColonyNetwork, deployColonyVersionGLWSS4, deployColonyNetworkVersionGLWSS4, + deployColonyVersionHMWSS, } = require("../../scripts/deployOldUpgradeableVersion"); const IMetaColony = artifacts.require("IMetaColony"); @@ -421,6 +422,7 @@ contract("Meta Colony", (accounts) => { let globalSkillId; beforeEach(async () => { const { OldInterface } = await deployColonyVersionGLWSS4(colonyNetwork); + await deployColonyVersionHMWSS(colonyNetwork); await downgradeColony(colonyNetwork, metaColony, "glwss4"); // Make the colonyNetwork the old version @@ -439,6 +441,7 @@ contract("Meta Colony", (accounts) => { // Upgrade to current version await colonyNetworkAsEtherRouter.setResolver(latestResolver); await metaColony.upgrade(14); + await metaColony.upgrade(15); }); describe("when getting a skill", () => { diff --git a/test/extensions/one-tx-payment.js b/test/extensions/one-tx-payment.js index 022db86bb7..2275f9c4d7 100644 --- a/test/extensions/one-tx-payment.js +++ b/test/extensions/one-tx-payment.js @@ -34,6 +34,7 @@ const { downgradeColonyNetwork, deployColonyVersionGLWSS4, deployColonyNetworkVersionGLWSS4, + deployColonyVersionHMWSS, } = require("../../scripts/deployOldUpgradeableVersion"); contract("One transaction payments", (accounts) => { @@ -260,6 +261,7 @@ contract("One transaction payments", (accounts) => { it("should not allow an admin to specify a global skill (which is now removed functionality), either deprecated or undeprecated", async () => { const { OldInterface } = await deployColonyVersionGLWSS4(colonyNetwork); + await deployColonyVersionHMWSS(colonyNetwork); await downgradeColony(colonyNetwork, metaColony, "glwss4"); // Make the colonyNetwork the old version @@ -281,6 +283,7 @@ contract("One transaction payments", (accounts) => { // Upgrade to current version await colonyNetworkAsEtherRouter.setResolver(latestResolver); await metaColony.upgrade(14); + await metaColony.upgrade(15); await checkErrorRevert( oneTxPayment.makePaymentFundedFromDomain(1, UINT256_MAX, 1, UINT256_MAX, [USER1], [token.address], [10], 1, globalSkillId), @@ -557,6 +560,8 @@ contract("One transaction payments", (accounts) => { // V5 is `glwss4`, await deployOldExtensionVersion("OneTxPayment", "OneTxPayment", ["OneTxPayment"], "glwss4", colonyNetwork); await deployColonyNetworkVersionGLWSS4(); + await deployColonyVersionGLWSS4(colonyNetwork); + await deployColonyVersionHMWSS(colonyNetwork); }); beforeEach(async () => { From da632624307b5094b07b390472b0bb12ee1bb5cc Mon Sep 17 00:00:00 2001 From: Alex Rea Date: Sat, 9 Mar 2024 07:55:25 +0100 Subject: [PATCH 2/2] finalizeExpenditureViaArbitration --- contracts/colony/Colony.sol | 3 +++ contracts/colony/ColonyAuthority.sol | 2 ++ contracts/colony/ColonyExpenditure.sol | 19 +++++++++++++++++++ contracts/colony/IColony.sol | 10 ++++++++++ docs/interfaces/icolony.md | 14 ++++++++++++++ scripts/deployOldUpgradeableVersion.js | 15 ++++++++++++++- test-gas-costs/gasCosts.js | 5 ++--- test-smoke/colony-storage-consistent.js | 10 +++++----- test/contracts-network/colony-expenditure.js | 16 ++++++++++++++++ test/contracts-network/colony-recovery.js | 1 + 10 files changed, 86 insertions(+), 9 deletions(-) diff --git a/contracts/colony/Colony.sol b/contracts/colony/Colony.sol index ba3f1ac9a2..f84ff1286b 100755 --- a/contracts/colony/Colony.sol +++ b/contracts/colony/Colony.sol @@ -309,6 +309,9 @@ contract Colony is BasicMetaTransaction, Multicall, ColonyStorage, PatriciaTreeP sig = bytes4(keccak256("cancelExpenditureViaArbitration(uint256,uint256,uint256)")); colonyAuthority.setRoleCapability(uint8(ColonyRole.Arbitration), address(this), sig, true); + + sig = bytes4(keccak256("finalizeExpenditureViaArbitration(uint256,uint256,uint256)")); + colonyAuthority.setRoleCapability(uint8(ColonyRole.Arbitration), address(this), sig, true); } function getMetatransactionNonce(address _user) public view override returns (uint256 nonce) { diff --git a/contracts/colony/ColonyAuthority.sol b/contracts/colony/ColonyAuthority.sol index 7e52d1735d..e5d1e44f01 100644 --- a/contracts/colony/ColonyAuthority.sol +++ b/contracts/colony/ColonyAuthority.sol @@ -129,7 +129,9 @@ contract ColonyAuthority is CommonAuthority { // Added in colony v10 (ginger-lwss) addRoleCapability(ARBITRATION_ROLE, "setExpenditurePayout(uint256,uint256,uint256,uint256,address,uint256)"); + // Added in colony v15 (hazel-lwss-2) addRoleCapability(ARBITRATION_ROLE, "cancelExpenditureViaArbitration(uint256,uint256,uint256)"); + addRoleCapability(ARBITRATION_ROLE, "finalizeExpenditureViaArbitration(uint256,uint256,uint256)"); } function addRoleCapability(uint8 role, bytes memory sig) private { diff --git a/contracts/colony/ColonyExpenditure.sol b/contracts/colony/ColonyExpenditure.sol index 62845065ca..2f90a60dcf 100644 --- a/contracts/colony/ColonyExpenditure.sol +++ b/contracts/colony/ColonyExpenditure.sol @@ -122,6 +122,25 @@ contract ColonyExpenditure is ColonyStorage { emit ExpenditureLocked(msgSender(), _id); } + function finalizeExpenditureViaArbitration( + uint256 _permissionDomainId, + uint256 _childSkillIndex, + uint256 _id + ) + public + stoppable + expenditureDraftOrLocked(_id) + authDomain(_permissionDomainId, _childSkillIndex, expenditures[_id].domainId) + { + FundingPot storage fundingPot = fundingPots[expenditures[_id].fundingPotId]; + require(fundingPot.payoutsWeCannotMake == 0, "colony-expenditure-not-funded"); + + expenditures[_id].status = ExpenditureStatus.Finalized; + expenditures[_id].finalizedTimestamp = block.timestamp; + + emit ExpenditureFinalized(msgSender(), _id); + } + function finalizeExpenditure( uint256 _id ) public stoppable expenditureDraftOrLocked(_id) expenditureOnlyOwner(_id) { diff --git a/contracts/colony/IColony.sol b/contracts/colony/IColony.sol index 163a342771..ee773c6c77 100644 --- a/contracts/colony/IColony.sol +++ b/contracts/colony/IColony.sol @@ -488,6 +488,16 @@ interface IColony is ColonyDataTypes, IRecovery, IBasicMetaTransaction, IMultica /// @param _id Expenditure identifier function finalizeExpenditure(uint256 _id) external; + /// @notice Finalizes the expenditure and allows for funds to be claimed. Can only be called by expenditure owner. + /// @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 Expenditure identifier + function finalizeExpenditureViaArbitration( + uint256 _permissionDomainId, + uint256 _childSkillIndex, + 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 diff --git a/docs/interfaces/icolony.md b/docs/interfaces/icolony.md index 4cb42437ae..2ec7579107 100644 --- a/docs/interfaces/icolony.md +++ b/docs/interfaces/icolony.md @@ -349,6 +349,20 @@ Finalizes the expenditure and allows for funds to be claimed. Can only be called |_id|uint256|Expenditure identifier +### ▸ `finalizeExpenditureViaArbitration(uint256 _permissionDomainId, uint256 _childSkillIndex, uint256 _id)` + +Finalizes the expenditure and allows for funds to be claimed. Can only be called by expenditure owner. + + +**Parameters** + +|Name|Type|Description| +|---|---|---| +|_permissionDomainId|uint256|The domainId in which I have the permission to take this action +|_childSkillIndex|uint256|The index that the `_domainId` is relative to `_permissionDomainId`, +|_id|uint256|Expenditure identifier + + ### ▸ `finalizeRewardPayout(uint256 _payoutId)` Finalises the reward payout. Allows creation of next reward payouts for token that has been used in `_payoutId`. Can only be called when reward payout cycle is finished i.e when 60 days have passed from its creation. diff --git a/scripts/deployOldUpgradeableVersion.js b/scripts/deployOldUpgradeableVersion.js index 92478a8d97..eb7bc1e50e 100644 --- a/scripts/deployOldUpgradeableVersion.js +++ b/scripts/deployOldUpgradeableVersion.js @@ -8,7 +8,7 @@ const Promise = require("bluebird"); const exec = Promise.promisify(require("child_process").exec); const contract = require("@truffle/contract"); const { getColonyEditable, getColonyNetworkEditable, web3GetCode } = require("../helpers/test-helper"); -const { ROOT_ROLE, RECOVERY_ROLE, ADMINISTRATION_ROLE, ARCHITECTURE_ROLE } = require("../helpers/constants"); +const { ROOT_ROLE, RECOVERY_ROLE, ADMINISTRATION_ROLE, ARCHITECTURE_ROLE, ADDRESS_ZERO } = require("../helpers/constants"); const colonyDeployed = {}; const colonyNetworkDeployed = {}; @@ -92,7 +92,19 @@ module.exports.deployOldColonyVersion = async (contractName, interfaceName, impl // Already deployed... if truffle's not snapshotted it away. See if there's any code there. const { resolverAddress } = colonyDeployed[interfaceName][versionTag]; const code = await web3GetCode(resolverAddress); + console.log(versionTag, "code", code); if (code !== "0x") { + // Could be a different colony network. Check it's registered with the network. + const resolver = await artifacts.require("Resolver").at(resolverAddress); + const versionImplementationAddress = await resolver.lookup(web3.utils.soliditySha3("version()").slice(0, 10)); + const versionImplementation = await artifacts.require("IMetaColony").at(versionImplementationAddress); + const version = await versionImplementation.version(); + const registeredResolverAddress = await colonyNetwork.getColonyVersionResolver(version); + if (registeredResolverAddress === ADDRESS_ZERO) { + const metaColonyAddress = await colonyNetwork.getMetaColony(); + const metaColony = await artifacts.require("IMetaColony").at(metaColonyAddress); + await metaColony.addNetworkColonyVersion(version, resolverAddress); + } return colonyDeployed[interfaceName][versionTag]; } } @@ -257,6 +269,7 @@ module.exports.deployOldUpgradeableVersion = async (contractName, interfaceName, const network = process.env.SOLIDITY_COVERAGE ? "coverage" : "development"; let res; + console.log("deploying"); try { res = await exec( diff --git a/test-gas-costs/gasCosts.js b/test-gas-costs/gasCosts.js index a99ccb3072..ca28b8b4c0 100644 --- a/test-gas-costs/gasCosts.js +++ b/test-gas-costs/gasCosts.js @@ -130,9 +130,8 @@ contract("All", function (accounts) { // 1 tx payment to one recipient, with skill await oneTxExtension.makePayment(1, UINT256_MAX, 1, UINT256_MAX, [WORKER], [token.address], [10], 1, localSkillId); - const firstToken = token.address < otherToken.address ? token.address : otherToken.address; - const secondToken = token.address < otherToken.address ? otherToken.address : token.address; - + const firstToken = token.address.toLowerCase() < otherToken.address.toLowerCase() ? token.address : otherToken.address; + const secondToken = token.address.toLowerCase() < otherToken.address.toLowerCase() ? otherToken.address : token.address; // 1 tx payment to one recipient, two tokens await oneTxExtension.makePayment(1, UINT256_MAX, 1, UINT256_MAX, [WORKER, WORKER], [firstToken, secondToken], [10, 10], 1, 0); diff --git a/test-smoke/colony-storage-consistent.js b/test-smoke/colony-storage-consistent.js index a8d3d2a2bc..6334bcfb93 100644 --- a/test-smoke/colony-storage-consistent.js +++ b/test-smoke/colony-storage-consistent.js @@ -155,11 +155,11 @@ contract("Contract Storage", (accounts) => { console.log("miningCycleStateHash:", miningCycleStateHash); console.log("tokenLockingStateHash:", tokenLockingStateHash); - expect(colonyNetworkStateHash).to.equal("0x8ddc8d3b55aa9dcc332854cfdc05d470306b1352cd1c7cb463149b263e23000e"); - expect(colonyStateHash).to.equal("0x545133a18e7ed2a90179ea3661bcca3817b4d545bddbce1dad2eb7a3e1c66111"); - expect(metaColonyStateHash).to.equal("0x6c1447525a40a2d3fabea2a758043a52c9d44ee4fdf1e65f956810bdcc19e0cf"); - expect(miningCycleStateHash).to.equal("0x5f04f203ae1ca038a9e86f46cadb22f9a5f75d70732b4af7415ef627bfe153e9"); - expect(tokenLockingStateHash).to.equal("0x06cb0760dd2c02417a7577013c119523e123aeb2bbc8343d278d2b94fd2652ce"); + expect(colonyNetworkStateHash).to.equal("0x7b43f3e7e6cda0d4828db085a635f3bfa5513595d3048b835eac558070b8980f"); + expect(colonyStateHash).to.equal("0x3fd9f27a6b7e09500e5ec9314027a47477d03d01b4a2f5c305cd98c74205c647"); + expect(metaColonyStateHash).to.equal("0x87a14b838f1db5f0bd5a883cfad2f1ef124cc822ea4c9a124531b54676843864"); + expect(miningCycleStateHash).to.equal("0xd59299ca385c8d9795a56de6dcaea40048712832669421091e132db492ee84bc"); + expect(tokenLockingStateHash).to.equal("0x871a5dedede31530886db450e3aaec934d643989910a7c225ded0127cecd65e9"); }); }); }); diff --git a/test/contracts-network/colony-expenditure.js b/test/contracts-network/colony-expenditure.js index cc9c8b09f9..999d7ae44f 100644 --- a/test/contracts-network/colony-expenditure.js +++ b/test/contracts-network/colony-expenditure.js @@ -779,6 +779,22 @@ contract("Colony Expenditure", (accounts) => { await colony.finalizeExpenditure(expenditureId, { from: ADMIN }); }); + + it("should allow non-owners with arbitration permission to finalise expenditures, but not repeatedly", async () => { + let expenditure = await colony.getExpenditure(expenditureId); + expect(expenditure.owner).to.equal(ADMIN); + + await checkErrorRevert(colony.finalizeExpenditureViaArbitration(1, UINT256_MAX, expenditureId, { from: ADMIN }), "ds-auth-unauthorized"); + await colony.finalizeExpenditureViaArbitration(1, UINT256_MAX, expenditureId, { from: ARBITRATOR }); + + expenditure = await colony.getExpenditure(expenditureId); + expect(expenditure.status).to.eq.BN(FINALIZED); + + await checkErrorRevert( + colony.finalizeExpenditureViaArbitration(1, UINT256_MAX, expenditureId, { from: ARBITRATOR }), + "colony-expenditure-not-draft-or-locked", + ); + }); }); describe("when claiming expenditures", () => { diff --git a/test/contracts-network/colony-recovery.js b/test/contracts-network/colony-recovery.js index eaa5f4eb47..33274f0cea 100644 --- a/test/contracts-network/colony-recovery.js +++ b/test/contracts-network/colony-recovery.js @@ -176,6 +176,7 @@ contract("Colony Recovery", (accounts) => { await checkErrorRevert(metaColony.cancelExpenditure(0), "colony-in-recovery-mode"); await checkErrorRevert(metaColony.lockExpenditure(0), "colony-in-recovery-mode"); await checkErrorRevert(metaColony.finalizeExpenditure(0), "colony-in-recovery-mode"); + await checkErrorRevert(metaColony.finalizeExpenditureViaArbitration(0, 0, 0), "colony-in-recovery-mode"); await checkErrorRevert(metaColony.setExpenditureMetadata(0, ""), "colony-in-recovery-mode"); await checkErrorRevert(metaColony.setExpenditureMetadata(0, 0, 0, ""), "colony-in-recovery-mode"); await checkErrorRevert(metaColony.setExpenditureRecipients(0, [], []), "colony-in-recovery-mode");