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");