diff --git a/contracts/colony/Colony.sol b/contracts/colony/Colony.sol index bd08d08ee6..53e12aa608 100755 --- a/contracts/colony/Colony.sol +++ b/contracts/colony/Colony.sol @@ -117,6 +117,25 @@ contract Colony is ColonyStorage, PatriciaTreeProofs { return token; } + function emitDomainReputationPenalty( + uint256 _permissionDomainId, + uint256 _childSkillIndex, + uint256 _domainId, + address _user, + int256 _amount + ) public stoppable authDomain(_permissionDomainId, _childSkillIndex, _domainId) + { + require(_amount <= 0, "colony-penalty-cannot-be-positive"); + IColonyNetwork(colonyNetworkAddress).appendReputationUpdateLog(_user, _amount, domains[_domainId].skillId); + } + + function emitSkillReputationPenalty(uint256 _skillId, address _user, int256 _amount) + public stoppable auth validGlobalSkill(_skillId) + { + require(_amount <= 0, "colony-penalty-cannot-be-positive"); + IColonyNetwork(colonyNetworkAddress).appendReputationUpdateLog(_user, _amount, _skillId); + } + function initialiseColony(address _colonyNetworkAddress, address _token) public stoppable { require(colonyNetworkAddress == address(0x0), "colony-already-initialised-network"); require(token == address(0x0), "colony-already-initialised-token"); @@ -302,14 +321,17 @@ contract Colony is ColonyStorage, PatriciaTreeProofs { // v4 to v5 function finishUpgrade() public always { ColonyAuthority colonyAuthority = ColonyAuthority(address(authority)); + bytes4 sig; - // Add stake management functionality - colonyAuthority.setRoleCapability( - uint8(ColonyRole.Arbitration), - address(this), - bytes4(keccak256("transferStake(uint256,uint256,address,address,uint256,uint256,address)")), - true - ); + // Add stake management functionality (colonyNetwork#757) + sig = bytes4(keccak256("transferStake(uint256,uint256,address,address,uint256,uint256,address)")); + colonyAuthority.setRoleCapability(uint8(ColonyRole.Arbitration), address(this), sig, true); + + // Add reputation penalty functionality (colonyNetwork#845) + sig = bytes4(keccak256("emitDomainReputationPenalty(uint256,uint256,uint256,address,int256)")); + colonyAuthority.setRoleCapability(uint8(ColonyRole.Arbitration), address(this), sig, true); + sig = bytes4(keccak256("emitSkillReputationPenalty(uint256,address,int256)")); + colonyAuthority.setRoleCapability(uint8(ColonyRole.Arbitration), address(this), sig, true); } function checkNotAdditionalProtectedVariable(uint256 _slot) public view recovery { diff --git a/contracts/colony/ColonyAuthority.sol b/contracts/colony/ColonyAuthority.sol index c260dabd19..b2cc095e58 100644 --- a/contracts/colony/ColonyAuthority.sol +++ b/contracts/colony/ColonyAuthority.sol @@ -85,8 +85,10 @@ contract ColonyAuthority is CommonAuthority { addRoleCapability(ARBITRATION_ROLE, "setExpenditurePayoutModifier(uint256,uint256,uint256,uint256,int256)"); addRoleCapability(ARBITRATION_ROLE, "setExpenditureClaimDelay(uint256,uint256,uint256,uint256,uint256)"); - // Added in colony v5 + // Added in colony v5 (current version) addRoleCapability(ARBITRATION_ROLE, "transferStake(uint256,uint256,address,address,uint256,uint256,address)"); + addRoleCapability(ARBITRATION_ROLE, "emitDomainReputationPenalty(uint256,uint256,uint256,address,int256)"); + addRoleCapability(ARBITRATION_ROLE, "emitSkillReputationPenalty(uint256,address,int256)"); } function addRoleCapability(uint8 role, bytes memory sig) private { diff --git a/contracts/colony/IColony.sol b/contracts/colony/IColony.sol index b953ef0c41..3378dd2860 100644 --- a/contracts/colony/IColony.sol +++ b/contracts/colony/IColony.sol @@ -127,6 +127,26 @@ contract IColony is ColonyDataTypes, IRecovery { /// @return roles bytes32 representation of the roles function getUserRoles(address who, uint256 where) public view returns (bytes32 roles); + /// @notice Emit a negative domain reputation update. Available only to Arbitration role holders + /// @param _permissionDomainId The domainId in which I hold the Arbitration role + /// @param _childSkillIndex The index that the `_domainId` is relative to `_permissionDomainId` + /// @param _domainId The domain where the user will lose reputation + /// @param _user The user who will lose reputation + /// @param _amount The (negative) amount of reputation to lose + function emitDomainReputationPenalty( + uint256 _permissionDomainId, + uint256 _childSkillIndex, + uint256 _domainId, + address _user, + int256 _amount + ) public; + + /// @notice Emit a negative skill reputation update. Available only to Arbitration role holders in the root domain + /// @param _skillId The skill where the user will lose reputation + /// @param _user The user who will lose reputation + /// @param _amount The (negative) amount of reputation to lose + function emitSkillReputationPenalty(uint256 _skillId, address _user, int256 _amount) public; + /// @notice Called once when the colony is created to initialise certain storage slot values. /// @dev Sets the reward inverse to the uint max 2**256 - 1. /// @param _colonyNetworkAddress Address of the colony network diff --git a/docs/_Interface_IColony.md b/docs/_Interface_IColony.md index c3177e3e98..c2a60f342c 100644 --- a/docs/_Interface_IColony.md +++ b/docs/_Interface_IColony.md @@ -206,6 +206,36 @@ Deobligate the user some amount of tokens, releasing the stake. |_amount|uint256|Amount of internal token we are deobligating. +### `emitDomainReputationPenalty` + +Emit a negative domain reputation update. Available only to Arbitration role holders + + +**Parameters** + +|Name|Type|Description| +|---|---|---| +|_permissionDomainId|uint256|The domainId in which I hold the Arbitration role +|_childSkillIndex|uint256|The index that the `_domainId` is relative to `_permissionDomainId` +|_domainId|uint256|The domain where the user will lose reputation +|_user|address|The user who will lose reputation +|_amount|int256|The (negative) amount of reputation to lose + + +### `emitSkillReputationPenalty` + +Emit a negative skill reputation update. Available only to Arbitration role holders in the root domain + + +**Parameters** + +|Name|Type|Description| +|---|---|---| +|_skillId|uint256|The skill where the user will lose reputation +|_user|address|The user who will lose reputation +|_amount|int256|The (negative) amount of reputation to lose + + ### `executeTaskChange` Executes a task update transaction `_data` which is approved and signed by two of its roles (e.g. manager and worker) using the detached signatures for these users. diff --git a/test/contracts-network/colony-permissions.js b/test/contracts-network/colony-permissions.js index 0d689d18ad..73f9bb1343 100644 --- a/test/contracts-network/colony-permissions.js +++ b/test/contracts-network/colony-permissions.js @@ -12,6 +12,7 @@ import { ADMINISTRATION_ROLE, INITIAL_FUNDING, SPECIFICATION_HASH, + GLOBAL_SKILL_ID, } from "../../helpers/constants"; import { fundColonyWithTokens, makeTask, setupRandomColony } from "../../helpers/test-data-generator"; @@ -278,6 +279,20 @@ contract("ColonyPermissions", (accounts) => { await colony.setAdministrationRole(1, 1, USER2, 3, true, { from: USER1 }); }); + it("should allow users with arbitration permission to emit negative reputation penalties", async () => { + await colony.setArbitrationRole(1, UINT256_MAX, USER1, 1, true); + await colony.setArbitrationRole(1, 0, USER2, 2, true); + + // Domain penalties + await colony.emitDomainReputationPenalty(1, 1, 3, USER2, -100, { from: USER1 }); + await checkErrorRevert(colony.emitDomainReputationPenalty(1, 1, 3, USER2, 100, { from: USER1 }), "colony-penalty-cannot-be-positive"); + + // Skill penalties (root domain only) + await colony.emitSkillReputationPenalty(GLOBAL_SKILL_ID, USER2, -100, { from: USER1 }); + await checkErrorRevert(colony.emitSkillReputationPenalty(GLOBAL_SKILL_ID, USER2, 100, { from: USER1 }), "colony-penalty-cannot-be-positive"); + await checkErrorRevert(colony.emitSkillReputationPenalty(GLOBAL_SKILL_ID, USER2, -100, { from: USER2 }), "ds-auth-unauthorized"); + }); + it("should allow permissions to propagate to subdomains", async () => { // Give User 2 funding permissions in domain 1 await colony.setFundingRole(1, UINT256_MAX, USER2, 1, true);