Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cancelExpenditureViaArbitration #1223

Merged
merged 2 commits into from
Mar 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 4 additions & 46 deletions contracts/colony/Colony.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -303,57 +303,15 @@ 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);
}
}
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) {
Expand Down
4 changes: 4 additions & 0 deletions contracts/colony/ColonyAuthority.sol
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,10 @@ 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 {
Expand Down
34 changes: 34 additions & 0 deletions contracts/colony/ColonyExpenditure.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -107,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) {
Expand Down
21 changes: 21 additions & 0 deletions contracts/colony/IColony.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -477,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.
kronosapiens marked this conversation as resolved.
Show resolved Hide resolved
/// @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
Expand Down
28 changes: 28 additions & 0 deletions docs/interfaces/icolony.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -335,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.
Expand Down
2 changes: 1 addition & 1 deletion helpers/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
28 changes: 27 additions & 1 deletion scripts/deployOldUpgradeableVersion.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {};
Expand Down Expand Up @@ -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(
"",
Expand All @@ -79,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];
}
}
Expand Down Expand Up @@ -244,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(
Expand Down
5 changes: 2 additions & 3 deletions test-gas-costs/gasCosts.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
10 changes: 5 additions & 5 deletions test-smoke/colony-storage-consistent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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("0x7b43f3e7e6cda0d4828db085a635f3bfa5513595d3048b835eac558070b8980f");
expect(colonyStateHash).to.equal("0x3fd9f27a6b7e09500e5ec9314027a47477d03d01b4a2f5c305cd98c74205c647");
expect(metaColonyStateHash).to.equal("0x87a14b838f1db5f0bd5a883cfad2f1ef124cc822ea4c9a124531b54676843864");
expect(miningCycleStateHash).to.equal("0xd59299ca385c8d9795a56de6dcaea40048712832669421091e132db492ee84bc");
expect(tokenLockingStateHash).to.equal("0x871a5dedede31530886db450e3aaec934d643989910a7c225ded0127cecd65e9");
});
});
});
43 changes: 43 additions & 0 deletions test/contracts-network/colony-expenditure.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const {
downgradeColony,
deployColonyNetworkVersionGLWSS4,
downgradeColonyNetwork,
deployColonyVersionHMWSS,
} = require("../../scripts/deployOldUpgradeableVersion");

const { expect } = chai;
Expand Down Expand Up @@ -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");
});
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand All @@ -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");
Expand Down Expand Up @@ -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");
});
Expand Down Expand Up @@ -752,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", () => {
Expand Down
Loading