From e4d622cfe6939f6c3aed38c7cada2dc8630743ad Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Tue, 29 Oct 2019 15:12:39 +0200 Subject: [PATCH 1/4] Add arbitration penalties --- contracts/Colony.sol | 28 ++++++++++++++++++ contracts/ColonyAuthority.sol | 2 ++ contracts/IColony.sol | 22 ++++++++++++++ docs/_Interface_IColony.md | 31 ++++++++++++++++++++ test/contracts-network/colony-permissions.js | 16 +++++++++- 5 files changed, 98 insertions(+), 1 deletion(-) diff --git a/contracts/Colony.sol b/contracts/Colony.sol index 44529f228e..8899fddf20 100755 --- a/contracts/Colony.sol +++ b/contracts/Colony.sol @@ -119,6 +119,26 @@ 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 _permissionDomainId, uint256 _skillId, address _user, int256 _amount) + public stoppable validGlobalSkill(_skillId) + { + require(_amount <= 0, "colony-penalty-cannot-be-positive"); + require(isAuthorized(msg.sender, _permissionDomainId, msg.sig), "ds-auth-unauthorized"); + 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"); @@ -336,6 +356,10 @@ contract Colony is ColonyStorage, PatriciaTreeProofs { bytes4 constant SIG5 = bytes4(keccak256("setExpenditurePayoutModifier(uint256,uint256,uint256,uint256,int256)")); bytes4 constant SIG6 = bytes4(keccak256("setExpenditureClaimDelay(uint256,uint256,uint256,uint256,uint256)")); + // Introducing arbitration penalties + bytes4 constant SIG7 = bytes4(keccak256("emitDomainReputationPenalty(uint256,uint256,uint256,address,int256)")); + bytes4 constant SIG8 = bytes4(keccak256("emitSkillReputationPenalty(uint256,uint256,address,int256)")); + // v3 to v4 function finishUpgrade() public always { // Remove payment/task mutability from multisig @@ -350,6 +374,10 @@ contract Colony is ColonyStorage, PatriciaTreeProofs { colonyAuthority.setRoleCapability(uint8(ColonyRole.Arbitration), address(this), SIG4, true); colonyAuthority.setRoleCapability(uint8(ColonyRole.Arbitration), address(this), SIG5, true); colonyAuthority.setRoleCapability(uint8(ColonyRole.Arbitration), address(this), SIG6, true); + + // Add arbitration penalty capabilities + colonyAuthority.setRoleCapability(uint8(ColonyRole.Arbitration), address(this), SIG7, true); + colonyAuthority.setRoleCapability(uint8(ColonyRole.Arbitration), address(this), SIG8, true); } function checkNotAdditionalProtectedVariable(uint256 _slot) public view recovery { diff --git a/contracts/ColonyAuthority.sol b/contracts/ColonyAuthority.sol index 317851e92f..9b27837f1e 100644 --- a/contracts/ColonyAuthority.sol +++ b/contracts/ColonyAuthority.sol @@ -85,6 +85,8 @@ contract ColonyAuthority is CommonAuthority { addRoleCapability(ARBITRATION_ROLE, "transferExpenditure(uint256,uint256,uint256,address)"); addRoleCapability(ARBITRATION_ROLE, "setExpenditurePayoutModifier(uint256,uint256,uint256,uint256,int256)"); addRoleCapability(ARBITRATION_ROLE, "setExpenditureClaimDelay(uint256,uint256,uint256,uint256,uint256)"); + addRoleCapability(ARBITRATION_ROLE, "emitDomainReputationPenalty(uint256,uint256,uint256,address,int256)"); + addRoleCapability(ARBITRATION_ROLE, "emitSkillReputationPenalty(uint256,uint256,address,int256)"); } function addRoleCapability(uint8 role, bytes memory sig) private { diff --git a/contracts/IColony.sol b/contracts/IColony.sol index d92d25e932..1ab634ace3 100644 --- a/contracts/IColony.sol +++ b/contracts/IColony.sol @@ -132,6 +132,28 @@ 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`, + /// (only used if `_permissionDomainId` is different to `_domainId`) + /// @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. + /// @param _permissionDomainId The domainId in which I hold the Arbitration role. + /// @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 _permissionDomainId, 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 3cfdc18722..9d9c4cd347 100644 --- a/docs/_Interface_IColony.md +++ b/docs/_Interface_IColony.md @@ -178,6 +178,37 @@ Mark a task as complete after the due date has passed. This allows the task to b |_id|uint256|Id of the task +### `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`, (only used if `_permissionDomainId` is different to `_domainId`) +|_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. + + +**Parameters** + +|Name|Type|Description| +|---|---|---| +|_permissionDomainId|uint256|The domainId in which I hold the Arbitration role. +|_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 24bc1917c6..ae6ff2de31 100644 --- a/test/contracts-network/colony-permissions.js +++ b/test/contracts-network/colony-permissions.js @@ -11,7 +11,8 @@ import { FUNDING_ROLE, ADMINISTRATION_ROLE, INITIAL_FUNDING, - SPECIFICATION_HASH + SPECIFICATION_HASH, + GLOBAL_SKILL_ID } from "../../helpers/constants"; import { fundColonyWithTokens, makeTask, setupRandomColony } from "../../helpers/test-data-generator"; @@ -280,6 +281,19 @@ 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, 0, USER1, 1, 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 + await colony.emitSkillReputationPenalty(1, GLOBAL_SKILL_ID, USER2, -100, { from: USER1 }); + await checkErrorRevert(colony.emitSkillReputationPenalty(1, GLOBAL_SKILL_ID, USER2, 100, { from: USER1 }), "colony-penalty-cannot-be-positive"); + await checkErrorRevert(colony.emitSkillReputationPenalty(2, GLOBAL_SKILL_ID, USER2, -100, { from: USER1 }), "ds-auth-unauthorized"); + }); + it("should allow permissions to propagate to subdomains", async () => { // Give User 2 funding permissions in domain 1 await colony.setFundingRole(1, 0, USER2, 1, true); From 7364b0e492a2c61ff3bd191be6be8165ff1aac7b Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Tue, 29 Oct 2019 15:16:44 +0200 Subject: [PATCH 2/4] Add tasks extension --- contracts/extensions/Tasks.sol | 682 ++++++++ contracts/extensions/TasksFactory.sol | 44 + helpers/task-review-signing.js | 37 +- helpers/test-data-generator.js | 30 +- scripts/check-recovery.js | 2 + scripts/check-storage.js | 2 + test/extensions/tasks.js | 2108 +++++++++++++++++++++++++ 7 files changed, 2877 insertions(+), 28 deletions(-) create mode 100644 contracts/extensions/Tasks.sol create mode 100644 contracts/extensions/TasksFactory.sol create mode 100644 test/extensions/tasks.js diff --git a/contracts/extensions/Tasks.sol b/contracts/extensions/Tasks.sol new file mode 100644 index 0000000000..a9a74afc1e --- /dev/null +++ b/contracts/extensions/Tasks.sol @@ -0,0 +1,682 @@ +/* + 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.5.8; +pragma experimental ABIEncoderV2; + +import "../../lib/dappsys/math.sol"; +import "./../ColonyDataTypes.sol"; +import "./../ColonyAuthority.sol"; +import "./../IColony.sol"; +import "./../IColonyNetwork.sol"; + + +contract Tasks is DSMath { + uint256 constant RATING_COMMIT_TIMEOUT = 5 days; + uint256 constant RATING_REVEAL_TIMEOUT = 5 days; + + /// @notice Event logged when a new task is added + /// @param taskId The newly added task id + event TaskAdded(uint256 taskId); + + /// @notice Event logged when a task's specification hash changes + /// @param taskId Id of the task + /// @param specificationHash New specification hash of the task + event TaskBriefSet(uint256 indexed taskId, bytes32 specificationHash); + + /// @notice Event logged when a task's due date changes + /// @param taskId Id of the task + /// @param dueDate New due date of the task + event TaskDueDateSet(uint256 indexed taskId, uint256 dueDate); + + /// @notice Event logged when a deliverable has been submitted for a task + /// @param taskId Id of the task + /// @param deliverableHash Hash of the work performed + event TaskDeliverableSubmitted(uint256 indexed taskId, bytes32 deliverableHash); + + /// @notice Event logged when a task has been completed. This is either because the dueDate has passed + /// and the manager closed the task, or the worker has submitted the deliverable. In the + /// latter case, TaskDeliverableSubmitted will also be emitted. + event TaskCompleted(uint256 indexed taskId); + + /// @notice Event logged when the rating of a role was revealed + /// @param taskId Id of the task + /// @param role Role that got rated + /// @param rating Rating the role received + event TaskWorkRatingRevealed(uint256 indexed taskId, TaskRole indexed role, uint8 rating); + + enum TaskRatings { None, Unsatisfactory, Satisfactory, Excellent } + enum TaskRole { Manager, Evaluator, Worker } + + struct Task { + uint256 expenditureId; + bytes32 specificationHash; + bytes32 deliverableHash; + uint256 dueDate; + uint256 completionTimestamp; + uint256 changeNonce; + } + + struct Role { + bool rateFail; + TaskRatings rating; + } + + struct RatingSecrets { + uint256 count; + uint256 timestamp; + mapping (uint8 => bytes32) secret; + } + + uint256 taskCount; + mapping (uint256 => Task) tasks; + mapping (uint256 => mapping (uint8 => Role)) taskRoles; + mapping (uint256 => RatingSecrets) ratingSecrets; + + // Role assignment functions require special type of sign-off. + // This keeps track of which functions are related to role assignment + mapping (bytes4 => bool) roleAssignmentSigs; + // Mapping function signature to 2 task roles whose approval is needed to execute + mapping (bytes4 => TaskRole[2]) reviewers; + + IColony colony; + + constructor(address _colony) public { + colony = IColony(_colony); + + roleAssignmentSigs[bytes4(keccak256("setTaskManagerRole(uint256,address,uint256,uint256)"))] = true; + roleAssignmentSigs[bytes4(keccak256("setTaskEvaluatorRole(uint256,address)"))] = true; + roleAssignmentSigs[bytes4(keccak256("setTaskWorkerRole(uint256,address)"))] = true; + + // Initialise the task update reviewers + reviewers[bytes4(keccak256("setTaskBrief(uint256,bytes32)"))] = [TaskRole.Manager, TaskRole.Worker]; + reviewers[bytes4(keccak256("setTaskDueDate(uint256,uint256)"))] = [TaskRole.Manager, TaskRole.Worker]; + reviewers[bytes4(keccak256("setTaskSkill(uint256,uint256)"))] = [TaskRole.Manager, TaskRole.Worker]; + // We are setting a manager to both reviewers, but it will require just one signature from manager + reviewers[bytes4(keccak256("setTaskManagerPayout(uint256,address,uint256)"))] = [TaskRole.Manager, TaskRole.Manager]; + reviewers[bytes4(keccak256("setTaskEvaluatorPayout(uint256,address,uint256)"))] = [TaskRole.Manager, TaskRole.Evaluator]; + reviewers[bytes4(keccak256("setTaskWorkerPayout(uint256,address,uint256)"))] = [TaskRole.Manager, TaskRole.Worker]; + reviewers[bytes4(keccak256("removeTaskEvaluatorRole(uint256)"))] = [TaskRole.Manager, TaskRole.Evaluator]; + reviewers[bytes4(keccak256("removeTaskWorkerRole(uint256)"))] = [TaskRole.Manager, TaskRole.Worker]; + reviewers[bytes4(keccak256("cancelTask(uint256)"))] = [TaskRole.Manager, TaskRole.Worker]; + } + + modifier self() { + require(address(this) == msg.sender, "colony-task-not-self"); + _; + } + + modifier isAdmin(address _user, uint256 _permissionDomainId, uint256 _childSkillIndex, uint256 _domainId) { + ColonyDataTypes.ColonyRole admin = ColonyDataTypes.ColonyRole.Administration; + require(colony.hasInheritedUserRole(_user, _permissionDomainId, admin, _childSkillIndex, _domainId), "colony-task-not-admin"); + _; + } + + modifier taskExists(uint256 _id) { + require(_id > 0 && _id <= taskCount, "colony-task-does-not-exist"); + _; + } + + modifier taskComplete(uint256 _id) { + require(tasks[_id].completionTimestamp > 0, "colony-task-not-complete"); + _; + } + + modifier taskNotComplete(uint256 _id) { + require(tasks[_id].completionTimestamp == 0, "colony-task-complete"); + _; + } + + modifier confirmTaskRoleIdentity(uint256 _id, address _user, TaskRole _role) { + require(msg.sender == getTaskRoleUser(_id, _role), "colony-task-role-identity-mismatch"); + _; + } + + function executeTaskChange( + uint8[] memory _sigV, + bytes32[] memory _sigR, + bytes32[] memory _sigS, + uint8[] memory _mode, + uint256 _value, + bytes memory _data + ) + public + { + require(_value == 0, "colony-task-change-non-zero-value"); + require(_sigR.length == _sigS.length && _sigR.length == _sigV.length, "colony-task-change-signatures-count-do-not-match"); + + bytes4 sig; + uint256 taskId; + (sig, taskId) = deconstructCall(_data); + require(taskId > 0 && taskId <= taskCount, "colony-task-does-not-exist"); + require(!roleAssignmentSigs[sig], "colony-task-change-is-role-assignment"); + + ColonyDataTypes.ExpenditureStatus status = colony.getExpenditure(tasks[taskId].expenditureId).status; + require(status != ColonyDataTypes.ExpenditureStatus.Finalized, "colony-task-finalized"); + + uint8 nSignaturesRequired; + address taskRole1User = getTaskRoleUser(taskId, TaskRole(reviewers[sig][0])); + address taskRole2User = getTaskRoleUser(taskId, TaskRole(reviewers[sig][1])); + if (taskRole1User == address(0) || taskRole2User == address(0)) { + // When one of the roles is not set, allow the other one to execute a change with just their signature + nSignaturesRequired = 1; + } else if (taskRole1User == taskRole2User) { + // We support roles being assumed by the same user, in this case, allow them to execute a change with just their signature + nSignaturesRequired = 1; + } else { + nSignaturesRequired = 2; + } + require(_sigR.length == nSignaturesRequired, "colony-task-change-does-not-meet-signatures-required"); + + bytes32 msgHash = keccak256(abi.encodePacked(address(this), address(this), _value, _data, tasks[taskId].changeNonce)); + address[] memory reviewerAddresses = getReviewerAddresses(_sigV, _sigR, _sigS, _mode, msgHash); + + require( + reviewerAddresses[0] == taskRole1User || reviewerAddresses[0] == taskRole2User, + "colony-task-signatures-do-not-match-reviewer-1" + ); + + if (nSignaturesRequired == 2) { + require(reviewerAddresses[0] != reviewerAddresses[1], "colony-task-duplicate-reviewers"); + require( + reviewerAddresses[1] == taskRole1User || reviewerAddresses[1] == taskRole2User, + "colony-task-signatures-do-not-match-reviewer-2" + ); + } + + tasks[taskId].changeNonce += 1; + require(executeCall(address(this), _value, _data), "colony-task-change-execution-failed"); + } + + function executeTaskRoleAssignment( + uint8[] memory _sigV, + bytes32[] memory _sigR, + bytes32[] memory _sigS, + uint8[] memory _mode, + uint256 _value, + bytes memory _data + ) + public + { + require(_value == 0, "colony-task-role-assignment-non-zero-value"); + require(_sigR.length == _sigS.length && _sigR.length == _sigV.length, "colony-task-role-assignment-signatures-count-do-not-match"); + + bytes4 sig; + uint256 taskId; + address userAddress; + (sig, taskId, userAddress) = deconstructRoleChangeCall(_data); + require(taskId > 0 && taskId <= taskCount, "colony-task-does-not-exist"); + require(roleAssignmentSigs[sig], "colony-task-change-is-not-role-assignment"); + + ColonyDataTypes.ExpenditureStatus status = colony.getExpenditure(tasks[taskId].expenditureId).status; + require(status != ColonyDataTypes.ExpenditureStatus.Finalized, "colony-task-finalized"); + + uint8 nSignaturesRequired; + address manager = getTaskRoleUser(taskId, TaskRole.Manager); + // If manager wants to set himself to a role + if (userAddress == manager) { + nSignaturesRequired = 1; + } else { + nSignaturesRequired = 2; + } + require(_sigR.length == nSignaturesRequired, "colony-task-role-assignment-does-not-meet-required-signatures"); + + bytes32 msgHash = keccak256(abi.encodePacked(address(this), address(this), _value, _data, tasks[taskId].changeNonce)); + address[] memory reviewerAddresses = getReviewerAddresses(_sigV, _sigR, _sigS, _mode, msgHash); + + if (nSignaturesRequired == 1) { + // Since we want to set a manager as an evaluator, require just manager's signature + require(reviewerAddresses[0] == manager, "colony-task-role-assignment-not-signed-by-manager"); + } else { + // One of signers must be a manager + require( + reviewerAddresses[0] == manager || + reviewerAddresses[1] == manager, + "colony-task-role-assignment-not-signed-by-manager" + ); + + // One of the signers must be an address we want to set here + require( + userAddress == reviewerAddresses[0] || userAddress == reviewerAddresses[1], + "colony-task-role-assignment-not-signed-by-new-user-for-role" + ); + // Require that signatures are not from the same address + // This will never throw, because we require that manager is one of the signers, + // and if manager is both signers, then `userAddress` must also be a manager, and if + // `userAddress` is a manager, then we require 1 signature (will be kept for possible future changes) + require(reviewerAddresses[0] != reviewerAddresses[1], "colony-task-role-assignment-duplicate-signatures"); + } + + tasks[taskId].changeNonce += 1; + require(executeCall(address(this), _value, _data), "colony-task-role-assignment-execution-failed"); + } + + // Permissions pertain to the Administration role here + function makeTask( + uint256 _permissionDomainId, + uint256 _childSkillIndex, + uint256 _callerPermissionDomainId, + uint256 _callerChildSkillIndex, + bytes32 _specificationHash, + uint256 _domainId, + uint256 _skillId, + uint256 _dueDate + ) + public + isAdmin(msg.sender, _callerPermissionDomainId, _callerChildSkillIndex, _domainId) + { + uint256 expenditureId = colony.makeExpenditure(_permissionDomainId, _childSkillIndex, _domainId); + + taskCount += 1; + tasks[taskCount].expenditureId = expenditureId; + tasks[taskCount].specificationHash = _specificationHash; + tasks[taskCount].dueDate = (_dueDate > 0) ? _dueDate : now + 90 days; // Note: can set dueDate in past? + + setTaskRoleUser(taskCount, TaskRole.Manager, msg.sender); + setTaskRoleUser(taskCount, TaskRole.Evaluator, msg.sender); + + if (_skillId > 0) { + this.setTaskSkill(taskCount, _skillId); + } + + emit TaskAdded(taskCount); + emit TaskDueDateSet(taskCount, tasks[taskCount].dueDate); + } + + function submitTaskWorkRating(uint256 _id, TaskRole _role, bytes32 _secret) + public + taskComplete(_id) + { + if (_role == TaskRole.Manager) { // Manager rated by worker + require(msg.sender == getTaskRoleUser(_id, TaskRole.Worker), "colony-user-cannot-rate-task-manager"); + } else if (_role == TaskRole.Worker) { // Worker rated by evaluator + require(msg.sender == getTaskRoleUser(_id, TaskRole.Evaluator), "colony-user-cannot-rate-task-worker"); + } else { + revert("colony-unsupported-role-to-rate"); + } + + require(sub(now, tasks[_id].completionTimestamp) <= RATING_COMMIT_TIMEOUT, "colony-task-rating-secret-submit-period-closed"); + require(ratingSecrets[_id].secret[uint8(_role)] == "", "colony-task-rating-secret-already-exists"); + + ratingSecrets[_id].count += 1; + ratingSecrets[_id].timestamp = now; + ratingSecrets[_id].secret[uint8(_role)] = _secret; + } + + function revealTaskWorkRating(uint256 _id, TaskRole _role, uint8 _rating, bytes32 _salt) + public + { + assert(ratingSecrets[_id].count <= 2); + + // If both ratings have been received, start the reveal period from the time of the last rating commit + // Otherwise start the reveal period after the commit period has expired + // In both cases, keep reveal period open for 5 days + if (ratingSecrets[_id].count == 2) { + require(sub(now, ratingSecrets[_id].timestamp) <= RATING_REVEAL_TIMEOUT, "colony-task-rating-secret-reveal-period-closed"); + } else { + uint taskCompletionTime = tasks[_id].completionTimestamp; + require(sub(now, taskCompletionTime) > RATING_COMMIT_TIMEOUT, "colony-task-rating-secret-reveal-period-not-open"); + require(sub(now, taskCompletionTime) <= add(RATING_COMMIT_TIMEOUT, RATING_REVEAL_TIMEOUT), "colony-task-rating-secret-reveal-period-closed"); + } + + bytes32 secret = generateSecret(_salt, _rating); + require(secret == ratingSecrets[_id].secret[uint8(_role)], "colony-task-rating-secret-mismatch"); + + TaskRatings rating = TaskRatings(_rating); + require(rating != TaskRatings.None, "colony-task-rating-missing"); + taskRoles[_id][uint8(_role)].rating = rating; + + emit TaskWorkRatingRevealed(_id, _role, _rating); + } + + function generateSecret(bytes32 _salt, uint256 _value) public pure returns (bytes32) { + return keccak256(abi.encodePacked(_salt, _value)); + } + + function getTaskWorkRatingSecretsInfo(uint256 _id) public view returns (uint256, uint256) { + return (ratingSecrets[_id].count, ratingSecrets[_id].timestamp); + } + + function getTaskWorkRatingSecret(uint256 _id, uint8 _role) public view returns (bytes32) { + return ratingSecrets[_id].secret[_role]; + } + + // Note: the domain permissions arguments are placed at the end for consistency with the other role change functions + function setTaskManagerRole(uint256 _id, address payable _user, uint256 _permissionDomainId, uint256 _childSkillIndex) + public + self + isAdmin(_user, _permissionDomainId, _childSkillIndex, colony.getExpenditure(tasks[_id].expenditureId).domainId) + { + setTaskRoleUser(_id, TaskRole.Manager, _user); + } + + function setTaskEvaluatorRole(uint256 _id, address payable _user) public self { + // Can only assign role if no one is currently assigned to it + require(getTaskRoleUser(_id, TaskRole.Evaluator) == address(0x0), "colony-task-evaluator-role-already-assigned"); + setTaskRoleUser(_id, TaskRole.Evaluator, _user); + } + + function setTaskWorkerRole(uint256 _id, address payable _user) public self { + // Can only assign role if no one is currently assigned to it + require(getTaskRoleUser(_id, TaskRole.Worker) == address(0x0), "colony-task-worker-role-already-assigned"); + uint256[] memory skills = colony.getExpenditureSlot(tasks[_id].expenditureId, uint256(TaskRole.Worker)).skills; + require(skills[0] > 0, "colony-task-skill-not-set"); // ignore-swc-110 + setTaskRoleUser(_id, TaskRole.Worker, _user); + } + + function removeTaskEvaluatorRole(uint256 _id) public self { + setTaskRoleUser(_id, TaskRole.Evaluator, address(0x0)); + } + + function removeTaskWorkerRole(uint256 _id) public self { + setTaskRoleUser(_id, TaskRole.Worker, address(0x0)); + } + + function setTaskManagerPayout(uint256 _id, address _token, uint256 _amount) public self { + colony.setExpenditurePayout(_id, uint256(TaskRole.Manager), _token, _amount); + } + + function setTaskEvaluatorPayout(uint256 _id, address _token, uint256 _amount) public self { + colony.setExpenditurePayout(_id, uint256(TaskRole.Evaluator), _token, _amount); + } + + function setTaskWorkerPayout(uint256 _id, address _token, uint256 _amount) public self { + colony.setExpenditurePayout(_id, uint256(TaskRole.Worker), _token, _amount); + } + + function setAllTaskPayouts( + uint256 _id, + address _token, + uint256 _managerAmount, + uint256 _evaluatorAmount, + uint256 _workerAmount + ) + public + confirmTaskRoleIdentity(_id, msg.sender, TaskRole.Manager) + { + + address manager = getTaskRoleUser(_id, TaskRole.Manager); + address evaluator = getTaskRoleUser(_id, TaskRole.Evaluator); + address worker = getTaskRoleUser(_id, TaskRole.Worker); + + require(evaluator == manager || evaluator == address(0x0), "colony-funding-evaluator-already-set"); + require(worker == manager || worker == address(0x0), "colony-funding-worker-already-set"); + + this.setTaskManagerPayout(_id, _token, _managerAmount); + this.setTaskEvaluatorPayout(_id, _token, _evaluatorAmount); + this.setTaskWorkerPayout(_id, _token, _workerAmount); + } + + function setTaskSkill(uint256 _id, uint256 _skillId) public self { + colony.setExpenditureSkill(tasks[_id].expenditureId, uint256(TaskRole.Worker), _skillId); + } + + function setTaskBrief(uint256 _id, bytes32 _specificationHash) + public + self + taskExists(_id) + taskNotComplete(_id) + { + tasks[_id].specificationHash = _specificationHash; + + emit TaskBriefSet(_id, _specificationHash); + } + + function setTaskDueDate(uint256 _id, uint256 _dueDate) + public + self + taskExists(_id) + taskNotComplete(_id) + { + tasks[_id].dueDate = _dueDate; + + emit TaskDueDateSet(_id, _dueDate); + } + + function submitTaskDeliverable(uint256 _id, bytes32 _deliverableHash) + public + taskExists(_id) + taskNotComplete(_id) + confirmTaskRoleIdentity(_id, msg.sender, TaskRole.Worker) + { + tasks[_id].deliverableHash = _deliverableHash; + tasks[_id].completionTimestamp = now; + + emit TaskDeliverableSubmitted(_id, _deliverableHash); + emit TaskCompleted(_id); + } + + function submitTaskDeliverableAndRating(uint256 _id, bytes32 _deliverableHash, bytes32 _secret) public { + submitTaskDeliverable(_id, _deliverableHash); + submitTaskWorkRating(_id, TaskRole.Manager, _secret); + } + + function completeTask(uint256 _id) + public + taskExists(_id) + taskNotComplete(_id) + confirmTaskRoleIdentity(_id, msg.sender, TaskRole.Manager) + { + require(now >= tasks[_id].dueDate, "colony-task-due-date-in-future"); + tasks[_id].completionTimestamp = now; + + emit TaskCompleted(_id); + } + + function cancelTask(uint256 _id) + public + self + taskExists(_id) + taskNotComplete(_id) + { + colony.cancelExpenditure(tasks[_id].expenditureId); + } + + // Permissions pertain to the Arbitration role here + function finalizeTask(uint256 _permissionDomainId, uint256 _childSkillIndex, uint256 _id) + public + taskExists(_id) + taskComplete(_id) + { + colony.finalizeExpenditure(tasks[_id].expenditureId); + + assignWorkRatings(_id); + + for (uint8 roleId = 0; roleId <= 2; roleId++) { + Role storage role = taskRoles[_id][roleId]; + assert(role.rating != TaskRatings.None); + + // Emit reputation penalty if unsatisfactory + if (role.rating == TaskRatings.Unsatisfactory) { + emitReputationPenalty(_permissionDomainId, _childSkillIndex, _id, roleId); + } + + // Set payout modifier in all cases + setPayoutModifier(_permissionDomainId, _childSkillIndex, _id, roleId); + } + } + + function getTaskCount() public view returns (uint256) { + return taskCount; + } + + function getTaskChangeNonce(uint256 _id) public view returns (uint256) { + return tasks[_id].changeNonce; + } + + function getTask(uint256 _id) public view returns (Task memory task) { + task = tasks[_id]; + } + + function getTaskRole(uint256 _id, uint8 _role) public view returns (Role memory role) { + role = taskRoles[_id][_role]; + } + + function getTaskRoleUser(uint256 _id, TaskRole _role) public view returns (address) { + return colony.getExpenditureSlot(tasks[_id].expenditureId, uint256(_role)).recipient; + } + + function emitReputationPenalty(uint256 _permissionDomainId, uint256 _childSkillIndex, uint256 _id, uint8 _roleId) internal { + Role storage role = taskRoles[_id][_roleId]; + assert(role.rating == TaskRatings.Unsatisfactory); + + address user = getTaskRoleUser(_id, TaskRole(_roleId)); + address token = colony.getToken(); + uint256 payout = colony.getExpenditureSlotPayout(tasks[_id].expenditureId, uint256(_roleId), token); + int256 reputation = -int256(add(mul(payout, 2), role.rateFail ? payout : 0) / 2); + + uint256 domainId = colony.getExpenditure(tasks[_id].expenditureId).domainId; + colony.emitDomainReputationPenalty(_permissionDomainId, _childSkillIndex, domainId, user, reputation); + + // We do not penalise skill reputation if the worker did not rate -- calculate it again without the penalty. + if (TaskRole(_roleId) == TaskRole.Worker) { + uint256[] memory skills = colony.getExpenditureSlot(tasks[_id].expenditureId, uint256(_roleId)).skills; + int256 reputationPerSkill = -int256(payout / max(skills.length, 1)); + + for (uint i = 0; i < skills.length; i += 1) { + colony.emitSkillReputationPenalty(_permissionDomainId, skills[i], user, reputationPerSkill); + } + } + } + + function setPayoutModifier( + uint256 _permissionDomainId, + uint256 _childSkillIndex, + uint256 _id, + uint8 _roleId + ) + internal + { + Role storage role = taskRoles[_id][_roleId]; + uint256 payoutScalar; + + if (role.rating == TaskRatings.Satisfactory || role.rating == TaskRatings.Excellent) { + payoutScalar = (role.rating == TaskRatings.Excellent) ? 3 : 2; + payoutScalar -= role.rateFail ? 1 : 0; + payoutScalar *= WAD / 2; + } + + int256 payoutModifier = int256(payoutScalar) - int256(WAD); + colony.setExpenditurePayoutModifier(_permissionDomainId, _childSkillIndex, tasks[_id].expenditureId, uint256(_roleId), payoutModifier); + } + + function getReviewerAddresses( + uint8[] memory _sigV, + bytes32[] memory _sigR, + bytes32[] memory _sigS, + uint8[] memory _mode, + bytes32 msgHash + ) + internal + pure + returns (address[] memory) + { + address[] memory reviewerAddresses = new address[](_sigR.length); + for (uint i = 0; i < _sigR.length; i++) { + // 0 'Normal' mode - geth, etc. + // >0 'Trezor' mode + // Correct incantation helpfully cribbed from https://github.com/trezor/trezor-mcu/issues/163#issuecomment-368435292 + bytes32 txHash; + if (_mode[i] == 0) { + txHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", msgHash)); + } else { + txHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n\x20", msgHash)); + } + reviewerAddresses[i] = ecrecover(txHash, _sigV[i], _sigR[i], _sigS[i]); + } + return reviewerAddresses; + } + + // The address.call() syntax is no longer recommended, see: + // https://github.com/ethereum/solidity/issues/2884 + function executeCall(address to, uint256 value, bytes memory data) internal returns (bool success) { + assembly { + success := call(gas, to, value, add(data, 0x20), mload(data), 0, 0) + } + } + + // Get the function signature and task id from the transaction bytes data + // Note: Relies on the encoded function's first parameter to be the uint256 taskId + function deconstructCall(bytes memory _data) internal pure returns (bytes4 sig, uint256 taskId) { + assembly { + sig := mload(add(_data, 0x20)) + taskId := mload(add(_data, 0x24)) // same as calldataload(72) + } + } + + function deconstructRoleChangeCall(bytes memory _data) internal pure returns (bytes4 sig, uint256 taskId, address userAddress) { + assembly { + sig := mload(add(_data, 0x20)) + taskId := mload(add(_data, 0x24)) // same as calldataload(72) + userAddress := mload(add(_data, 0x44)) + } + } + + function taskWorkRatingsAssigned(uint256 _id) internal view returns (bool) { + Role storage workerRole = taskRoles[_id][uint8(TaskRole.Worker)]; + Role storage managerRole = taskRoles[_id][uint8(TaskRole.Manager)]; + return (workerRole.rating != TaskRatings.None) && (managerRole.rating != TaskRatings.None); + } + + function taskWorkRatingsClosed(uint256 _id) internal view returns (bool) { + assert(tasks[_id].completionTimestamp > 0); + assert(ratingSecrets[_id].count <= 2); + if (ratingSecrets[_id].count == 2) { + return sub(now, ratingSecrets[_id].timestamp) > RATING_REVEAL_TIMEOUT; + } else { + return sub(now, tasks[_id].completionTimestamp) > add(RATING_COMMIT_TIMEOUT, RATING_REVEAL_TIMEOUT); + } + } + + function assignWorkRatings(uint256 _id) internal { + require(taskWorkRatingsAssigned(_id) || taskWorkRatingsClosed(_id), "colony-task-ratings-not-closed"); + + // In the event of a user not committing/revealing within the rating window, + // their rating of their counterpart is assumed to be the maximum + // and they will receive a (payout/2) reputation penalty + + Role storage managerRole = taskRoles[_id][uint8(TaskRole.Manager)]; + Role storage workerRole = taskRoles[_id][uint8(TaskRole.Worker)]; + Role storage evaluatorRole = taskRoles[_id][uint8(TaskRole.Evaluator)]; + + if (workerRole.rating == TaskRatings.None) { + evaluatorRole.rating = TaskRatings.Unsatisfactory; + evaluatorRole.rateFail = true; + workerRole.rating = TaskRatings.Excellent; + } else { + evaluatorRole.rating = TaskRatings.Satisfactory; + } + + if (managerRole.rating == TaskRatings.None) { + workerRole.rateFail = true; + managerRole.rating = TaskRatings.Excellent; + } + } + + function setTaskRoleUser(uint256 _id, TaskRole _role, address payable _user) + private + taskExists(_id) + taskNotComplete(_id) + { + taskRoles[_id][uint8(_role)] = Role({ + rateFail: false, + rating: TaskRatings.None + }); + + colony.setExpenditureRecipient(tasks[_id].expenditureId, uint256(_role), _user); + } +} diff --git a/contracts/extensions/TasksFactory.sol b/contracts/extensions/TasksFactory.sol new file mode 100644 index 0000000000..9a3796e853 --- /dev/null +++ b/contracts/extensions/TasksFactory.sol @@ -0,0 +1,44 @@ +/* + 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.5.8; +pragma experimental ABIEncoderV2; + +import "./../ColonyDataTypes.sol"; +import "./../IColony.sol"; +import "./../ColonyAuthority.sol"; +import "./ExtensionFactory.sol"; +import "./Tasks.sol"; + + +contract TasksFactory is ExtensionFactory, ColonyDataTypes { // ignore-swc-123 + mapping (address => Tasks) public deployedExtensions; + + function deployExtension(address _colony) external { + require(IColony(_colony).hasUserRole(msg.sender, 1, ColonyRole.Root), "colony-extension-user-not-root"); // ignore-swc-123 + require(deployedExtensions[_colony] == Tasks(0x00), "colony-extension-already-deployed"); + Tasks newExtensionAddress = new Tasks(_colony); + deployedExtensions[_colony] = newExtensionAddress; + emit ExtensionDeployed("Tasks", _colony, address(newExtensionAddress)); + } + + function removeExtension(address _colony) external { + require(IColony(_colony).hasUserRole(msg.sender, 1, ColonyRole.Root), "colony-extension-user-not-root"); // ignore-swc-123 + deployedExtensions[_colony] = Tasks(0x00); + emit ExtensionRemoved("Tasks", _colony); + } +} diff --git a/helpers/task-review-signing.js b/helpers/task-review-signing.js index 69e9a2e8cc..050b625814 100644 --- a/helpers/task-review-signing.js +++ b/helpers/task-review-signing.js @@ -4,17 +4,17 @@ import fs from "fs"; import { ethers } from "ethers"; import { BigNumber } from "bignumber.js"; -export async function executeSignedTaskChange({ colony, taskId, functionName, signers, privKeys, sigTypes, args }) { - const { sigV, sigR, sigS, txData } = await getSigsAndTransactionData({ colony, taskId, functionName, signers, privKeys, sigTypes, args }); - return colony.executeTaskChange(sigV, sigR, sigS, sigTypes, 0, txData); +export async function executeSignedTaskChange({ colony, tasks, taskId, functionName, signers, privKeys, sigTypes, args }) { + const { sigV, sigR, sigS, txData } = await getSigsAndTransactionData({ colony, tasks, taskId, functionName, signers, privKeys, sigTypes, args }); + return (colony || tasks).executeTaskChange(sigV, sigR, sigS, sigTypes, 0, txData); } -export async function executeSignedRoleAssignment({ colony, taskId, functionName, signers, privKeys, sigTypes, args }) { - const { sigV, sigR, sigS, txData } = await getSigsAndTransactionData({ colony, taskId, functionName, signers, privKeys, sigTypes, args }); - return colony.executeTaskRoleAssignment(sigV, sigR, sigS, sigTypes, 0, txData); +export async function executeSignedRoleAssignment({ colony, tasks, taskId, functionName, signers, privKeys, sigTypes, args }) { + const { sigV, sigR, sigS, txData } = await getSigsAndTransactionData({ colony, tasks, taskId, functionName, signers, privKeys, sigTypes, args }); + return (colony || tasks).executeTaskRoleAssignment(sigV, sigR, sigS, sigTypes, 0, txData); } -export async function getSigsAndTransactionData({ colony, taskId, functionName, signers, privKeys, sigTypes, args }) { +export async function getSigsAndTransactionData({ colony, tasks, taskId, functionName, signers, privKeys, sigTypes, args }) { // We have to pass in an ethers BN because of https://github.com/ethereum/web3.js/issues/1920 // and https://github.com/ethereum/web3.js/issues/2077 const ethersBNTaskId = ethers.utils.bigNumberify(taskId.toString()); @@ -32,16 +32,17 @@ export async function getSigsAndTransactionData({ colony, taskId, functionName, } }); - const txData = await colony.contract.methods[functionName](...convertedArgs).encodeABI(); + const taskContract = colony || tasks; + const txData = await taskContract.contract.methods[functionName](...convertedArgs).encodeABI(); const sigsPromises = sigTypes.map((type, i) => { let privKey = []; if (privKeys) { privKey = [privKeys[i]]; } if (type === 0) { - return createSignatures(colony, ethersBNTaskId, [signers[i]], privKey, 0, txData); + return createSignatures(taskContract, ethersBNTaskId, [signers[i]], privKey, 0, txData); } - return createSignaturesTrezor(colony, ethersBNTaskId, [signers[i]], privKey, 0, txData); + return createSignaturesTrezor(taskContract, ethersBNTaskId, [signers[i]], privKey, 0, txData); }); const sigs = await Promise.all(sigsPromises); const sigV = sigs.map(sig => sig.sigV[0]); @@ -50,10 +51,10 @@ export async function getSigsAndTransactionData({ colony, taskId, functionName, return { sigV, sigR, sigS, txData }; } -export async function createSignatures(colony, taskId, signers, privKeys, value, data) { - const sourceAddress = colony.address; - const destinationAddress = colony.address; - const nonce = await colony.getTaskChangeNonce(taskId); +export async function createSignatures(taskContract, taskId, signers, privKeys, value, data) { + const sourceAddress = taskContract.address; + const destinationAddress = taskContract.address; + const nonce = await taskContract.getTaskChangeNonce(taskId); const input = `0x${sourceAddress.slice(2)}${destinationAddress.slice(2)}${padLeft(value.toString(16), "64", "0")}${data.slice(2)}${padLeft( nonce.toString(16), "64", @@ -92,10 +93,10 @@ export async function createSignatures(colony, taskId, signers, privKeys, value, return { sigV, sigR, sigS }; } -export async function createSignaturesTrezor(colony, taskId, signers, privKeys, value, data) { - const sourceAddress = colony.address; - const destinationAddress = colony.address; - const nonce = await colony.getTaskChangeNonce(taskId); +export async function createSignaturesTrezor(taskContract, taskId, signers, privKeys, value, data) { + const sourceAddress = taskContract.address; + const destinationAddress = taskContract.address; + const nonce = await taskContract.getTaskChangeNonce(taskId); const input = `0x${sourceAddress.slice(2)}${destinationAddress.slice(2)}${padLeft(value.toString(16), "64", "0")}${data.slice(2)}${padLeft( nonce.toString(16), "64", diff --git a/helpers/test-data-generator.js b/helpers/test-data-generator.js index a2f6776933..4d43ae9d2c 100644 --- a/helpers/test-data-generator.js +++ b/helpers/test-data-generator.js @@ -59,10 +59,11 @@ export async function makeTask({ colonyNetwork, colony, hash = SPECIFICATION_HAS return logs.filter(log => log.event === "TaskAdded")[0].args.taskId; } -export async function assignRoles({ colony, taskId, manager, evaluator, worker }) { +export async function assignRoles({ colony, tasks, taskId, manager, evaluator, worker }) { if (evaluator && manager !== evaluator) { await executeSignedTaskChange({ colony, + tasks, taskId, functionName: "removeTaskEvaluatorRole", signers: [manager], @@ -72,6 +73,7 @@ export async function assignRoles({ colony, taskId, manager, evaluator, worker } await executeSignedRoleAssignment({ colony, + tasks, taskId, functionName: "setTaskEvaluatorRole", signers: [manager, evaluator], @@ -85,6 +87,7 @@ export async function assignRoles({ colony, taskId, manager, evaluator, worker } await executeSignedRoleAssignment({ colony, + tasks, taskId, functionName: "setTaskWorkerRole", signers, @@ -93,17 +96,24 @@ export async function assignRoles({ colony, taskId, manager, evaluator, worker } }); } -export async function submitDeliverableAndRatings({ colony, taskId, managerRating = MANAGER_RATING, workerRating = WORKER_RATING }) { +export async function submitDeliverableAndRatings({ colony, tasks, taskId, managerRating = MANAGER_RATING, workerRating = WORKER_RATING }) { const managerRatingSecret = soliditySha3(RATING_1_SALT, managerRating); const workerRatingSecret = soliditySha3(RATING_2_SALT, workerRating); - - const evaluatorRole = await colony.getTaskRole(taskId, EVALUATOR_ROLE); - const workerRole = await colony.getTaskRole(taskId, WORKER_ROLE); - - await colony.submitTaskDeliverableAndRating(taskId, DELIVERABLE_HASH, managerRatingSecret, { from: workerRole.user }); - await colony.submitTaskWorkRating(taskId, WORKER_ROLE, workerRatingSecret, { from: evaluatorRole.user }); - await colony.revealTaskWorkRating(taskId, MANAGER_ROLE, managerRating, RATING_1_SALT, { from: workerRole.user }); - await colony.revealTaskWorkRating(taskId, WORKER_ROLE, workerRating, RATING_2_SALT, { from: evaluatorRole.user }); + let evaluator; + let worker; + if (colony) { + const evaluatorRole = await colony.getTaskRole(taskId, EVALUATOR_ROLE); + const workerRole = await colony.getTaskRole(taskId, WORKER_ROLE); + evaluator = evaluatorRole.user; + worker = workerRole.user; + } else { + evaluator = await tasks.getTaskRoleUser(taskId, EVALUATOR_ROLE); + worker = await tasks.getTaskRoleUser(taskId, WORKER_ROLE); + } + await (colony || tasks).submitTaskDeliverableAndRating(taskId, DELIVERABLE_HASH, managerRatingSecret, { from: worker }); + await (colony || tasks).submitTaskWorkRating(taskId, WORKER_ROLE, workerRatingSecret, { from: evaluator }); + await (colony || tasks).revealTaskWorkRating(taskId, MANAGER_ROLE, managerRating, RATING_1_SALT, { from: worker }); + await (colony || tasks).revealTaskWorkRating(taskId, WORKER_ROLE, workerRating, RATING_2_SALT, { from: evaluator }); } export async function setupAssignedTask({ colonyNetwork, colony, dueDate, domainId = 1, skillId, manager, evaluator, worker }) { diff --git a/scripts/check-recovery.js b/scripts/check-recovery.js index 3eb4377535..7d3ff2f93a 100644 --- a/scripts/check-recovery.js +++ b/scripts/check-recovery.js @@ -28,6 +28,8 @@ walkSync("./contracts/").forEach(contractName => { "contracts/extensions/ExtensionFactory.sol", "contracts/extensions/OneTxPayment.sol", "contracts/extensions/OneTxPaymentFactory.sol", + "contracts/extensions/Tasks.sol", + "contracts/extensions/TasksFactory.sol", "contracts/gnosis/MultiSigWallet.sol", "contracts/PatriciaTree/Bits.sol", "contracts/PatriciaTree/Data.sol", diff --git a/scripts/check-storage.js b/scripts/check-storage.js index f4a6803c7c..0b1d3b5c4a 100644 --- a/scripts/check-storage.js +++ b/scripts/check-storage.js @@ -16,6 +16,8 @@ walkSync("./contracts/").forEach(contractName => { [ "contracts/extensions/OneTxPayment.sol", "contracts/extensions/OneTxPaymentFactory.sol", + "contracts/extensions/Tasks.sol", + "contracts/extensions/TasksFactory.sol", "contracts/CommonAuthority.sol", "contracts/ColonyAuthority.sol", "contracts/ColonyNetworkAuthority.sol", diff --git a/test/extensions/tasks.js b/test/extensions/tasks.js new file mode 100644 index 0000000000..e7b050decd --- /dev/null +++ b/test/extensions/tasks.js @@ -0,0 +1,2108 @@ +/* global artifacts */ +import { BN } from "bn.js"; +import { ethers } from "ethers"; +import chai from "chai"; +import bnChai from "bn-chai"; +import { soliditySha3 } from "web3-utils"; + +import { + UINT256_MAX, + WAD, + MANAGER_ROLE, + EVALUATOR_ROLE, + WORKER_ROLE, + SPECIFICATION_HASH, + SPECIFICATION_HASH_UPDATED, + DELIVERABLE_HASH, + INITIAL_FUNDING, + SECONDS_PER_DAY, + MANAGER_PAYOUT, + WORKER_PAYOUT, + EVALUATOR_PAYOUT, + MANAGER_RATING, + RATING_1_SALT, + RATING_2_SALT, + RATING_1_SECRET, + RATING_2_SECRET, + CANCELLED_TASK_STATE, + FINALIZED_TASK_STATE, + GLOBAL_SKILL_ID +} from "../../helpers/constants"; + +import { + web3GetBalance, + checkErrorRevert, + expectEvent, + expectAllEvents, + forwardTime, + currentBlockTime, + getBlockTime +} from "../../helpers/test-helper"; + +import { fundColonyWithTokens, setupRandomColony, assignRoles, submitDeliverableAndRatings } from "../../helpers/test-data-generator"; +import { getSigsAndTransactionData, executeSignedTaskChange, executeSignedRoleAssignment } from "../../helpers/task-review-signing"; + +const { expect } = chai; +chai.use(bnChai(web3.utils.BN)); + +const EtherRouter = artifacts.require("EtherRouter"); +const IMetaColony = artifacts.require("IMetaColony"); +const IColonyNetwork = artifacts.require("IColonyNetwork"); +const IReputationMiningCycle = artifacts.require("IReputationMiningCycle"); + +const TasksFactory = artifacts.require("TasksFactory"); +const Tasks = artifacts.require("Tasks"); + +contract("Tasks extension", accounts => { + const MANAGER = accounts[0]; + const EVALUATOR = accounts[1]; + const WORKER = accounts[2]; + + const ADMIN = accounts[3]; + const OTHER = accounts[4]; + + let colonyNetwork; + let metaColony; + + let colony; + let token; + let domain1; + + let tasksFactory; + let tasks; + + before(async () => { + const etherRouter = await EtherRouter.deployed(); + colonyNetwork = await IColonyNetwork.at(etherRouter.address); + + const metaColonyAddress = await colonyNetwork.getMetaColony(); + metaColony = await IMetaColony.at(metaColonyAddress); + await metaColony.setNetworkFeeInverse(UINT256_MAX); + + tasksFactory = await TasksFactory.new(); + + ({ colony, token } = await setupRandomColony(colonyNetwork)); + await colony.setRewardInverse(UINT256_MAX); + await colony.setAdministrationRole(1, 0, ADMIN, 1, true); + await colony.addDomain(1, 0, 1); // Domain 2 + domain1 = await colony.getDomain(1); + + await tasksFactory.deployExtension(colony.address); + const tasksAddress = await tasksFactory.deployedExtensions(colony.address); + tasks = await Tasks.at(tasksAddress); + + await colony.setArbitrationRole(1, 0, tasks.address, 1, true); + await colony.setAdministrationRole(1, 0, tasks.address, 1, true); + }); + + describe("when deploying the extension", () => { + it("should not allow non-root users to deploy", async () => { + await checkErrorRevert(tasksFactory.deployExtension(colony.address, { from: OTHER }), "colony-extension-user-not-root"); + }); + + it("should not allow the extension to be deployed twice", async () => { + await checkErrorRevert(tasksFactory.deployExtension(colony.address), "colony-extension-already-deployed"); + }); + + it("should not allow non-root users to remove the extension", async () => { + await checkErrorRevert(tasksFactory.removeExtension(colony.address, { from: OTHER }), "colony-extension-user-not-root"); + }); + + it("should allow root users to remove the extension", async () => { + await tasksFactory.removeExtension(colony.address); + }); + }); + + describe("when creating tasks", () => { + it("should allow admins to make a task", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + const task = await tasks.getTask(taskId); + + expect(task.specificationHash).to.equal(SPECIFICATION_HASH); + expect(task.deliverableHash).to.equal(ethers.constants.HashZero); + expect(task.completionTimestamp).to.be.zero; + }); + + it("should fail if a non-admin user tries to make a task", async () => { + await checkErrorRevert(tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: OTHER }), "colony-task-not-admin"); + }); + + it("should set the task creator as the manager and evaluator", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + const task = await tasks.getTask(taskId); + + const taskManager = await tasks.getTaskRoleUser(taskId, MANAGER_ROLE); + const taskEvaluator = await tasks.getTaskRoleUser(taskId, EVALUATOR_ROLE); + const expenditureManager = await colony.getExpenditureSlot(task.expenditureId, MANAGER_ROLE); + const expenditureEvaluator = await colony.getExpenditureSlot(task.expenditureId, EVALUATOR_ROLE); + + expect(taskManager).to.equal(MANAGER); + expect(taskEvaluator).to.equal(MANAGER); + expect(expenditureManager.recipient).to.equal(MANAGER); + expect(expenditureEvaluator.recipient).to.equal(MANAGER); + }); + + it("should allow the reassignment of evaluator", async () => { + const newEvaluator = accounts[1]; + expect(newEvaluator).to.not.equal(MANAGER); + + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + let evaluator = await tasks.getTaskRoleUser(taskId, EVALUATOR_ROLE); + expect(evaluator).to.equal(MANAGER); + + await executeSignedTaskChange({ + tasks, + taskId, + functionName: "removeTaskEvaluatorRole", + signers: [MANAGER], // NOTE: only one signature because manager === evaluator + sigTypes: [0], + args: [taskId] + }); + + evaluator = await tasks.getTaskRoleUser(taskId, EVALUATOR_ROLE); + expect(evaluator).to.equal(ethers.constants.AddressZero); + + await executeSignedRoleAssignment({ + tasks, + taskId, + functionName: "setTaskEvaluatorRole", + signers: [MANAGER, newEvaluator], + sigTypes: [0, 0], + args: [taskId, newEvaluator] + }); + + evaluator = await tasks.getTaskRoleUser(taskId, EVALUATOR_ROLE); + expect(evaluator).to.equal(newEvaluator); + }); + + it("should return the correct number of tasks", async () => { + const taskCountBefore = await tasks.getTaskCount(); + + for (let i = 0; i < 5; i += 1) { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + } + + const taskCountAfter = await tasks.getTaskCount(); + expect(taskCountAfter.sub(taskCountBefore)).to.be.eq.BN(5); + }); + + it("should set the task domain correctly", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 2, 0, 0); + const taskId = await tasks.getTaskCount(); + + const task = await tasks.getTask(taskId); + const expenditure = await colony.getExpenditure(task.expenditureId); + expect(expenditure.domainId).to.eq.BN(2); + }); + + it("should log TaskAdded and TaskDueDateSet events", async () => { + await expectAllEvents(tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0), ["TaskAdded", "TaskDueDateSet"]); + }); + + it("should optionally set the skill and due date", async () => { + const currTime = await currentBlockTime(); + const dueDate = currTime + SECONDS_PER_DAY; + + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, dueDate); + const taskId = await tasks.getTaskCount(); + + const task = await tasks.getTask(taskId); + expect(task.dueDate).to.eq.BN(dueDate); + + const slot = await colony.getExpenditureSlot(task.expenditureId, WORKER_ROLE); + expect(slot.skills[0]).to.eq.BN(GLOBAL_SKILL_ID); + }); + + it("should set the due date to 90 days from now if unspecified", async () => { + const tx = await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0); + const taskId = await tasks.getTaskCount(); + + const task = await tasks.getTask(taskId); + const currTime = await getBlockTime(tx.receipt.blockNumber); + expect(task.dueDate).to.eq.BN(currTime + SECONDS_PER_DAY * 90); + }); + }); + + describe("when updating tasks", () => { + it("should not be able to `executeTaskRoleAssignment` on a nonexistent task", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await checkErrorRevert( + executeSignedRoleAssignment({ + tasks, + taskId, + functionName: "setTaskWorkerRole", + signers: [MANAGER, WORKER], + sigTypes: [0, 0], + args: [taskId.addn(1), WORKER] + }), + "colony-task-does-not-exist" + ); + }); + + it("should not be able to `executeTaskRoleAssignment` on a finalized task", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); + await submitDeliverableAndRatings({ tasks, taskId }); + await tasks.finalizeTask(1, 0, taskId, { from: MANAGER }); + + await checkErrorRevert( + executeSignedRoleAssignment({ + tasks, + taskId, + functionName: "setTaskWorkerRole", + signers: [MANAGER, WORKER], + sigTypes: [0, 0], + args: [taskId, WORKER] + }), + "colony-task-finalized" + ); + }); + + it("should not be able to pass unallowed function signature to `executeTaskRoleAssignment`", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await checkErrorRevert( + executeSignedRoleAssignment({ + tasks, + taskId, + functionName: "setTaskDueDate", // Not a role change function! + signers: [MANAGER, WORKER], + sigTypes: [0, 0], + args: [taskId, WORKER] + }), + "colony-task-change-is-not-role-assignment" + ); + }); + + it("should not be able to send any ether while assigning a role", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + const { sigV, sigR, sigS, txData } = await getSigsAndTransactionData({ + tasks, + taskId, + functionName: "setTaskWorkerRole", + signers: [MANAGER, WORKER], + sigTypes: [0, 0], + args: [taskId, WORKER] + }); + + await checkErrorRevert(tasks.executeTaskRoleAssignment(sigV, sigR, sigS, [0, 0], 10, txData), "colony-task-role-assignment-non-zero-value"); + }); + + it("should not be able to execute task change when the number of signature parts differ", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + const { sigV, sigR, sigS, txData } = await getSigsAndTransactionData({ + tasks, + taskId, + functionName: "setTaskWorkerRole", + signers: [MANAGER, WORKER], + sigTypes: [0, 0], + args: [taskId, WORKER] + }); + + await checkErrorRevert( + tasks.executeTaskRoleAssignment([sigV[0]], sigR, sigS, [0], 0, txData), + "colony-task-role-assignment-signatures-count-do-not-match" + ); + }); + + it("should allow the evaluator and worker roles to be assigned", async () => { + const newEvaluator = accounts[1]; + expect(newEvaluator).to.not.equal(MANAGER); + + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await executeSignedTaskChange({ + tasks, + taskId, + functionName: "removeTaskEvaluatorRole", + signers: [MANAGER], + sigTypes: [0], + args: [taskId] + }); + + await executeSignedRoleAssignment({ + tasks, + taskId, + functionName: "setTaskEvaluatorRole", + signers: [MANAGER, newEvaluator], + sigTypes: [0, 0], + args: [taskId, newEvaluator] + }); + + const evaluator = await tasks.getTaskRoleUser(taskId, EVALUATOR_ROLE); + expect(evaluator).to.equal(newEvaluator); + + await executeSignedRoleAssignment({ + tasks, + taskId, + functionName: "setTaskWorkerRole", + signers: [MANAGER, WORKER], + sigTypes: [0, 0], + args: [taskId, WORKER] + }); + + const worker = await tasks.getTaskRoleUser(taskId, WORKER_ROLE); + expect(worker).to.equal(WORKER); + }); + + it("should not allow a worker to be assigned if the task has no skill", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await checkErrorRevert( + executeSignedRoleAssignment({ + tasks, + taskId, + functionName: "setTaskWorkerRole", + signers: [MANAGER, WORKER], + sigTypes: [0, 0], + args: [taskId, WORKER] + }), + "colony-task-role-assignment-execution-failed" + ); + + await executeSignedTaskChange({ + tasks, + taskId, + functionName: "setTaskSkill", + signers: [MANAGER], + sigTypes: [0], + args: [taskId, GLOBAL_SKILL_ID] + }); + + executeSignedRoleAssignment({ + tasks, + taskId, + functionName: "setTaskWorkerRole", + signers: [MANAGER, WORKER], + sigTypes: [0, 0], + args: [taskId, WORKER] + }); + }); + + it("should not allow the evaluator or worker roles to be assigned only by manager", async () => { + const newEvaluator = accounts[1]; + expect(newEvaluator).to.not.equal(MANAGER); + + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await executeSignedTaskChange({ + tasks, + taskId, + functionName: "removeTaskEvaluatorRole", + signers: [MANAGER], + sigTypes: [0], + args: [taskId] + }); + + await checkErrorRevert( + executeSignedRoleAssignment({ + tasks, + taskId, + functionName: "setTaskEvaluatorRole", + signers: [MANAGER], + sigTypes: [0], + args: [taskId, newEvaluator] + }), + "colony-task-role-assignment-does-not-meet-required-signatures" + ); + + await checkErrorRevert( + executeSignedRoleAssignment({ + tasks, + taskId, + functionName: "setTaskWorkerRole", + signers: [MANAGER], + sigTypes: [0], + args: [taskId, WORKER] + }), + "colony-task-role-assignment-does-not-meet-required-signatures" + ); + }); + + it("should not allow role to be assigned if it is already assigned to somebody", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await checkErrorRevert( + executeSignedRoleAssignment({ + tasks, + taskId, + functionName: "setTaskEvaluatorRole", + signers: [MANAGER, OTHER], + sigTypes: [0, 0], + args: [taskId, OTHER] + }), + "colony-task-role-assignment-execution-failed" + ); + + await executeSignedRoleAssignment({ + tasks, + taskId, + functionName: "setTaskWorkerRole", + signers: [MANAGER, WORKER], + sigTypes: [0, 0], + args: [taskId, WORKER] + }); + + await checkErrorRevert( + executeSignedRoleAssignment({ + tasks, + taskId, + functionName: "setTaskWorkerRole", + signers: [MANAGER, OTHER], + sigTypes: [0, 0], + args: [taskId, OTHER] + }), + "colony-task-role-assignment-execution-failed" + ); + }); + + it("should allow role to be unassigned, as long as the current assigned address agrees", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await executeSignedRoleAssignment({ + tasks, + taskId, + functionName: "setTaskWorkerRole", + signers: [MANAGER, WORKER], + sigTypes: [0, 0], + args: [taskId, WORKER] + }); + + let worker = await tasks.getTaskRoleUser(taskId, WORKER_ROLE); + expect(worker).to.equal(WORKER); + + // Worker does not agree + await checkErrorRevert( + executeSignedRoleAssignment({ + tasks, + taskId, + functionName: "setTaskWorkerRole", + signers: [MANAGER, OTHER], + sigTypes: [0, 0], + args: [taskId, ethers.constants.AddressZero] + }), + "colony-task-role-assignment-not-signed-by-new-user-for-role" + ); + + // Now they do! + await executeSignedTaskChange({ + tasks, + taskId, + functionName: "removeTaskWorkerRole", + signers: [MANAGER, WORKER], + sigTypes: [0, 0], + args: [taskId] + }); + + worker = await tasks.getTaskRoleUser(taskId, WORKER_ROLE); + expect(worker).to.equal(ethers.constants.AddressZero); + }); + + it("should not allow role to be assigned if passed address is not equal to one of the signers", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await checkErrorRevert( + executeSignedRoleAssignment({ + tasks, + taskId, + functionName: "setTaskWorkerRole", + signers: [MANAGER, OTHER], + sigTypes: [0, 0], + args: [taskId, WORKER] + }), + "colony-task-role-assignment-not-signed-by-new-user-for-role" + ); + }); + + it("should allow manager to assign themself to a role", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await executeSignedRoleAssignment({ + tasks, + taskId, + functionName: "setTaskWorkerRole", + signers: [MANAGER], + sigTypes: [0], + args: [taskId, MANAGER] + }); + + const worker = await tasks.getTaskRoleUser(taskId, WORKER_ROLE); + expect(worker).to.equal(MANAGER); + }); + + it("should not allow anyone to assign themself to a role with one signature except manager", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await checkErrorRevert( + executeSignedRoleAssignment({ + tasks, + taskId, + functionName: "setTaskWorkerRole", + signers: [WORKER], + sigTypes: [0], + args: [taskId, WORKER] + }), + "colony-task-role-assignment-does-not-meet-required-signatures" + ); + }); + + it("should allow different modes of signing when assigning roles", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await executeSignedRoleAssignment({ + tasks, + taskId, + functionName: "setTaskWorkerRole", + signers: [WORKER, MANAGER], + sigTypes: [0, 1], // Different sig types + args: [taskId, WORKER] + }); + + const worker = await tasks.getTaskRoleUser(taskId, WORKER_ROLE); + expect(worker).to.equal(WORKER); + }); + + it("should not allow role assignment if none of the signers is manager", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await checkErrorRevert( + executeSignedRoleAssignment({ + tasks, + taskId, + functionName: "setTaskWorkerRole", + signers: [WORKER, OTHER], + sigTypes: [0, 0], + args: [taskId, WORKER] + }), + "colony-task-role-assignment-not-signed-by-manager" + ); + }); + + it("should allow to change manager role if the user agrees", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await executeSignedRoleAssignment({ + tasks, + taskId, + functionName: "setTaskManagerRole", + signers: [MANAGER, ADMIN], + sigTypes: [0, 0], + args: [taskId, ADMIN, 1, 0] + }); + + const manager = await tasks.getTaskRoleUser(taskId, MANAGER_ROLE); + expect(manager).to.equal(ADMIN); + }); + + it("should not allow one-signature assignment of manager to a role if signer is not manager", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await checkErrorRevert( + executeSignedRoleAssignment({ + tasks, + taskId, + functionName: "setTaskWorkerRole", + signers: [OTHER], + sigTypes: [0], + args: [taskId, MANAGER] + }), + "colony-task-role-assignment-not-signed-by-manager" + ); + }); + + it("should not allow assignment of manager role if the user does not agree", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await checkErrorRevert( + executeSignedRoleAssignment({ + tasks, + taskId, + functionName: "setTaskManagerRole", + signers: [MANAGER, OTHER], + sigTypes: [0, 0], + args: [taskId, ADMIN, 1, 0] + }), + "colony-task-role-assignment-not-signed-by-new-user-for-role" + ); + }); + + it("should not allow assignment of manager role if user is not an admin", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await checkErrorRevert( + executeSignedRoleAssignment({ + tasks, + taskId, + functionName: "setTaskManagerRole", + signers: [MANAGER, OTHER], + sigTypes: [0, 0], + args: [taskId, OTHER, 1, 0] + }), + "colony-task-role-assignment-execution-failed" + ); + }); + + it("should not allow removal of manager role", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await checkErrorRevert( + executeSignedRoleAssignment({ + tasks, + taskId, + functionName: "setTaskManagerRole", + signers: [MANAGER, ADMIN], + sigTypes: [0, 0], + args: [taskId, ethers.constants.AddressZero, 1, 0] + }), + "colony-task-role-assignment-not-signed-by-new-user-for-role" + ); + }); + + it("should not allow assignment of manager role if current manager is not one of the signers", async () => { + const newEvaluator = accounts[1]; + expect(newEvaluator).to.not.equal(MANAGER); + + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + // Setting the worker + await executeSignedRoleAssignment({ + tasks, + taskId, + functionName: "setTaskWorkerRole", + signers: [MANAGER, ADMIN], + sigTypes: [0, 0], + args: [taskId, ADMIN] + }); + + // Setting the evaluator + await executeSignedTaskChange({ + tasks, + taskId, + functionName: "removeTaskEvaluatorRole", + signers: [MANAGER], + sigTypes: [0], + args: [taskId] + }); + + await executeSignedRoleAssignment({ + tasks, + taskId, + functionName: "setTaskEvaluatorRole", + signers: [MANAGER, newEvaluator], + sigTypes: [0, 0], + args: [taskId, newEvaluator] + }); + + // Evaluator and worker trying to set a manager + await checkErrorRevert( + executeSignedRoleAssignment({ + tasks, + taskId, + functionName: "setTaskManagerRole", + signers: [newEvaluator, WORKER], + sigTypes: [0, 0], + args: [taskId, WORKER, 1, 0] + }), + "colony-task-role-assignment-not-signed-by-manager" + ); + }); + + it("should correctly increment `taskChangeNonce` for multiple updates on multiple tasks", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + const taskId1 = await tasks.getTaskCount(); + + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + const taskId2 = await tasks.getTaskCount(); + + // Change the task1 brief + await executeSignedTaskChange({ + tasks, + taskId: taskId1, + functionName: "setTaskBrief", + signers: [MANAGER], + sigTypes: [0], + args: [taskId1, SPECIFICATION_HASH_UPDATED] + }); + let taskChangeNonce = await tasks.getTaskChangeNonce(taskId1); + expect(taskChangeNonce).to.eq.BN(1); + + await executeSignedTaskChange({ + tasks, + taskId: taskId2, + functionName: "setTaskBrief", + signers: [MANAGER], + sigTypes: [0], + args: [taskId2, SPECIFICATION_HASH_UPDATED] + }); + + taskChangeNonce = await tasks.getTaskChangeNonce(taskId2); + expect(taskChangeNonce).to.eq.BN(1); + + // Change the task2 due date + const dueDate = await currentBlockTime(); + + await executeSignedTaskChange({ + tasks, + taskId: taskId2, + functionName: "setTaskDueDate", + signers: [MANAGER], + sigTypes: [0], + args: [taskId2, dueDate] + }); + + taskChangeNonce = await tasks.getTaskChangeNonce(taskId2); + expect(taskChangeNonce).to.eq.BN(2); + }); + + it("should allow update of task brief signed by manager only when worker has not been assigned", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await executeSignedTaskChange({ + tasks, + taskId, + functionName: "setTaskBrief", + signers: [MANAGER], + sigTypes: [0], + args: [taskId, SPECIFICATION_HASH_UPDATED] + }); + + const task = await tasks.getTask(taskId); + expect(task.specificationHash).to.eq.BN(SPECIFICATION_HASH_UPDATED); + }); + + it("should allow update of task brief signed by manager and worker", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await executeSignedRoleAssignment({ + tasks, + taskId, + functionName: "setTaskWorkerRole", + signers: [MANAGER, WORKER], + sigTypes: [0, 0], + args: [taskId, WORKER] + }); + + await executeSignedTaskChange({ + tasks, + taskId, + functionName: "setTaskBrief", + signers: [MANAGER, WORKER], + sigTypes: [0, 0], + args: [taskId, SPECIFICATION_HASH_UPDATED] + }); + + const task = await tasks.getTask(taskId); + expect(task.specificationHash).to.eq.BN(SPECIFICATION_HASH_UPDATED); + }); + + it("should allow update of task brief signed by manager and worker using Trezor-style signatures", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await executeSignedRoleAssignment({ + tasks, + taskId, + functionName: "setTaskWorkerRole", + signers: [MANAGER, WORKER], + sigTypes: [0, 0], + args: [taskId, WORKER] + }); + + await executeSignedTaskChange({ + tasks, + taskId, + functionName: "setTaskBrief", + signers: [MANAGER, WORKER], + sigTypes: [1, 1], + args: [taskId, SPECIFICATION_HASH_UPDATED] + }); + + const task = await tasks.getTask(taskId); + expect(task.specificationHash).to.eq.BN(SPECIFICATION_HASH_UPDATED); + }); + + it("should allow update of task brief signed by manager and worker if one uses Trezor-style signatures and the other does not", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await executeSignedRoleAssignment({ + tasks, + taskId, + functionName: "setTaskWorkerRole", + signers: [MANAGER, WORKER], + sigTypes: [0, 0], + args: [taskId, WORKER] + }); + + await executeSignedTaskChange({ + tasks, + taskId, + functionName: "setTaskBrief", + signers: [MANAGER, WORKER], + sigTypes: [0, 1], + args: [taskId, SPECIFICATION_HASH_UPDATED] + }); + + const task = await tasks.getTask(taskId); + expect(task.specificationHash).to.eq.BN(SPECIFICATION_HASH_UPDATED); + }); + + it("should not allow update of task brief signed by manager twice, with two different signature styles", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await executeSignedRoleAssignment({ + tasks, + taskId, + functionName: "setTaskWorkerRole", + signers: [MANAGER, WORKER], + sigTypes: [0, 0], + args: [taskId, WORKER] + }); + + await checkErrorRevert( + executeSignedTaskChange({ + tasks, + taskId, + functionName: "setTaskBrief", + signers: [MANAGER, MANAGER], + sigTypes: [0, 1], + args: [taskId, SPECIFICATION_HASH_UPDATED] + }), + "colony-task-duplicate-reviewers" + ); + }); + + it("should allow update of task due date signed by manager and worker", async () => { + const dueDate = await currentBlockTime(); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await executeSignedRoleAssignment({ + tasks, + taskId, + functionName: "setTaskWorkerRole", + signers: [MANAGER, WORKER], + sigTypes: [0, 0], + args: [taskId, WORKER] + }); + + await executeSignedTaskChange({ + tasks, + taskId, + functionName: "setTaskDueDate", + signers: [MANAGER, WORKER], + sigTypes: [0, 0], + args: [taskId, dueDate] + }); + + const task = await tasks.getTask(taskId); + expect(task.dueDate).to.eq.BN(dueDate); + }); + + it("should fail if a non-colony call is made to the task update functions", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await checkErrorRevert(tasks.setTaskBrief(taskId, SPECIFICATION_HASH_UPDATED, { from: OTHER }), "colony-task-not-self"); + }); + + it("should fail update of task brief signed by a non-registered role", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await checkErrorRevert( + executeSignedTaskChange({ + tasks, + taskId, + functionName: "setTaskBrief", + signers: [MANAGER, OTHER], + sigTypes: [0, 0], + args: [taskId, SPECIFICATION_HASH_UPDATED] + }), + "colony-task-change-does-not-meet-signatures-required" + ); + }); + + it("should fail update of task brief signed by manager and evaluator", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await executeSignedRoleAssignment({ + tasks, + taskId, + functionName: "setTaskWorkerRole", + signers: [MANAGER, WORKER], + sigTypes: [0, 0], + args: [taskId, WORKER] + }); + + await checkErrorRevert( + executeSignedTaskChange({ + tasks, + taskId, + functionName: "setTaskBrief", + signers: [MANAGER], + sigTypes: [0], + args: [taskId, SPECIFICATION_HASH_UPDATED] + }), + "colony-task-change-does-not-meet-signatures-required" + ); + }); + + it("should fail to execute task change for a non-registered function signature", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await checkErrorRevert( + executeSignedTaskChange({ + tasks, + taskId, + functionName: "getTaskRole", + signers: [MANAGER, EVALUATOR], + sigTypes: [0, 0], + args: [taskId, 0] + }), + "colony-task-change-does-not-meet-signatures-required" + ); + }); + + it("should fail to execute change of task brief, using an invalid taskId", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + const taskCount = await tasks.getTaskCount(); + const nonExistentTaskId = taskCount.addn(10); + + await checkErrorRevert( + executeSignedTaskChange({ + tasks, + taskId, + functionName: "setTaskBrief", + signers: [MANAGER], + sigTypes: [0], + args: [nonExistentTaskId, SPECIFICATION_HASH_UPDATED] + }), + "colony-task-does-not-exist" + ); + }); + + it("should fail to execute change of task brief, using invalid taskId 0", async () => { + const taskId = 0; + + await checkErrorRevert( + executeSignedTaskChange({ + tasks, + taskId, + functionName: "setTaskBrief", + signers: [MANAGER], + sigTypes: [0], + args: [taskId, SPECIFICATION_HASH_UPDATED] + }), + "colony-task-does-not-exist" + ); + }); + + it("should fail to execute task changes, when trying to set skill to 0", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await checkErrorRevert( + executeSignedTaskChange({ + tasks, + taskId, + functionName: "setTaskSkill", + signers: [MANAGER], + sigTypes: [0], + args: [taskId, 0] + }), + "colony-task-change-execution-failed" + ); + }); + + it("should fail to execute task change, if the task is already finalized", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); + await submitDeliverableAndRatings({ tasks, taskId }); + await tasks.finalizeTask(1, 0, taskId, { from: MANAGER }); + + await checkErrorRevert( + executeSignedTaskChange({ + tasks, + taskId, + functionName: "setTaskBrief", + signers: [MANAGER], + sigTypes: [0], + args: [taskId, SPECIFICATION_HASH_UPDATED] + }), + "colony-task-finalized" + ); + }); + + it("should fail to change task manager, if the task is complete", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await forwardTime(90 * SECONDS_PER_DAY); + await tasks.completeTask(taskId, { from: MANAGER }); + + await checkErrorRevert( + executeSignedRoleAssignment({ + tasks, + taskId, + functionName: "setTaskManagerRole", + signers: [MANAGER, ADMIN], + sigTypes: [0, 0], + args: [taskId, ADMIN, 1, 0] + }), + "colony-task-role-assignment-execution-failed" + ); + }); + + it("should log a TaskBriefSet event, if the task brief gets changed", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await expectEvent( + executeSignedTaskChange({ + tasks, + taskId, + functionName: "setTaskBrief", + signers: [MANAGER], + sigTypes: [0], + args: [taskId, SPECIFICATION_HASH_UPDATED] + }), + "TaskBriefSet" + ); + }); + + it("should log a TaskDueDateSet event, if the task due date gets changed", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + const dueDate = await currentBlockTime(); + await expectEvent( + executeSignedTaskChange({ + tasks, + taskId, + functionName: "setTaskDueDate", + signers: [MANAGER], + sigTypes: [0], + args: [taskId, dueDate] + }), + "TaskDueDateSet" + ); + }); + + it("should fail to execute task change with a non zero value", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + const { sigV, sigR, sigS, txData } = await getSigsAndTransactionData({ + tasks, + taskId, + functionName: "setTaskBrief", + signers: [MANAGER], + sigTypes: [0], + args: [taskId, SPECIFICATION_HASH_UPDATED] + }); + + await checkErrorRevert(tasks.executeTaskChange(sigV, sigR, sigS, [0], 100, txData), "colony-task-change-non-zero-value"); + }); + + it("should fail to execute task change with a mismatched set of signature parts", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + const { sigV, sigR, sigS, txData } = await getSigsAndTransactionData({ + tasks, + taskId, + functionName: "setTaskBrief", + signers: [MANAGER, WORKER], + sigTypes: [0, 0], + args: [taskId, SPECIFICATION_HASH_UPDATED] + }); + + await checkErrorRevert(tasks.executeTaskChange([sigV[0]], sigR, sigS, [0], 0, txData), "colony-task-change-signatures-count-do-not-match"); + }); + + it("should fail to execute task change send for a task role assignment call (which should be using executeTaskRoleAssignment)", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + const { sigV, sigR, sigS, txData } = await getSigsAndTransactionData({ + tasks, + taskId, + functionName: "setTaskEvaluatorRole", + signers: [MANAGER], + sigTypes: [0], + args: [taskId, "0x29738B9BB168790211D84C99c4AEAd215c34D731"] + }); + + await checkErrorRevert(tasks.executeTaskChange(sigV, sigR, sigS, [0], 0, txData), "colony-task-change-is-role-assignment"); + }); + + it("should fail to execute task change with the wrong signatures, one signer", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + const { sigV, sigR, sigS, txData } = await getSigsAndTransactionData({ + tasks, + taskId, + functionName: "setTaskBrief", + signers: [OTHER], + sigTypes: [0], + args: [taskId, SPECIFICATION_HASH_UPDATED] + }); + + await checkErrorRevert(tasks.executeTaskChange(sigV, sigR, sigS, [0], 0, txData), "colony-task-signatures-do-not-match-reviewer-1"); + }); + + it("should fail to execute task change with the wrong signatures, two signers", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await executeSignedRoleAssignment({ + tasks, + taskId, + functionName: "setTaskWorkerRole", + signers: [MANAGER, WORKER], + sigTypes: [0, 0], + args: [taskId, WORKER] + }); + + const { sigV, sigR, sigS, txData } = await getSigsAndTransactionData({ + tasks, + taskId, + functionName: "setTaskBrief", + signers: [MANAGER, OTHER], + sigTypes: [0, 0], + args: [taskId, SPECIFICATION_HASH_UPDATED] + }); + + await checkErrorRevert(tasks.executeTaskChange(sigV, sigR, sigS, [0, 0], 0, txData), "colony-task-signatures-do-not-match-reviewer-2"); + }); + }); + + describe("when submitting task deliverable", () => { + it("should update task", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + await assignRoles({ tasks, taskId, manager: MANAGER, evaluator: EVALUATOR, worker: WORKER }); + + const tx = await tasks.submitTaskDeliverable(taskId, DELIVERABLE_HASH, { from: WORKER }); + + const task = await tasks.getTask(taskId); + const currTime = await getBlockTime(tx.receipt.blockNumber); + expect(task.deliverableHash).to.equal(DELIVERABLE_HASH); + expect(task.completionTimestamp).to.eq.BN(currTime); + }); + + it("should fail if I try to submit work for a task that is complete", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + await assignRoles({ tasks, taskId, manager: MANAGER, evaluator: EVALUATOR, worker: WORKER }); + + await forwardTime(90 * SECONDS_PER_DAY); + await tasks.completeTask(taskId, { from: MANAGER }); + + await checkErrorRevert(tasks.submitTaskDeliverable(taskId, DELIVERABLE_HASH), "colony-task-complete"); + }); + + it("should fail if I try to submit work for a task that is finalized", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); + await submitDeliverableAndRatings({ tasks, taskId }); + await tasks.finalizeTask(1, 0, taskId, { from: MANAGER }); + + await checkErrorRevert(tasks.submitTaskDeliverable(taskId, DELIVERABLE_HASH, { from: WORKER }), "colony-task-complete"); + }); + + it("should succeed if I try to submit work for a task that is past its due date but not yet marked as complete", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); + await forwardTime(SECONDS_PER_DAY * 90 + 1); + await tasks.submitTaskDeliverable(taskId, DELIVERABLE_HASH, { from: WORKER }); + + const task = await tasks.getTask(taskId); + expect(task.deliverableHash).to.equal(DELIVERABLE_HASH); + }); + + it("should fail if I try to submit work for a task using an invalid id", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await checkErrorRevert(tasks.submitTaskDeliverable(taskId.addn(1), DELIVERABLE_HASH), "colony-task-does-not-exist"); + }); + + it("should fail if I try to submit work twice", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); + await tasks.submitTaskDeliverable(taskId, DELIVERABLE_HASH, { from: WORKER }); + + await checkErrorRevert(tasks.submitTaskDeliverable(taskId, SPECIFICATION_HASH, { from: WORKER }), "colony-task-complete"); + }); + + it("should fail if I try to mark a taske complete after work is submitted", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); + await tasks.submitTaskDeliverable(taskId, DELIVERABLE_HASH, { from: WORKER }); + + await checkErrorRevert(tasks.completeTask(taskId, { from: MANAGER }), "colony-task-complete"); + }); + + it("should fail if I try to submit work if I'm not the assigned worker", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await assignRoles({ tasks, taskId, manager: MANAGER, worker: OTHER }); + await checkErrorRevert(tasks.submitTaskDeliverable(taskId, SPECIFICATION_HASH, { from: WORKER }), "colony-task-role-identity-mismatch"); + }); + + it("should log a TaskDeliverableSubmitted event", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); + await expectEvent(tasks.submitTaskDeliverable(taskId, DELIVERABLE_HASH, { from: WORKER }), "TaskDeliverableSubmitted"); + }); + + it("should fail if I try to complete the task before the due date", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); + await checkErrorRevert(tasks.completeTask(taskId, { from: MANAGER }), "colony-task-due-date-in-future"); + }); + }); + + describe("when evaluating a task", () => { + it("should fail if I try to evaluate before work is submitted", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await checkErrorRevert(tasks.submitTaskWorkRating(taskId, WORKER_ROLE, RATING_2_SECRET, { from: EVALUATOR }), "colony-task-not-complete"); + }); + + it("should fail if I try to evaluate twice", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); + await tasks.submitTaskDeliverableAndRating(taskId, DELIVERABLE_HASH, RATING_1_SECRET, { from: WORKER }); + + await checkErrorRevert( + tasks.submitTaskWorkRating(taskId, MANAGER_ROLE, RATING_1_SECRET, { from: WORKER }), + "colony-task-rating-secret-already-exists" + ); + }); + + it("should fail if the wrong user tries to rate the wrong role", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); + await tasks.submitTaskDeliverable(taskId, DELIVERABLE_HASH, { from: WORKER }); + + const SECRET = soliditySha3("secret"); + await checkErrorRevert(tasks.submitTaskWorkRating(taskId, MANAGER_ROLE, SECRET, { from: OTHER }), "colony-user-cannot-rate-task-manager"); + await checkErrorRevert(tasks.submitTaskWorkRating(taskId, WORKER_ROLE, SECRET, { from: OTHER }), "colony-user-cannot-rate-task-worker"); + await checkErrorRevert(tasks.submitTaskWorkRating(taskId, EVALUATOR_ROLE, SECRET), "colony-unsupported-role-to-rate"); + }); + + it("can retreive rating secret information", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); + + const tx = await tasks.submitTaskDeliverableAndRating(taskId, DELIVERABLE_HASH, RATING_1_SECRET, { from: WORKER }); + const currTime = await getBlockTime(tx.receipt.blockNumber); + + const ratingSecretsInfo = await tasks.getTaskWorkRatingSecretsInfo(taskId); + expect(ratingSecretsInfo[0]).to.eq.BN(1); + expect(ratingSecretsInfo[1]).to.eq.BN(currTime); + + const ratingSecret = await tasks.getTaskWorkRatingSecret(taskId, MANAGER_ROLE); + expect(ratingSecret).to.eq.BN(RATING_1_SECRET); + }); + + it("should fail if the user tries to rate too late", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); + + await tasks.submitTaskDeliverableAndRating(taskId, DELIVERABLE_HASH, RATING_1_SECRET, { from: WORKER }); + + await forwardTime(SECONDS_PER_DAY * 5 + 1); + await checkErrorRevert( + tasks.submitTaskWorkRating(taskId, WORKER_ROLE, RATING_2_SECRET, { from: MANAGER }), + "colony-task-rating-secret-submit-period-closed" + ); + }); + + it("should not allow a user to reveal after the deadline, with two secrets", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); + + await tasks.submitTaskDeliverableAndRating(taskId, DELIVERABLE_HASH, RATING_1_SECRET, { from: WORKER }); + await tasks.submitTaskWorkRating(taskId, WORKER_ROLE, RATING_2_SECRET, { from: MANAGER }); + + await forwardTime(SECONDS_PER_DAY * 5 + 1); + await checkErrorRevert( + tasks.revealTaskWorkRating(taskId, MANAGER_ROLE, MANAGER_RATING, RATING_1_SALT, { from: WORKER }), + "colony-task-rating-secret-reveal-period-closed" + ); + }); + + it("should not allow a user to reveal after the deadline, with one secret", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); + + await tasks.submitTaskDeliverableAndRating(taskId, DELIVERABLE_HASH, RATING_1_SECRET, { from: WORKER }); + + await forwardTime(SECONDS_PER_DAY * 10 + 1); + await checkErrorRevert( + tasks.revealTaskWorkRating(taskId, MANAGER_ROLE, MANAGER_RATING, RATING_1_SALT, { from: WORKER }), + "colony-task-rating-secret-reveal-period-closed" + ); + }); + + it("should not allow a user to reveal during the submission period", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); + + await tasks.submitTaskDeliverableAndRating(taskId, DELIVERABLE_HASH, RATING_1_SECRET, { from: WORKER }); + + await checkErrorRevert( + tasks.revealTaskWorkRating(taskId, MANAGER_ROLE, MANAGER_RATING, RATING_1_SALT, { from: WORKER }), + "colony-task-rating-secret-reveal-period-not-open" + ); + }); + + it("should not allow a user to reveal a non-matching rating", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); + + await tasks.submitTaskDeliverableAndRating(taskId, DELIVERABLE_HASH, soliditySha3(RATING_1_SALT, 3), { from: WORKER }); + await tasks.submitTaskWorkRating(taskId, WORKER_ROLE, RATING_2_SECRET, { from: MANAGER }); + + await checkErrorRevert( + tasks.revealTaskWorkRating(taskId, MANAGER_ROLE, 2, RATING_1_SALT, { from: WORKER }), + "colony-task-rating-secret-mismatch" + ); + }); + + it("should not allow a user to reveal a rating of None", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); + + await tasks.submitTaskDeliverableAndRating(taskId, DELIVERABLE_HASH, soliditySha3(RATING_1_SALT, 0), { from: WORKER }); + await tasks.submitTaskWorkRating(taskId, WORKER_ROLE, RATING_2_SECRET, { from: MANAGER }); + + await checkErrorRevert(tasks.revealTaskWorkRating(taskId, MANAGER_ROLE, 0, RATING_1_SALT, { from: WORKER }), "colony-task-rating-missing"); + }); + }); + + describe("when finalizing a task", () => { + it('should set the task "status" property to "finalized"', async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); + await submitDeliverableAndRatings({ tasks, taskId }); + await tasks.finalizeTask(1, 0, taskId, { from: MANAGER }); + + const task = await tasks.getTask(taskId); + const expenditure = await colony.getExpenditure(task.expenditureId); + expect(expenditure.status).to.eq.BN(FINALIZED_TASK_STATE); + }); + + it("should fail if I try to finalize a task twice", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); + await submitDeliverableAndRatings({ tasks, taskId }); + await tasks.finalizeTask(1, 0, taskId, { from: MANAGER }); + + await checkErrorRevert(tasks.finalizeTask(1, 0, taskId, { from: MANAGER }), "colony-expenditure-not-active"); + }); + + it("should fail if I try to finalize a task that is not complete", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await checkErrorRevert(tasks.finalizeTask(1, 0, taskId), "colony-task-not-complete"); + }); + + it("should fail if the task work ratings have not been assigned and they still have time to be", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); + await tasks.submitTaskDeliverable(taskId, SPECIFICATION_HASH, { from: WORKER }); + + await checkErrorRevert(tasks.finalizeTask(1, 0, taskId), "colony-task-ratings-not-closed"); + }); + + it("should fail if the task work ratings have not been revealed and they still have time to be", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); + await tasks.submitTaskDeliverableAndRating(taskId, SPECIFICATION_HASH, RATING_1_SECRET, { from: WORKER }); + await tasks.submitTaskWorkRating(taskId, WORKER_ROLE, RATING_2_SECRET, { from: MANAGER }); + + await checkErrorRevert(tasks.finalizeTask(1, 0, taskId), "colony-task-ratings-not-closed"); + }); + + it("should finalize if the rate and reveal period have elapsed", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); + await tasks.submitTaskDeliverable(taskId, SPECIFICATION_HASH, { from: WORKER }); + + // No ratings submitted, so must wait for both rate and reveal periods to elapse + await forwardTime(SECONDS_PER_DAY * 10 + 1); + await tasks.finalizeTask(1, 0, taskId, { from: MANAGER }); + }); + + it("should finalize if only the reveal period has elapsed after both secrets are submitted", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); + await tasks.submitTaskDeliverableAndRating(taskId, SPECIFICATION_HASH, RATING_1_SECRET, { from: WORKER }); + await tasks.submitTaskWorkRating(taskId, WORKER_ROLE, RATING_2_SECRET, { from: MANAGER }); + + await checkErrorRevert(tasks.finalizeTask(1, 0, taskId), "colony-task-ratings-not-closed"); + + // Both secrets submitted, so we only have to wait for the reveal period to elapse + await forwardTime(SECONDS_PER_DAY * 5 + 1); + await tasks.finalizeTask(1, 0, taskId, { from: MANAGER }); + }); + + it("should assign manager and worker maximum rating if unrated", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + const task = await tasks.getTask(taskId); + + await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); + await tasks.submitTaskDeliverable(taskId, SPECIFICATION_HASH, { from: WORKER }); + + forwardTime(SECONDS_PER_DAY * 10 + 1); + await tasks.finalizeTask(1, 0, taskId, { from: MANAGER }); + + const managerSlot = await colony.getExpenditureSlot(task.expenditureId, MANAGER_ROLE); + const evaluatorSlot = await colony.getExpenditureSlot(task.expenditureId, EVALUATOR_ROLE); + const workerSlot = await colony.getExpenditureSlot(task.expenditureId, WORKER_ROLE); + + expect(managerSlot.payoutModifier).to.eq.BN(WAD.divn(2)); // Implicit rating of 3 + expect(evaluatorSlot.payoutModifier).to.eq.BN(WAD.neg()); // Rating of 0 for failing to rate + expect(workerSlot.payoutModifier).to.be.zero; // Implicit rating of 3, minus 1 for rateFail, gives 2 + }); + + it("should fail if it's not sufficiently funded to support all its payouts", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await executeSignedTaskChange({ + tasks, + taskId, + functionName: "setTaskWorkerPayout", + signers: [MANAGER], + sigTypes: [0], + args: [taskId, token.address, WORKER_PAYOUT] + }); + + await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); + await submitDeliverableAndRatings({ tasks, taskId }); + + await checkErrorRevert(tasks.finalizeTask(1, 0, taskId), "colony-expenditure-not-funded"); + }); + + it("should fail if I try to accept a task that was finalized before", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); + await submitDeliverableAndRatings({ tasks, taskId }); + await tasks.finalizeTask(1, 0, taskId, { from: MANAGER }); + + await checkErrorRevert(tasks.finalizeTask(1, 0, taskId), "colony-expenditure-not-active"); + }); + + it("should fail if I try to accept a task using an invalid id", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await checkErrorRevert(tasks.finalizeTask(1, 0, taskId.addn(1)), "colony-task-does-not-exist"); + }); + + it("should emit two negative reputation updates for a bad worker rating", async () => { + await fundColonyWithTokens(colony, token, INITIAL_FUNDING); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + const task = await tasks.getTask(taskId); + const expenditure = await colony.getExpenditure(task.expenditureId); + + const addr = await colonyNetwork.getReputationMiningCycle(false); + const repCycle = await IReputationMiningCycle.at(addr); + const numEntriesBefore = await repCycle.getReputationUpdateLogLength(); + + await tasks.setAllTaskPayouts(taskId, token.address, 0, 0, WAD, { from: MANAGER }); + await colony.moveFundsBetweenPots(1, 0, 0, domain1.fundingPotId, expenditure.fundingPotId, WAD, token.address); + await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); + + // Worker doesn't rate, so extra penalties + await tasks.submitTaskDeliverable(taskId, SPECIFICATION_HASH, { from: WORKER }); + await tasks.submitTaskWorkRating(taskId, WORKER_ROLE, soliditySha3(RATING_2_SALT, 1), { from: MANAGER }); + await forwardTime(SECONDS_PER_DAY * 5 + 1); + await tasks.revealTaskWorkRating(taskId, WORKER_ROLE, 1, RATING_2_SALT, { from: MANAGER }); + await forwardTime(SECONDS_PER_DAY * 5 + 1); + await tasks.finalizeTask(1, 0, taskId, { from: MANAGER }); + + const numEntriesAfter = await repCycle.getReputationUpdateLogLength(); + expect(numEntriesAfter.sub(numEntriesBefore)).to.eq.BN(2); + + // Does not include rateFail penalty + const skillPenalty = await repCycle.getReputationUpdateLogEntry(numEntriesAfter.subn(1)); + expect(skillPenalty.user).to.equal(WORKER); + expect(skillPenalty.skillId).to.eq.BN(GLOBAL_SKILL_ID); + expect(skillPenalty.amount).to.eq.BN(WAD.neg()); + + // Includes rateFail penalty + const domainPenalty = await repCycle.getReputationUpdateLogEntry(numEntriesAfter.subn(2)); + expect(domainPenalty.user).to.equal(WORKER); + expect(domainPenalty.skillId).to.eq.BN(domain1.skillId); + expect(domainPenalty.amount).to.eq.BN(WAD.muln(3).divn(2).neg()); // eslint-disable-line prettier/prettier + }); + + it("should emit one negative reputation update for a bad manager/evaluator rating", async () => { + await fundColonyWithTokens(colony, token, INITIAL_FUNDING); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + const task = await tasks.getTask(taskId); + const expenditure = await colony.getExpenditure(task.expenditureId); + + const addr = await colonyNetwork.getReputationMiningCycle(false); + const repCycle = await IReputationMiningCycle.at(addr); + const numEntriesBefore = await repCycle.getReputationUpdateLogLength(); + + await tasks.setAllTaskPayouts(taskId, token.address, WAD, 0, 0, { from: MANAGER }); + await colony.moveFundsBetweenPots(1, 0, 0, domain1.fundingPotId, expenditure.fundingPotId, WAD, token.address); + await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); + await submitDeliverableAndRatings({ tasks, taskId, managerRating: 1 }); + await tasks.finalizeTask(1, 0, taskId, { from: MANAGER }); + + const numEntriesAfter = await repCycle.getReputationUpdateLogLength(); + expect(numEntriesAfter.sub(numEntriesBefore)).to.eq.BN(1); + + const domainPenalty = await repCycle.getReputationUpdateLogEntry(numEntriesAfter.subn(1)); + expect(domainPenalty.user).to.equal(MANAGER); + expect(domainPenalty.skillId).to.eq.BN(domain1.skillId); + expect(domainPenalty.amount).to.eq.BN(WAD.neg()); + }); + }); + + describe("when cancelling a task", () => { + it('should set the task "status" property to "cancelled"', async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await executeSignedTaskChange({ + tasks, + taskId, + functionName: "cancelTask", + signers: [MANAGER], + sigTypes: [0], + args: [taskId] + }); + + const task = await tasks.getTask(taskId); + const expenditure = await colony.getExpenditure(task.expenditureId); + expect(expenditure.status).to.eq.BN(CANCELLED_TASK_STATE); + }); + + it("should fail if manager tries to cancel a task that was completed", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); + await tasks.submitTaskDeliverable(taskId, SPECIFICATION_HASH, { from: WORKER }); + + await checkErrorRevert( + executeSignedTaskChange({ + tasks, + taskId, + functionName: "cancelTask", + signers: [MANAGER, WORKER], + sigTypes: [0, 0], + args: [taskId] + }), + "colony-task-change-execution-failed" + ); + }); + + it("should fail if manager tries to cancel a task with invalid id", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await checkErrorRevert( + executeSignedTaskChange({ + tasks, + taskId, + functionName: "cancelTask", + signers: [MANAGER], + sigTypes: [0], + args: [taskId.addn(1)] + }), + "colony-task-does-not-exist" + ); + }); + }); + + describe("when funding tasks", () => { + it("should be able to set the task payouts for different roles", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await executeSignedRoleAssignment({ + tasks, + taskId, + functionName: "setTaskWorkerRole", + signers: [MANAGER, WORKER], + sigTypes: [0, 0], + args: [taskId, WORKER] + }); + + // Set the manager payout as 5000 wei and 100 colony tokens + await executeSignedTaskChange({ + tasks, + taskId, + functionName: "setTaskManagerPayout", + signers: [MANAGER], + sigTypes: [0], + args: [taskId, ethers.constants.AddressZero, 5000] + }); + + await executeSignedTaskChange({ + tasks, + taskId, + functionName: "setTaskManagerPayout", + signers: [MANAGER], + sigTypes: [0], + args: [taskId, token.address, 100] + }); + + // Set the evaluator payout as 1000 ethers + await executeSignedTaskChange({ + tasks, + taskId, + functionName: "setTaskEvaluatorPayout", + signers: [MANAGER], + sigTypes: [0], + args: [taskId, ethers.constants.AddressZero, 1000] + }); + + // Set the evaluator payout as 40 colony tokens + await executeSignedTaskChange({ + tasks, + taskId, + functionName: "setTaskEvaluatorPayout", + signers: [MANAGER], + sigTypes: [0], + args: [taskId, token.address, 40] + }); + + // Set the worker payout as 98000 wei and 200 colony tokens + await executeSignedTaskChange({ + tasks, + taskId, + functionName: "setTaskWorkerPayout", + signers: [MANAGER, WORKER], + sigTypes: [0, 0], + args: [taskId, ethers.constants.AddressZero, 98000] + }); + + await executeSignedTaskChange({ + tasks, + taskId, + functionName: "setTaskWorkerPayout", + signers: [MANAGER, WORKER], + sigTypes: [0, 0], + args: [taskId, token.address, 200] + }); + + const task = await tasks.getTask(taskId); + const taskPayoutManager1 = await colony.getExpenditureSlotPayout(task.expenditureId, MANAGER_ROLE, ethers.constants.AddressZero); + expect(taskPayoutManager1).to.eq.BN(5000); + const taskPayoutManager2 = await colony.getExpenditureSlotPayout(task.expenditureId, MANAGER_ROLE, token.address); + expect(taskPayoutManager2).to.eq.BN(100); + + const taskPayoutEvaluator1 = await colony.getExpenditureSlotPayout(task.expenditureId, EVALUATOR_ROLE, ethers.constants.AddressZero); + expect(taskPayoutEvaluator1).to.eq.BN(1000); + const taskPayoutEvaluator2 = await colony.getExpenditureSlotPayout(task.expenditureId, EVALUATOR_ROLE, token.address); + expect(taskPayoutEvaluator2).to.eq.BN(40); + + const taskPayoutWorker1 = await colony.getExpenditureSlotPayout(task.expenditureId, WORKER_ROLE, ethers.constants.AddressZero); + expect(taskPayoutWorker1).to.eq.BN(98000); + const taskPayoutWorker2 = await colony.getExpenditureSlotPayout(task.expenditureId, WORKER_ROLE, token.address); + expect(taskPayoutWorker2).to.eq.BN(200); + }); + + it("should be able (if manager) to set all payments at once if evaluator and worker are manager or unassigned", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + await checkErrorRevert( + tasks.setAllTaskPayouts(taskId, ethers.constants.AddressZero, 5000, 1000, 98000, { from: OTHER }), + "colony-task-role-identity-mismatch" + ); + await tasks.setAllTaskPayouts(taskId, ethers.constants.AddressZero, 5000, 1000, 98000); + + const taskPayoutManager = await colony.getExpenditureSlotPayout(taskId, MANAGER_ROLE, ethers.constants.AddressZero); + expect(taskPayoutManager).to.eq.BN(5000); + + const taskPayoutEvaluator = await colony.getExpenditureSlotPayout(taskId, EVALUATOR_ROLE, ethers.constants.AddressZero); + expect(taskPayoutEvaluator).to.eq.BN(1000); + + const taskPayoutWorker = await colony.getExpenditureSlotPayout(taskId, WORKER_ROLE, ethers.constants.AddressZero); + expect(taskPayoutWorker).to.eq.BN(98000); + }); + + it("should not be able to set all payments at once if worker is assigned and is not the manager", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await executeSignedRoleAssignment({ + tasks, + taskId, + functionName: "setTaskWorkerRole", + signers: [MANAGER, WORKER], + sigTypes: [0, 0], + args: [taskId, WORKER] + }); + + await checkErrorRevert(tasks.setAllTaskPayouts(taskId, ethers.constants.AddressZero, 5000, 1000, 98000), "colony-funding-worker-already-set"); + }); + + it("should not be able to set all payments at once if evaluator is assigned and is not the manager", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await executeSignedTaskChange({ + tasks, + taskId, + functionName: "removeTaskEvaluatorRole", + signers: [MANAGER], + sigTypes: [0], + args: [taskId] + }); + + await executeSignedRoleAssignment({ + tasks, + taskId, + functionName: "setTaskEvaluatorRole", + signers: [MANAGER, EVALUATOR], + sigTypes: [0, 0], + args: [taskId, EVALUATOR] + }); + + await checkErrorRevert( + tasks.setAllTaskPayouts(taskId, ethers.constants.AddressZero, 5000, 1000, 98000), + "colony-funding-evaluator-already-set" + ); + }); + + it("should correctly return the current total payout", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await tasks.setAllTaskPayouts(taskId, token.address, MANAGER_PAYOUT, EVALUATOR_PAYOUT, WORKER_PAYOUT); + + const task = await tasks.getTask(taskId); + const expenditure = await colony.getExpenditure(task.expenditureId); + const totalTokenPayout = await colony.getFundingPotPayout(expenditure.fundingPotId, token.address); + expect(totalTokenPayout).to.eq.BN(MANAGER_PAYOUT.add(EVALUATOR_PAYOUT).add(WORKER_PAYOUT)); + }); + + it("should be possible to return funds back to the domain if cancelled", async () => { + await fundColonyWithTokens(colony, token, INITIAL_FUNDING); + + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await tasks.setAllTaskPayouts(taskId, token.address, 0, 0, WAD); + await tasks.setAllTaskPayouts(taskId, ethers.constants.AddressZero, 0, 0, WAD); + + const task = await tasks.getTask(taskId); + const expenditure = await colony.getExpenditure(task.expenditureId); + + await colony.send(WAD); + await colony.claimColonyFunds(ethers.constants.AddressZero); + await colony.moveFundsBetweenPots(1, 0, 0, domain1.fundingPotId, expenditure.fundingPotId, WAD, ethers.constants.AddressZero); + await colony.moveFundsBetweenPots(1, 0, 0, domain1.fundingPotId, expenditure.fundingPotId, WAD, token.address); + + // Keep track of original Ether balance in funding pots + const originalDomainEtherBalance = await colony.getFundingPotBalance(domain1.fundingPotId, ethers.constants.AddressZero); + const originalTaskEtherBalance = await colony.getFundingPotBalance(expenditure.fundingPotId, ethers.constants.AddressZero); + + // And same for the token + const originalDomainTokenBalance = await colony.getFundingPotBalance(domain1.fundingPotId, token.address); + const originalTaskTokenBalance = await colony.getFundingPotBalance(expenditure.fundingPotId, token.address); + + // Can't withdraw funds for active task... + await checkErrorRevert( + colony.moveFundsBetweenPots(1, 0, 0, expenditure.fundingPotId, domain1.fundingPotId, WAD, token.address), + "colony-funding-expenditure-bad-state" + ); + + // Now that everything is set up, let's cancel the task, move funds and compare funding pots afterwards + await executeSignedTaskChange({ + tasks, + taskId, + functionName: "cancelTask", + signers: [MANAGER], + sigTypes: [0], + args: [taskId] + }); + + await colony.moveFundsBetweenPots(1, 0, 0, expenditure.fundingPotId, domain1.fundingPotId, WAD, ethers.constants.AddressZero); + await colony.moveFundsBetweenPots(1, 0, 0, expenditure.fundingPotId, domain1.fundingPotId, WAD, token.address); + + const cancelledTaskEtherBalance = await colony.getFundingPotBalance(expenditure.fundingPotId, ethers.constants.AddressZero); + const cancelledDomainEtherBalance = await colony.getFundingPotBalance(domain1.fundingPotId, ethers.constants.AddressZero); + const cancelledTaskTokenBalance = await colony.getFundingPotBalance(expenditure.fundingPotId, token.address); + const cancelledDomainTokenBalance = await colony.getFundingPotBalance(domain1.fundingPotId, token.address); + + expect(originalTaskEtherBalance).to.not.eq.BN(cancelledTaskEtherBalance); + expect(originalDomainEtherBalance).to.not.eq.BN(cancelledDomainEtherBalance); + expect(originalTaskTokenBalance).to.not.eq.BN(cancelledTaskTokenBalance); + expect(originalDomainTokenBalance).to.not.eq.BN(cancelledDomainTokenBalance); + + expect(cancelledTaskEtherBalance).to.be.zero; + expect(cancelledTaskTokenBalance).to.be.zero; + + expect(originalDomainEtherBalance.add(originalTaskEtherBalance)).to.eq.BN(cancelledDomainEtherBalance); + expect(originalDomainTokenBalance.add(originalTaskTokenBalance)).to.eq.BN(cancelledDomainTokenBalance); + }); + }); + + describe("when claiming payout for a task", () => { + it("should payout agreed ether and tokens for a task", async () => { + await fundColonyWithTokens(colony, token, INITIAL_FUNDING); + + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + // Setup payouts + await tasks.setAllTaskPayouts(taskId, ethers.constants.AddressZero, 0, 0, WAD); + await tasks.setAllTaskPayouts(taskId, token.address, 0, 0, WORKER_PAYOUT); + + const task = await tasks.getTask(taskId); + const expenditure = await colony.getExpenditure(task.expenditureId); + + await colony.send(WAD); + await colony.claimColonyFunds(ethers.constants.AddressZero); + await colony.moveFundsBetweenPots(1, 0, 0, domain1.fundingPotId, expenditure.fundingPotId, WAD, ethers.constants.AddressZero); + await colony.moveFundsBetweenPots(1, 0, 0, domain1.fundingPotId, expenditure.fundingPotId, WORKER_PAYOUT, token.address); + + // Complete task + await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); + await submitDeliverableAndRatings({ tasks, taskId }); + await tasks.finalizeTask(1, 0, taskId, { from: MANAGER }); + + // Claim payouts + const workerEtherBalanceBefore = await web3GetBalance(WORKER); + await colony.claimExpenditurePayout(task.expenditureId, WORKER_ROLE, ethers.constants.AddressZero); + + const workerEtherBalanceAfter = await web3GetBalance(WORKER); + expect(new BN(workerEtherBalanceAfter).sub(new BN(workerEtherBalanceBefore))).to.eq.BN(WAD.subn(1)); + + const workerBalanceBefore = await token.balanceOf(WORKER); + await colony.claimExpenditurePayout(task.expenditureId, WORKER_ROLE, token.address); + + const workerBalanceAfter = await token.balanceOf(WORKER); + expect(workerBalanceAfter.sub(workerBalanceBefore)).to.eq.BN(WORKER_PAYOUT.subn(1)); + }); + + it("should disburse nothing for unsatisfactory work, for manager and worker", async () => { + await fundColonyWithTokens(colony, token, INITIAL_FUNDING); + + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await tasks.setAllTaskPayouts(taskId, token.address, MANAGER_PAYOUT, EVALUATOR_PAYOUT, WORKER_PAYOUT); + + const task = await tasks.getTask(taskId); + const expenditure = await colony.getExpenditure(task.expenditureId); + const totalPayout = MANAGER_PAYOUT.add(EVALUATOR_PAYOUT).add(WORKER_PAYOUT); + await colony.moveFundsBetweenPots(1, 0, 0, 1, expenditure.fundingPotId, totalPayout, token.address); + + await assignRoles({ tasks, taskId, manager: MANAGER, evaluator: EVALUATOR, worker: WORKER }); + await submitDeliverableAndRatings({ tasks, taskId, managerRating: 1, workerRating: 1 }); + await tasks.finalizeTask(1, 0, taskId, { from: MANAGER }); + + const managerSlot = await colony.getExpenditureSlot(task.expenditureId, MANAGER_ROLE); + const evaluatorSlot = await colony.getExpenditureSlot(task.expenditureId, EVALUATOR_ROLE); + const workerSlot = await colony.getExpenditureSlot(task.expenditureId, WORKER_ROLE); + + expect(managerSlot.payoutModifier).to.eq.BN(WAD.neg()); // rating of 1 + expect(evaluatorSlot.payoutModifier).to.be.zero; // rating of 2 + expect(workerSlot.payoutModifier).to.eq.BN(WAD.neg()); // rating of 1 + + const managerBalanceBefore = await token.balanceOf(MANAGER); + const evaluatorBalanceBefore = await token.balanceOf(EVALUATOR); + const workerBalanceBefore = await token.balanceOf(WORKER); + + await colony.claimExpenditurePayout(task.expenditureId, MANAGER_ROLE, token.address); + await colony.claimExpenditurePayout(task.expenditureId, EVALUATOR_ROLE, token.address); + await colony.claimExpenditurePayout(task.expenditureId, WORKER_ROLE, token.address); + + const managerBalanceAfter = await token.balanceOf(MANAGER); + const evaluatorBalanceAfter = await token.balanceOf(EVALUATOR); + const workerBalanceAfter = await token.balanceOf(WORKER); + + expect(managerBalanceAfter.sub(managerBalanceBefore)).to.be.zero; + expect(evaluatorBalanceAfter.sub(evaluatorBalanceBefore)).to.eq.BN(EVALUATOR_PAYOUT.subn(1)); + expect(workerBalanceAfter.sub(workerBalanceBefore)).to.be.zero; + }); + + it("should disburse nothing for unsatisfactory work, for evaluator", async () => { + await fundColonyWithTokens(colony, token, INITIAL_FUNDING); + + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await tasks.setAllTaskPayouts(taskId, token.address, MANAGER_PAYOUT, EVALUATOR_PAYOUT, WORKER_PAYOUT); + + const task = await tasks.getTask(taskId); + const expenditure = await colony.getExpenditure(task.expenditureId); + const totalPayout = MANAGER_PAYOUT.add(EVALUATOR_PAYOUT).add(WORKER_PAYOUT); + await colony.moveFundsBetweenPots(1, 0, 0, 1, expenditure.fundingPotId, totalPayout, token.address); + + await assignRoles({ tasks, taskId, manager: MANAGER, evaluator: EVALUATOR, worker: WORKER }); + + await tasks.submitTaskDeliverableAndRating(taskId, SPECIFICATION_HASH, RATING_1_SECRET, { from: WORKER }); + await forwardTime(SECONDS_PER_DAY * 5 + 1); + await tasks.revealTaskWorkRating(taskId, MANAGER_ROLE, MANAGER_RATING, RATING_1_SALT, { from: WORKER }); + await forwardTime(SECONDS_PER_DAY * 5 + 1); + await tasks.finalizeTask(1, 0, taskId, { from: MANAGER }); + + const managerSlot = await colony.getExpenditureSlot(task.expenditureId, MANAGER_ROLE); + const evaluatorSlot = await colony.getExpenditureSlot(task.expenditureId, EVALUATOR_ROLE); + const workerSlot = await colony.getExpenditureSlot(task.expenditureId, WORKER_ROLE); + + expect(managerSlot.payoutModifier).to.be.zero; // Rating of 2 + expect(evaluatorSlot.payoutModifier).to.eq.BN(WAD.neg()); // Rating of 1 + expect(workerSlot.payoutModifier).to.eq.BN(WAD.divn(2)); // Implicit rating of 3 + + const managerPayout = await colony.getExpenditureSlotPayout(task.expenditureId, MANAGER_ROLE, token.address); + const evaluatorPayout = await colony.getExpenditureSlotPayout(task.expenditureId, EVALUATOR_ROLE, token.address); + const workerPayout = await colony.getExpenditureSlotPayout(task.expenditureId, WORKER_ROLE, token.address); + + expect(managerPayout).to.eq.BN(MANAGER_PAYOUT); + expect(evaluatorPayout).to.eq.BN(EVALUATOR_PAYOUT); + expect(workerPayout).to.eq.BN(WORKER_PAYOUT); + + const managerBalanceBefore = await token.balanceOf(MANAGER); + const evaluatorBalanceBefore = await token.balanceOf(EVALUATOR); + const workerBalanceBefore = await token.balanceOf(WORKER); + + await colony.claimExpenditurePayout(task.expenditureId, MANAGER_ROLE, token.address); + await colony.claimExpenditurePayout(task.expenditureId, EVALUATOR_ROLE, token.address); + await colony.claimExpenditurePayout(task.expenditureId, WORKER_ROLE, token.address); + + const managerBalanceAfter = await token.balanceOf(MANAGER); + const evaluatorBalanceAfter = await token.balanceOf(EVALUATOR); + const workerBalanceAfter = await token.balanceOf(WORKER); + + expect(managerBalanceAfter.sub(managerBalanceBefore)).to.eq.BN(MANAGER_PAYOUT.subn(1)); + expect(evaluatorBalanceAfter.sub(evaluatorBalanceBefore)).to.be.zero; + expect(workerBalanceAfter.sub(workerBalanceBefore)).to.eq.BN(WORKER_PAYOUT.subn(1)); + }); + + it("should automatically reclaim funds after unsatisfactory reviews", async () => { + await fundColonyWithTokens(colony, token, INITIAL_FUNDING); + + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await tasks.setAllTaskPayouts(taskId, token.address, 0, 0, WORKER_PAYOUT); + + const task = await tasks.getTask(taskId); + const expenditure = await colony.getExpenditure(task.expenditureId); + await colony.moveFundsBetweenPots(1, 0, 0, domain1.fundingPotId, expenditure.fundingPotId, WORKER_PAYOUT, token.address); + + await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); + await submitDeliverableAndRatings({ tasks, taskId, workerRating: 1 }); + await tasks.finalizeTask(1, 0, taskId, { from: MANAGER }); + + const balanceBefore = await colony.getFundingPotBalance(domain1.fundingPotId, token.address); + await colony.claimExpenditurePayout(task.expenditureId, WORKER_ROLE, token.address); + const balanceAfter = await colony.getFundingPotBalance(domain1.fundingPotId, token.address); + expect(balanceAfter.sub(balanceBefore)).to.eq.BN(WORKER_PAYOUT); + }); + + it("should return error when task is not finalized", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + const task = await tasks.getTask(taskId); + await checkErrorRevert(colony.claimExpenditurePayout(task.expenditureId, MANAGER_ROLE, token.address), "colony-expenditure-not-finalized"); + }); + }); +}); From b7574a4eb4836e8088c2411a8e2664623c7253ee Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Sun, 29 Sep 2019 17:34:03 +0300 Subject: [PATCH 3/4] Optimize bytecode for Tasks.sol --- contracts/extensions/Tasks.sol | 100 +++++++++++++----------- test/extensions/tasks.js | 135 +++++++++++++++------------------ 2 files changed, 114 insertions(+), 121 deletions(-) diff --git a/contracts/extensions/Tasks.sol b/contracts/extensions/Tasks.sol index a9a74afc1e..d25870cfda 100644 --- a/contracts/extensions/Tasks.sol +++ b/contracts/extensions/Tasks.sol @@ -116,33 +116,33 @@ contract Tasks is DSMath { } modifier self() { - require(address(this) == msg.sender, "colony-task-not-self"); + require(address(this) == msg.sender, "task-not-self"); _; } + ColonyDataTypes.ColonyRole constant ADMIN = ColonyDataTypes.ColonyRole.Administration; modifier isAdmin(address _user, uint256 _permissionDomainId, uint256 _childSkillIndex, uint256 _domainId) { - ColonyDataTypes.ColonyRole admin = ColonyDataTypes.ColonyRole.Administration; - require(colony.hasInheritedUserRole(_user, _permissionDomainId, admin, _childSkillIndex, _domainId), "colony-task-not-admin"); + require(colony.hasInheritedUserRole(_user, _permissionDomainId, ADMIN, _childSkillIndex, _domainId), "task-not-admin"); _; } modifier taskExists(uint256 _id) { - require(_id > 0 && _id <= taskCount, "colony-task-does-not-exist"); + require(doesTaskExist(_id), "task-does-not-exist"); _; } modifier taskComplete(uint256 _id) { - require(tasks[_id].completionTimestamp > 0, "colony-task-not-complete"); + require(isTaskComplete(_id), "task-not-complete"); _; } modifier taskNotComplete(uint256 _id) { - require(tasks[_id].completionTimestamp == 0, "colony-task-complete"); + require(!isTaskComplete(_id), "task-complete"); _; } modifier confirmTaskRoleIdentity(uint256 _id, address _user, TaskRole _role) { - require(msg.sender == getTaskRoleUser(_id, _role), "colony-task-role-identity-mismatch"); + require(getTaskRoleUser(_id, _role) == msg.sender, "task-role-identity-mismatch"); _; } @@ -156,17 +156,17 @@ contract Tasks is DSMath { ) public { - require(_value == 0, "colony-task-change-non-zero-value"); - require(_sigR.length == _sigS.length && _sigR.length == _sigV.length, "colony-task-change-signatures-count-do-not-match"); + require(_value == 0, "task-change-non-zero-value"); + require(_sigR.length == _sigS.length && _sigR.length == _sigV.length, "task-change-sig-count-no-match"); bytes4 sig; uint256 taskId; (sig, taskId) = deconstructCall(_data); - require(taskId > 0 && taskId <= taskCount, "colony-task-does-not-exist"); - require(!roleAssignmentSigs[sig], "colony-task-change-is-role-assignment"); + require(taskId > 0 && taskId <= taskCount, "task-does-not-exist"); + require(!roleAssignmentSigs[sig], "task-change-is-role-assign"); ColonyDataTypes.ExpenditureStatus status = colony.getExpenditure(tasks[taskId].expenditureId).status; - require(status != ColonyDataTypes.ExpenditureStatus.Finalized, "colony-task-finalized"); + require(status != ColonyDataTypes.ExpenditureStatus.Finalized, "task-finalized"); uint8 nSignaturesRequired; address taskRole1User = getTaskRoleUser(taskId, TaskRole(reviewers[sig][0])); @@ -180,26 +180,26 @@ contract Tasks is DSMath { } else { nSignaturesRequired = 2; } - require(_sigR.length == nSignaturesRequired, "colony-task-change-does-not-meet-signatures-required"); + require(_sigR.length == nSignaturesRequired, "task-change-wrong-num-sigs"); bytes32 msgHash = keccak256(abi.encodePacked(address(this), address(this), _value, _data, tasks[taskId].changeNonce)); address[] memory reviewerAddresses = getReviewerAddresses(_sigV, _sigR, _sigS, _mode, msgHash); require( reviewerAddresses[0] == taskRole1User || reviewerAddresses[0] == taskRole2User, - "colony-task-signatures-do-not-match-reviewer-1" + "task-sigs-no-match-reviewer-1" ); if (nSignaturesRequired == 2) { - require(reviewerAddresses[0] != reviewerAddresses[1], "colony-task-duplicate-reviewers"); + require(reviewerAddresses[0] != reviewerAddresses[1], "task-duplicate-reviewers"); require( reviewerAddresses[1] == taskRole1User || reviewerAddresses[1] == taskRole2User, - "colony-task-signatures-do-not-match-reviewer-2" + "task-sigs-no-match-reviewer-2" ); } tasks[taskId].changeNonce += 1; - require(executeCall(address(this), _value, _data), "colony-task-change-execution-failed"); + require(executeCall(address(this), _value, _data), "task-change-execution-failed"); } function executeTaskRoleAssignment( @@ -212,18 +212,18 @@ contract Tasks is DSMath { ) public { - require(_value == 0, "colony-task-role-assignment-non-zero-value"); - require(_sigR.length == _sigS.length && _sigR.length == _sigV.length, "colony-task-role-assignment-signatures-count-do-not-match"); + require(_value == 0, "task-role-assign-non-zero-value"); + require(_sigR.length == _sigS.length && _sigR.length == _sigV.length, "task-role-assign-sig-count-no-match"); bytes4 sig; uint256 taskId; address userAddress; (sig, taskId, userAddress) = deconstructRoleChangeCall(_data); - require(taskId > 0 && taskId <= taskCount, "colony-task-does-not-exist"); - require(roleAssignmentSigs[sig], "colony-task-change-is-not-role-assignment"); + require(taskId > 0 && taskId <= taskCount, "task-does-not-exist"); + require(roleAssignmentSigs[sig], "task-change-is-not-role-assign"); ColonyDataTypes.ExpenditureStatus status = colony.getExpenditure(tasks[taskId].expenditureId).status; - require(status != ColonyDataTypes.ExpenditureStatus.Finalized, "colony-task-finalized"); + require(status != ColonyDataTypes.ExpenditureStatus.Finalized, "task-finalized"); uint8 nSignaturesRequired; address manager = getTaskRoleUser(taskId, TaskRole.Manager); @@ -233,36 +233,36 @@ contract Tasks is DSMath { } else { nSignaturesRequired = 2; } - require(_sigR.length == nSignaturesRequired, "colony-task-role-assignment-does-not-meet-required-signatures"); + require(_sigR.length == nSignaturesRequired, "task-role-assign-wrong-num-sigs"); bytes32 msgHash = keccak256(abi.encodePacked(address(this), address(this), _value, _data, tasks[taskId].changeNonce)); address[] memory reviewerAddresses = getReviewerAddresses(_sigV, _sigR, _sigS, _mode, msgHash); if (nSignaturesRequired == 1) { // Since we want to set a manager as an evaluator, require just manager's signature - require(reviewerAddresses[0] == manager, "colony-task-role-assignment-not-signed-by-manager"); + require(reviewerAddresses[0] == manager, "task-role-assign-no-manager-sig"); } else { // One of signers must be a manager require( reviewerAddresses[0] == manager || reviewerAddresses[1] == manager, - "colony-task-role-assignment-not-signed-by-manager" + "task-role-assign-no-manager-sig" ); // One of the signers must be an address we want to set here require( userAddress == reviewerAddresses[0] || userAddress == reviewerAddresses[1], - "colony-task-role-assignment-not-signed-by-new-user-for-role" + "task-role-assign-no-new-user-sig" ); // Require that signatures are not from the same address // This will never throw, because we require that manager is one of the signers, // and if manager is both signers, then `userAddress` must also be a manager, and if // `userAddress` is a manager, then we require 1 signature (will be kept for possible future changes) - require(reviewerAddresses[0] != reviewerAddresses[1], "colony-task-role-assignment-duplicate-signatures"); + require(reviewerAddresses[0] != reviewerAddresses[1], "task-role-assign-duplicate-sigs"); } tasks[taskId].changeNonce += 1; - require(executeCall(address(this), _value, _data), "colony-task-role-assignment-execution-failed"); + require(executeCall(address(this), _value, _data), "task-role-assign-exec-failed"); } // Permissions pertain to the Administration role here @@ -302,15 +302,15 @@ contract Tasks is DSMath { taskComplete(_id) { if (_role == TaskRole.Manager) { // Manager rated by worker - require(msg.sender == getTaskRoleUser(_id, TaskRole.Worker), "colony-user-cannot-rate-task-manager"); + require(msg.sender == getTaskRoleUser(_id, TaskRole.Worker), "task-user-cannot-rate-manager"); } else if (_role == TaskRole.Worker) { // Worker rated by evaluator - require(msg.sender == getTaskRoleUser(_id, TaskRole.Evaluator), "colony-user-cannot-rate-task-worker"); + require(msg.sender == getTaskRoleUser(_id, TaskRole.Evaluator), "task-user-cannot-rate-worker"); } else { - revert("colony-unsupported-role-to-rate"); + revert("task-unsupported-role-to-rate"); } - require(sub(now, tasks[_id].completionTimestamp) <= RATING_COMMIT_TIMEOUT, "colony-task-rating-secret-submit-period-closed"); - require(ratingSecrets[_id].secret[uint8(_role)] == "", "colony-task-rating-secret-already-exists"); + require(sub(now, tasks[_id].completionTimestamp) <= RATING_COMMIT_TIMEOUT, "task-secret-submissions-closed"); + require(ratingSecrets[_id].secret[uint8(_role)] == "", "task-secret-already-exists"); ratingSecrets[_id].count += 1; ratingSecrets[_id].timestamp = now; @@ -326,18 +326,18 @@ contract Tasks is DSMath { // Otherwise start the reveal period after the commit period has expired // In both cases, keep reveal period open for 5 days if (ratingSecrets[_id].count == 2) { - require(sub(now, ratingSecrets[_id].timestamp) <= RATING_REVEAL_TIMEOUT, "colony-task-rating-secret-reveal-period-closed"); + require(sub(now, ratingSecrets[_id].timestamp) <= RATING_REVEAL_TIMEOUT, "task-secret-reveal-closed"); } else { uint taskCompletionTime = tasks[_id].completionTimestamp; - require(sub(now, taskCompletionTime) > RATING_COMMIT_TIMEOUT, "colony-task-rating-secret-reveal-period-not-open"); - require(sub(now, taskCompletionTime) <= add(RATING_COMMIT_TIMEOUT, RATING_REVEAL_TIMEOUT), "colony-task-rating-secret-reveal-period-closed"); + require(sub(now, taskCompletionTime) > RATING_COMMIT_TIMEOUT, "task-secret-reveal-not-open"); + require(sub(now, taskCompletionTime) <= add(RATING_COMMIT_TIMEOUT, RATING_REVEAL_TIMEOUT), "task-secret-reveal-closed"); } bytes32 secret = generateSecret(_salt, _rating); - require(secret == ratingSecrets[_id].secret[uint8(_role)], "colony-task-rating-secret-mismatch"); + require(secret == ratingSecrets[_id].secret[uint8(_role)], "task-secret-mismatch"); TaskRatings rating = TaskRatings(_rating); - require(rating != TaskRatings.None, "colony-task-rating-missing"); + require(rating != TaskRatings.None, "task-rating-missing"); taskRoles[_id][uint8(_role)].rating = rating; emit TaskWorkRatingRevealed(_id, _role, _rating); @@ -366,15 +366,15 @@ contract Tasks is DSMath { function setTaskEvaluatorRole(uint256 _id, address payable _user) public self { // Can only assign role if no one is currently assigned to it - require(getTaskRoleUser(_id, TaskRole.Evaluator) == address(0x0), "colony-task-evaluator-role-already-assigned"); + require(getTaskRoleUser(_id, TaskRole.Evaluator) == address(0x0), "task-evaluator-role-assigned"); setTaskRoleUser(_id, TaskRole.Evaluator, _user); } function setTaskWorkerRole(uint256 _id, address payable _user) public self { // Can only assign role if no one is currently assigned to it - require(getTaskRoleUser(_id, TaskRole.Worker) == address(0x0), "colony-task-worker-role-already-assigned"); + require(getTaskRoleUser(_id, TaskRole.Worker) == address(0x0), "task-worker-role-assigned"); uint256[] memory skills = colony.getExpenditureSlot(tasks[_id].expenditureId, uint256(TaskRole.Worker)).skills; - require(skills[0] > 0, "colony-task-skill-not-set"); // ignore-swc-110 + require(skills.length > 0 && skills[0] > 0, "task-skill-not-set"); // ignore-swc-110 setTaskRoleUser(_id, TaskRole.Worker, _user); } @@ -413,8 +413,8 @@ contract Tasks is DSMath { address evaluator = getTaskRoleUser(_id, TaskRole.Evaluator); address worker = getTaskRoleUser(_id, TaskRole.Worker); - require(evaluator == manager || evaluator == address(0x0), "colony-funding-evaluator-already-set"); - require(worker == manager || worker == address(0x0), "colony-funding-worker-already-set"); + require(evaluator == manager || evaluator == address(0x0), "task-evaluator-already-set"); + require(worker == manager || worker == address(0x0), "task-worker-already-set"); this.setTaskManagerPayout(_id, _token, _managerAmount); this.setTaskEvaluatorPayout(_id, _token, _evaluatorAmount); @@ -471,7 +471,7 @@ contract Tasks is DSMath { taskNotComplete(_id) confirmTaskRoleIdentity(_id, msg.sender, TaskRole.Manager) { - require(now >= tasks[_id].dueDate, "colony-task-due-date-in-future"); + require(now >= tasks[_id].dueDate, "task-due-date-in-future"); tasks[_id].completionTimestamp = now; emit TaskCompleted(_id); @@ -643,7 +643,7 @@ contract Tasks is DSMath { } function assignWorkRatings(uint256 _id) internal { - require(taskWorkRatingsAssigned(_id) || taskWorkRatingsClosed(_id), "colony-task-ratings-not-closed"); + require(taskWorkRatingsAssigned(_id) || taskWorkRatingsClosed(_id), "task-ratings-not-closed"); // In the event of a user not committing/revealing within the rating window, // their rating of their counterpart is assumed to be the maximum @@ -668,7 +668,7 @@ contract Tasks is DSMath { } function setTaskRoleUser(uint256 _id, TaskRole _role, address payable _user) - private + internal taskExists(_id) taskNotComplete(_id) { @@ -679,4 +679,12 @@ contract Tasks is DSMath { colony.setExpenditureRecipient(tasks[_id].expenditureId, uint256(_role), _user); } + + function doesTaskExist(uint256 _id) internal view returns (bool) { + return _id > 0 && _id <= taskCount; + } + + function isTaskComplete(uint256 _id) internal view returns (bool) { + return tasks[_id].completionTimestamp > 0; + } } diff --git a/test/extensions/tasks.js b/test/extensions/tasks.js index e7b050decd..1d81f19a41 100644 --- a/test/extensions/tasks.js +++ b/test/extensions/tasks.js @@ -125,7 +125,7 @@ contract("Tasks extension", accounts => { }); it("should fail if a non-admin user tries to make a task", async () => { - await checkErrorRevert(tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: OTHER }), "colony-task-not-admin"); + await checkErrorRevert(tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: OTHER }), "task-not-admin"); }); it("should set the task creator as the manager and evaluator", async () => { @@ -241,7 +241,7 @@ contract("Tasks extension", accounts => { sigTypes: [0, 0], args: [taskId.addn(1), WORKER] }), - "colony-task-does-not-exist" + "task-does-not-exist" ); }); @@ -279,7 +279,7 @@ contract("Tasks extension", accounts => { sigTypes: [0, 0], args: [taskId, WORKER] }), - "colony-task-change-is-not-role-assignment" + "task-change-is-not-role-assign" ); }); @@ -296,7 +296,7 @@ contract("Tasks extension", accounts => { args: [taskId, WORKER] }); - await checkErrorRevert(tasks.executeTaskRoleAssignment(sigV, sigR, sigS, [0, 0], 10, txData), "colony-task-role-assignment-non-zero-value"); + await checkErrorRevert(tasks.executeTaskRoleAssignment(sigV, sigR, sigS, [0, 0], 10, txData), "task-role-assign-non-zero-value"); }); it("should not be able to execute task change when the number of signature parts differ", async () => { @@ -312,10 +312,7 @@ contract("Tasks extension", accounts => { args: [taskId, WORKER] }); - await checkErrorRevert( - tasks.executeTaskRoleAssignment([sigV[0]], sigR, sigS, [0], 0, txData), - "colony-task-role-assignment-signatures-count-do-not-match" - ); + await checkErrorRevert(tasks.executeTaskRoleAssignment([sigV[0]], sigR, sigS, [0], 0, txData), "task-role-assign-sig-count-no-match"); }); it("should allow the evaluator and worker roles to be assigned", async () => { @@ -372,7 +369,7 @@ contract("Tasks extension", accounts => { sigTypes: [0, 0], args: [taskId, WORKER] }), - "colony-task-role-assignment-execution-failed" + "task-role-assign-exec-failed" ); await executeSignedTaskChange({ @@ -419,7 +416,7 @@ contract("Tasks extension", accounts => { sigTypes: [0], args: [taskId, newEvaluator] }), - "colony-task-role-assignment-does-not-meet-required-signatures" + "task-role-assign-wrong-num-sigs" ); await checkErrorRevert( @@ -431,7 +428,7 @@ contract("Tasks extension", accounts => { sigTypes: [0], args: [taskId, WORKER] }), - "colony-task-role-assignment-does-not-meet-required-signatures" + "task-role-assign-wrong-num-sigs" ); }); @@ -448,7 +445,7 @@ contract("Tasks extension", accounts => { sigTypes: [0, 0], args: [taskId, OTHER] }), - "colony-task-role-assignment-execution-failed" + "task-role-assign-exec-failed" ); await executeSignedRoleAssignment({ @@ -469,7 +466,7 @@ contract("Tasks extension", accounts => { sigTypes: [0, 0], args: [taskId, OTHER] }), - "colony-task-role-assignment-execution-failed" + "task-role-assign-exec-failed" ); }); @@ -499,7 +496,7 @@ contract("Tasks extension", accounts => { sigTypes: [0, 0], args: [taskId, ethers.constants.AddressZero] }), - "colony-task-role-assignment-not-signed-by-new-user-for-role" + "task-role-assign-no-new-user-sig" ); // Now they do! @@ -529,7 +526,7 @@ contract("Tasks extension", accounts => { sigTypes: [0, 0], args: [taskId, WORKER] }), - "colony-task-role-assignment-not-signed-by-new-user-for-role" + "task-role-assign-no-new-user-sig" ); }); @@ -563,7 +560,7 @@ contract("Tasks extension", accounts => { sigTypes: [0], args: [taskId, WORKER] }), - "colony-task-role-assignment-does-not-meet-required-signatures" + "task-role-assign-wrong-num-sigs" ); }); @@ -597,7 +594,7 @@ contract("Tasks extension", accounts => { sigTypes: [0, 0], args: [taskId, WORKER] }), - "colony-task-role-assignment-not-signed-by-manager" + "task-role-assign-no-manager-sig" ); }); @@ -631,7 +628,7 @@ contract("Tasks extension", accounts => { sigTypes: [0], args: [taskId, MANAGER] }), - "colony-task-role-assignment-not-signed-by-manager" + "task-role-assign-no-manager-sig" ); }); @@ -648,7 +645,7 @@ contract("Tasks extension", accounts => { sigTypes: [0, 0], args: [taskId, ADMIN, 1, 0] }), - "colony-task-role-assignment-not-signed-by-new-user-for-role" + "task-role-assign-no-new-user-sig" ); }); @@ -665,7 +662,7 @@ contract("Tasks extension", accounts => { sigTypes: [0, 0], args: [taskId, OTHER, 1, 0] }), - "colony-task-role-assignment-execution-failed" + "task-role-assign-exec-failed" ); }); @@ -682,7 +679,7 @@ contract("Tasks extension", accounts => { sigTypes: [0, 0], args: [taskId, ethers.constants.AddressZero, 1, 0] }), - "colony-task-role-assignment-not-signed-by-new-user-for-role" + "task-role-assign-no-new-user-sig" ); }); @@ -732,7 +729,7 @@ contract("Tasks extension", accounts => { sigTypes: [0, 0], args: [taskId, WORKER, 1, 0] }), - "colony-task-role-assignment-not-signed-by-manager" + "task-role-assign-no-manager-sig" ); }); @@ -900,7 +897,7 @@ contract("Tasks extension", accounts => { sigTypes: [0, 1], args: [taskId, SPECIFICATION_HASH_UPDATED] }), - "colony-task-duplicate-reviewers" + "task-duplicate-reviewers" ); }); @@ -935,7 +932,7 @@ contract("Tasks extension", accounts => { await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); const taskId = await tasks.getTaskCount(); - await checkErrorRevert(tasks.setTaskBrief(taskId, SPECIFICATION_HASH_UPDATED, { from: OTHER }), "colony-task-not-self"); + await checkErrorRevert(tasks.setTaskBrief(taskId, SPECIFICATION_HASH_UPDATED, { from: OTHER }), "task-not-self"); }); it("should fail update of task brief signed by a non-registered role", async () => { @@ -951,7 +948,7 @@ contract("Tasks extension", accounts => { sigTypes: [0, 0], args: [taskId, SPECIFICATION_HASH_UPDATED] }), - "colony-task-change-does-not-meet-signatures-required" + "task-change-wrong-num-sigs" ); }); @@ -977,7 +974,7 @@ contract("Tasks extension", accounts => { sigTypes: [0], args: [taskId, SPECIFICATION_HASH_UPDATED] }), - "colony-task-change-does-not-meet-signatures-required" + "task-change-wrong-num-sigs" ); }); @@ -994,7 +991,7 @@ contract("Tasks extension", accounts => { sigTypes: [0, 0], args: [taskId, 0] }), - "colony-task-change-does-not-meet-signatures-required" + "task-change-wrong-num-sigs" ); }); @@ -1013,7 +1010,7 @@ contract("Tasks extension", accounts => { sigTypes: [0], args: [nonExistentTaskId, SPECIFICATION_HASH_UPDATED] }), - "colony-task-does-not-exist" + "task-does-not-exist" ); }); @@ -1029,7 +1026,7 @@ contract("Tasks extension", accounts => { sigTypes: [0], args: [taskId, SPECIFICATION_HASH_UPDATED] }), - "colony-task-does-not-exist" + "task-does-not-exist" ); }); @@ -1046,7 +1043,7 @@ contract("Tasks extension", accounts => { sigTypes: [0], args: [taskId, 0] }), - "colony-task-change-execution-failed" + "task-change-execution-failed" ); }); @@ -1087,7 +1084,7 @@ contract("Tasks extension", accounts => { sigTypes: [0, 0], args: [taskId, ADMIN, 1, 0] }), - "colony-task-role-assignment-execution-failed" + "olony-task-role-assignment-execution-failed" ); }); @@ -1139,7 +1136,7 @@ contract("Tasks extension", accounts => { args: [taskId, SPECIFICATION_HASH_UPDATED] }); - await checkErrorRevert(tasks.executeTaskChange(sigV, sigR, sigS, [0], 100, txData), "colony-task-change-non-zero-value"); + await checkErrorRevert(tasks.executeTaskChange(sigV, sigR, sigS, [0], 100, txData), "task-change-non-zero-value"); }); it("should fail to execute task change with a mismatched set of signature parts", async () => { @@ -1155,7 +1152,7 @@ contract("Tasks extension", accounts => { args: [taskId, SPECIFICATION_HASH_UPDATED] }); - await checkErrorRevert(tasks.executeTaskChange([sigV[0]], sigR, sigS, [0], 0, txData), "colony-task-change-signatures-count-do-not-match"); + await checkErrorRevert(tasks.executeTaskChange([sigV[0]], sigR, sigS, [0], 0, txData), "task-change-sig-count-no-match"); }); it("should fail to execute task change send for a task role assignment call (which should be using executeTaskRoleAssignment)", async () => { @@ -1171,7 +1168,7 @@ contract("Tasks extension", accounts => { args: [taskId, "0x29738B9BB168790211D84C99c4AEAd215c34D731"] }); - await checkErrorRevert(tasks.executeTaskChange(sigV, sigR, sigS, [0], 0, txData), "colony-task-change-is-role-assignment"); + await checkErrorRevert(tasks.executeTaskChange(sigV, sigR, sigS, [0], 0, txData), "task-change-is-role-assign"); }); it("should fail to execute task change with the wrong signatures, one signer", async () => { @@ -1187,7 +1184,7 @@ contract("Tasks extension", accounts => { args: [taskId, SPECIFICATION_HASH_UPDATED] }); - await checkErrorRevert(tasks.executeTaskChange(sigV, sigR, sigS, [0], 0, txData), "colony-task-signatures-do-not-match-reviewer-1"); + await checkErrorRevert(tasks.executeTaskChange(sigV, sigR, sigS, [0], 0, txData), "task-sigs-no-match-reviewer-1"); }); it("should fail to execute task change with the wrong signatures, two signers", async () => { @@ -1212,7 +1209,7 @@ contract("Tasks extension", accounts => { args: [taskId, SPECIFICATION_HASH_UPDATED] }); - await checkErrorRevert(tasks.executeTaskChange(sigV, sigR, sigS, [0, 0], 0, txData), "colony-task-signatures-do-not-match-reviewer-2"); + await checkErrorRevert(tasks.executeTaskChange(sigV, sigR, sigS, [0, 0], 0, txData), "task-sigs-no-match-reviewer-2"); }); }); @@ -1238,7 +1235,7 @@ contract("Tasks extension", accounts => { await forwardTime(90 * SECONDS_PER_DAY); await tasks.completeTask(taskId, { from: MANAGER }); - await checkErrorRevert(tasks.submitTaskDeliverable(taskId, DELIVERABLE_HASH), "colony-task-complete"); + await checkErrorRevert(tasks.submitTaskDeliverable(taskId, DELIVERABLE_HASH), "task-complete"); }); it("should fail if I try to submit work for a task that is finalized", async () => { @@ -1249,7 +1246,7 @@ contract("Tasks extension", accounts => { await submitDeliverableAndRatings({ tasks, taskId }); await tasks.finalizeTask(1, 0, taskId, { from: MANAGER }); - await checkErrorRevert(tasks.submitTaskDeliverable(taskId, DELIVERABLE_HASH, { from: WORKER }), "colony-task-complete"); + await checkErrorRevert(tasks.submitTaskDeliverable(taskId, DELIVERABLE_HASH, { from: WORKER }), "task-complete"); }); it("should succeed if I try to submit work for a task that is past its due date but not yet marked as complete", async () => { @@ -1268,7 +1265,7 @@ contract("Tasks extension", accounts => { await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); const taskId = await tasks.getTaskCount(); - await checkErrorRevert(tasks.submitTaskDeliverable(taskId.addn(1), DELIVERABLE_HASH), "colony-task-does-not-exist"); + await checkErrorRevert(tasks.submitTaskDeliverable(taskId.addn(1), DELIVERABLE_HASH), "task-does-not-exist"); }); it("should fail if I try to submit work twice", async () => { @@ -1278,7 +1275,7 @@ contract("Tasks extension", accounts => { await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); await tasks.submitTaskDeliverable(taskId, DELIVERABLE_HASH, { from: WORKER }); - await checkErrorRevert(tasks.submitTaskDeliverable(taskId, SPECIFICATION_HASH, { from: WORKER }), "colony-task-complete"); + await checkErrorRevert(tasks.submitTaskDeliverable(taskId, SPECIFICATION_HASH, { from: WORKER }), "task-complete"); }); it("should fail if I try to mark a taske complete after work is submitted", async () => { @@ -1288,7 +1285,7 @@ contract("Tasks extension", accounts => { await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); await tasks.submitTaskDeliverable(taskId, DELIVERABLE_HASH, { from: WORKER }); - await checkErrorRevert(tasks.completeTask(taskId, { from: MANAGER }), "colony-task-complete"); + await checkErrorRevert(tasks.completeTask(taskId, { from: MANAGER }), "task-complete"); }); it("should fail if I try to submit work if I'm not the assigned worker", async () => { @@ -1296,7 +1293,7 @@ contract("Tasks extension", accounts => { const taskId = await tasks.getTaskCount(); await assignRoles({ tasks, taskId, manager: MANAGER, worker: OTHER }); - await checkErrorRevert(tasks.submitTaskDeliverable(taskId, SPECIFICATION_HASH, { from: WORKER }), "colony-task-role-identity-mismatch"); + await checkErrorRevert(tasks.submitTaskDeliverable(taskId, SPECIFICATION_HASH, { from: WORKER }), "task-role-identity-mismatch"); }); it("should log a TaskDeliverableSubmitted event", async () => { @@ -1312,7 +1309,7 @@ contract("Tasks extension", accounts => { const taskId = await tasks.getTaskCount(); await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); - await checkErrorRevert(tasks.completeTask(taskId, { from: MANAGER }), "colony-task-due-date-in-future"); + await checkErrorRevert(tasks.completeTask(taskId, { from: MANAGER }), "task-due-date-in-future"); }); }); @@ -1321,7 +1318,7 @@ contract("Tasks extension", accounts => { await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); const taskId = await tasks.getTaskCount(); - await checkErrorRevert(tasks.submitTaskWorkRating(taskId, WORKER_ROLE, RATING_2_SECRET, { from: EVALUATOR }), "colony-task-not-complete"); + await checkErrorRevert(tasks.submitTaskWorkRating(taskId, WORKER_ROLE, RATING_2_SECRET, { from: EVALUATOR }), "task-not-complete"); }); it("should fail if I try to evaluate twice", async () => { @@ -1331,10 +1328,7 @@ contract("Tasks extension", accounts => { await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); await tasks.submitTaskDeliverableAndRating(taskId, DELIVERABLE_HASH, RATING_1_SECRET, { from: WORKER }); - await checkErrorRevert( - tasks.submitTaskWorkRating(taskId, MANAGER_ROLE, RATING_1_SECRET, { from: WORKER }), - "colony-task-rating-secret-already-exists" - ); + await checkErrorRevert(tasks.submitTaskWorkRating(taskId, MANAGER_ROLE, RATING_1_SECRET, { from: WORKER }), "task-secret-already-exists"); }); it("should fail if the wrong user tries to rate the wrong role", async () => { @@ -1345,9 +1339,9 @@ contract("Tasks extension", accounts => { await tasks.submitTaskDeliverable(taskId, DELIVERABLE_HASH, { from: WORKER }); const SECRET = soliditySha3("secret"); - await checkErrorRevert(tasks.submitTaskWorkRating(taskId, MANAGER_ROLE, SECRET, { from: OTHER }), "colony-user-cannot-rate-task-manager"); - await checkErrorRevert(tasks.submitTaskWorkRating(taskId, WORKER_ROLE, SECRET, { from: OTHER }), "colony-user-cannot-rate-task-worker"); - await checkErrorRevert(tasks.submitTaskWorkRating(taskId, EVALUATOR_ROLE, SECRET), "colony-unsupported-role-to-rate"); + await checkErrorRevert(tasks.submitTaskWorkRating(taskId, MANAGER_ROLE, SECRET, { from: OTHER }), "task-user-cannot-rate-manager"); + await checkErrorRevert(tasks.submitTaskWorkRating(taskId, WORKER_ROLE, SECRET, { from: OTHER }), "task-user-cannot-rate-worker"); + await checkErrorRevert(tasks.submitTaskWorkRating(taskId, EVALUATOR_ROLE, SECRET), "task-unsupported-role-to-rate"); }); it("can retreive rating secret information", async () => { @@ -1376,10 +1370,7 @@ contract("Tasks extension", accounts => { await tasks.submitTaskDeliverableAndRating(taskId, DELIVERABLE_HASH, RATING_1_SECRET, { from: WORKER }); await forwardTime(SECONDS_PER_DAY * 5 + 1); - await checkErrorRevert( - tasks.submitTaskWorkRating(taskId, WORKER_ROLE, RATING_2_SECRET, { from: MANAGER }), - "colony-task-rating-secret-submit-period-closed" - ); + await checkErrorRevert(tasks.submitTaskWorkRating(taskId, WORKER_ROLE, RATING_2_SECRET, { from: MANAGER }), "task-secret-submissions-closed"); }); it("should not allow a user to reveal after the deadline, with two secrets", async () => { @@ -1394,7 +1385,7 @@ contract("Tasks extension", accounts => { await forwardTime(SECONDS_PER_DAY * 5 + 1); await checkErrorRevert( tasks.revealTaskWorkRating(taskId, MANAGER_ROLE, MANAGER_RATING, RATING_1_SALT, { from: WORKER }), - "colony-task-rating-secret-reveal-period-closed" + "task-secret-reveal-closed" ); }); @@ -1409,7 +1400,7 @@ contract("Tasks extension", accounts => { await forwardTime(SECONDS_PER_DAY * 10 + 1); await checkErrorRevert( tasks.revealTaskWorkRating(taskId, MANAGER_ROLE, MANAGER_RATING, RATING_1_SALT, { from: WORKER }), - "colony-task-rating-secret-reveal-period-closed" + "task-secret-reveal-closed" ); }); @@ -1423,7 +1414,7 @@ contract("Tasks extension", accounts => { await checkErrorRevert( tasks.revealTaskWorkRating(taskId, MANAGER_ROLE, MANAGER_RATING, RATING_1_SALT, { from: WORKER }), - "colony-task-rating-secret-reveal-period-not-open" + "task-secret-reveal-not-open" ); }); @@ -1436,10 +1427,7 @@ contract("Tasks extension", accounts => { await tasks.submitTaskDeliverableAndRating(taskId, DELIVERABLE_HASH, soliditySha3(RATING_1_SALT, 3), { from: WORKER }); await tasks.submitTaskWorkRating(taskId, WORKER_ROLE, RATING_2_SECRET, { from: MANAGER }); - await checkErrorRevert( - tasks.revealTaskWorkRating(taskId, MANAGER_ROLE, 2, RATING_1_SALT, { from: WORKER }), - "colony-task-rating-secret-mismatch" - ); + await checkErrorRevert(tasks.revealTaskWorkRating(taskId, MANAGER_ROLE, 2, RATING_1_SALT, { from: WORKER }), "task-secret-mismatch"); }); it("should not allow a user to reveal a rating of None", async () => { @@ -1451,7 +1439,7 @@ contract("Tasks extension", accounts => { await tasks.submitTaskDeliverableAndRating(taskId, DELIVERABLE_HASH, soliditySha3(RATING_1_SALT, 0), { from: WORKER }); await tasks.submitTaskWorkRating(taskId, WORKER_ROLE, RATING_2_SECRET, { from: MANAGER }); - await checkErrorRevert(tasks.revealTaskWorkRating(taskId, MANAGER_ROLE, 0, RATING_1_SALT, { from: WORKER }), "colony-task-rating-missing"); + await checkErrorRevert(tasks.revealTaskWorkRating(taskId, MANAGER_ROLE, 0, RATING_1_SALT, { from: WORKER }), "task-rating-missing"); }); }); @@ -1484,7 +1472,7 @@ contract("Tasks extension", accounts => { await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); const taskId = await tasks.getTaskCount(); - await checkErrorRevert(tasks.finalizeTask(1, 0, taskId), "colony-task-not-complete"); + await checkErrorRevert(tasks.finalizeTask(1, 0, taskId), "task-not-complete"); }); it("should fail if the task work ratings have not been assigned and they still have time to be", async () => { @@ -1494,7 +1482,7 @@ contract("Tasks extension", accounts => { await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); await tasks.submitTaskDeliverable(taskId, SPECIFICATION_HASH, { from: WORKER }); - await checkErrorRevert(tasks.finalizeTask(1, 0, taskId), "colony-task-ratings-not-closed"); + await checkErrorRevert(tasks.finalizeTask(1, 0, taskId), "task-ratings-not-closed"); }); it("should fail if the task work ratings have not been revealed and they still have time to be", async () => { @@ -1505,7 +1493,7 @@ contract("Tasks extension", accounts => { await tasks.submitTaskDeliverableAndRating(taskId, SPECIFICATION_HASH, RATING_1_SECRET, { from: WORKER }); await tasks.submitTaskWorkRating(taskId, WORKER_ROLE, RATING_2_SECRET, { from: MANAGER }); - await checkErrorRevert(tasks.finalizeTask(1, 0, taskId), "colony-task-ratings-not-closed"); + await checkErrorRevert(tasks.finalizeTask(1, 0, taskId), "task-ratings-not-closed"); }); it("should finalize if the rate and reveal period have elapsed", async () => { @@ -1528,7 +1516,7 @@ contract("Tasks extension", accounts => { await tasks.submitTaskDeliverableAndRating(taskId, SPECIFICATION_HASH, RATING_1_SECRET, { from: WORKER }); await tasks.submitTaskWorkRating(taskId, WORKER_ROLE, RATING_2_SECRET, { from: MANAGER }); - await checkErrorRevert(tasks.finalizeTask(1, 0, taskId), "colony-task-ratings-not-closed"); + await checkErrorRevert(tasks.finalizeTask(1, 0, taskId), "task-ratings-not-closed"); // Both secrets submitted, so we only have to wait for the reveal period to elapse await forwardTime(SECONDS_PER_DAY * 5 + 1); @@ -1589,7 +1577,7 @@ contract("Tasks extension", accounts => { await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); const taskId = await tasks.getTaskCount(); - await checkErrorRevert(tasks.finalizeTask(1, 0, taskId.addn(1)), "colony-task-does-not-exist"); + await checkErrorRevert(tasks.finalizeTask(1, 0, taskId.addn(1)), "task-does-not-exist"); }); it("should emit two negative reputation updates for a bad worker rating", async () => { @@ -1710,7 +1698,7 @@ contract("Tasks extension", accounts => { sigTypes: [0], args: [taskId.addn(1)] }), - "colony-task-does-not-exist" + "task-does-not-exist" ); }); }); @@ -1809,7 +1797,7 @@ contract("Tasks extension", accounts => { const taskId = await tasks.getTaskCount(); await checkErrorRevert( tasks.setAllTaskPayouts(taskId, ethers.constants.AddressZero, 5000, 1000, 98000, { from: OTHER }), - "colony-task-role-identity-mismatch" + "task-role-identity-mismatch" ); await tasks.setAllTaskPayouts(taskId, ethers.constants.AddressZero, 5000, 1000, 98000); @@ -1836,7 +1824,7 @@ contract("Tasks extension", accounts => { args: [taskId, WORKER] }); - await checkErrorRevert(tasks.setAllTaskPayouts(taskId, ethers.constants.AddressZero, 5000, 1000, 98000), "colony-funding-worker-already-set"); + await checkErrorRevert(tasks.setAllTaskPayouts(taskId, ethers.constants.AddressZero, 5000, 1000, 98000), "task-worker-already-set"); }); it("should not be able to set all payments at once if evaluator is assigned and is not the manager", async () => { @@ -1861,10 +1849,7 @@ contract("Tasks extension", accounts => { args: [taskId, EVALUATOR] }); - await checkErrorRevert( - tasks.setAllTaskPayouts(taskId, ethers.constants.AddressZero, 5000, 1000, 98000), - "colony-funding-evaluator-already-set" - ); + await checkErrorRevert(tasks.setAllTaskPayouts(taskId, ethers.constants.AddressZero, 5000, 1000, 98000), "task-evaluator-already-set"); }); it("should correctly return the current total payout", async () => { From 989565e7a70a09554e80bce64f9e2fc9968145bc Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Tue, 24 Sep 2019 12:42:48 +0300 Subject: [PATCH 4/4] Introduce secure vs. managed tasks --- contracts/extensions/Tasks.sol | 116 ++++++---- test/extensions/tasks.js | 382 ++++++++++++++++++++++----------- 2 files changed, 332 insertions(+), 166 deletions(-) diff --git a/contracts/extensions/Tasks.sol b/contracts/extensions/Tasks.sol index d25870cfda..8d6874f3f9 100644 --- a/contracts/extensions/Tasks.sol +++ b/contracts/extensions/Tasks.sol @@ -33,6 +33,11 @@ contract Tasks is DSMath { /// @param taskId The newly added task id event TaskAdded(uint256 taskId); + /// @notice Event logged when a task's security status changes (secure vs. managed) + /// @param taskId Id of the task + /// @param secure Boolean of security status (true: secure, false: managed) + event TaskSecuritySet(uint256 indexed taskId, bool secure); + /// @notice Event logged when a task's specification hash changes /// @param taskId Id of the task /// @param specificationHash New specification hash of the task @@ -69,6 +74,7 @@ contract Tasks is DSMath { uint256 dueDate; uint256 completionTimestamp; uint256 changeNonce; + bool secure; } struct Role { @@ -103,6 +109,7 @@ contract Tasks is DSMath { roleAssignmentSigs[bytes4(keccak256("setTaskWorkerRole(uint256,address)"))] = true; // Initialise the task update reviewers + reviewers[bytes4(keccak256("setTaskSecurity(uint256,bool)"))] = [TaskRole.Manager, TaskRole.Worker]; reviewers[bytes4(keccak256("setTaskBrief(uint256,bytes32)"))] = [TaskRole.Manager, TaskRole.Worker]; reviewers[bytes4(keccak256("setTaskDueDate(uint256,uint256)"))] = [TaskRole.Manager, TaskRole.Worker]; reviewers[bytes4(keccak256("setTaskSkill(uint256,uint256)"))] = [TaskRole.Manager, TaskRole.Worker]; @@ -115,8 +122,8 @@ contract Tasks is DSMath { reviewers[bytes4(keccak256("cancelTask(uint256)"))] = [TaskRole.Manager, TaskRole.Worker]; } - modifier self() { - require(address(this) == msg.sender, "task-not-self"); + modifier self(uint256 _id) { + require(managerCanCall(_id) || address(this) == msg.sender, "task-not-self"); _; } @@ -131,6 +138,16 @@ contract Tasks is DSMath { _; } + modifier taskSecure(uint256 _id) { + require(isTaskSecure(_id), "task-not-secure"); + _; + } + + modifier taskManaged(uint256 _id) { + require(!isTaskSecure(_id), "task-not-managed"); + _; + } + modifier taskComplete(uint256 _id) { require(isTaskComplete(_id), "task-not-complete"); _; @@ -162,12 +179,10 @@ contract Tasks is DSMath { bytes4 sig; uint256 taskId; (sig, taskId) = deconstructCall(_data); - require(taskId > 0 && taskId <= taskCount, "task-does-not-exist"); + require(doesTaskExist(taskId), "task-does-not-exist"); + require(!isTaskComplete(taskId), "task-complete"); require(!roleAssignmentSigs[sig], "task-change-is-role-assign"); - ColonyDataTypes.ExpenditureStatus status = colony.getExpenditure(tasks[taskId].expenditureId).status; - require(status != ColonyDataTypes.ExpenditureStatus.Finalized, "task-finalized"); - uint8 nSignaturesRequired; address taskRole1User = getTaskRoleUser(taskId, TaskRole(reviewers[sig][0])); address taskRole2User = getTaskRoleUser(taskId, TaskRole(reviewers[sig][1])); @@ -219,12 +234,10 @@ contract Tasks is DSMath { uint256 taskId; address userAddress; (sig, taskId, userAddress) = deconstructRoleChangeCall(_data); - require(taskId > 0 && taskId <= taskCount, "task-does-not-exist"); + require(doesTaskExist(taskId), "task-does-not-exist"); + require(!isTaskComplete(taskId), "task-complete"); require(roleAssignmentSigs[sig], "task-change-is-not-role-assign"); - ColonyDataTypes.ExpenditureStatus status = colony.getExpenditure(tasks[taskId].expenditureId).status; - require(status != ColonyDataTypes.ExpenditureStatus.Finalized, "task-finalized"); - uint8 nSignaturesRequired; address manager = getTaskRoleUser(taskId, TaskRole.Manager); // If manager wants to set himself to a role @@ -274,7 +287,8 @@ contract Tasks is DSMath { bytes32 _specificationHash, uint256 _domainId, uint256 _skillId, - uint256 _dueDate + uint256 _dueDate, + bool _secure ) public isAdmin(msg.sender, _callerPermissionDomainId, _callerChildSkillIndex, _domainId) @@ -285,9 +299,13 @@ contract Tasks is DSMath { tasks[taskCount].expenditureId = expenditureId; tasks[taskCount].specificationHash = _specificationHash; tasks[taskCount].dueDate = (_dueDate > 0) ? _dueDate : now + 90 days; // Note: can set dueDate in past? + tasks[taskCount].secure = _secure; setTaskRoleUser(taskCount, TaskRole.Manager, msg.sender); - setTaskRoleUser(taskCount, TaskRole.Evaluator, msg.sender); + + if (_secure) { + setTaskRoleUser(taskCount, TaskRole.Evaluator, msg.sender); + } if (_skillId > 0) { this.setTaskSkill(taskCount, _skillId); @@ -299,6 +317,8 @@ contract Tasks is DSMath { function submitTaskWorkRating(uint256 _id, TaskRole _role, bytes32 _secret) public + taskExists(_id) + taskSecure(_id) taskComplete(_id) { if (_role == TaskRole.Manager) { // Manager rated by worker @@ -355,22 +375,32 @@ contract Tasks is DSMath { return ratingSecrets[_id].secret[_role]; } + function setTaskSecurity(uint256 _id, bool _secure) public self(_id) { + tasks[_id].secure = _secure; + + if (!_secure) { + removeTaskEvaluatorRole(_id); + } + + emit TaskSecuritySet(_id, _secure); + } + // Note: the domain permissions arguments are placed at the end for consistency with the other role change functions function setTaskManagerRole(uint256 _id, address payable _user, uint256 _permissionDomainId, uint256 _childSkillIndex) public - self + self(_id) isAdmin(_user, _permissionDomainId, _childSkillIndex, colony.getExpenditure(tasks[_id].expenditureId).domainId) { setTaskRoleUser(_id, TaskRole.Manager, _user); } - function setTaskEvaluatorRole(uint256 _id, address payable _user) public self { + function setTaskEvaluatorRole(uint256 _id, address payable _user) public self(_id) taskSecure(_id) { // Can only assign role if no one is currently assigned to it require(getTaskRoleUser(_id, TaskRole.Evaluator) == address(0x0), "task-evaluator-role-assigned"); setTaskRoleUser(_id, TaskRole.Evaluator, _user); } - function setTaskWorkerRole(uint256 _id, address payable _user) public self { + function setTaskWorkerRole(uint256 _id, address payable _user) public self(_id) { // Can only assign role if no one is currently assigned to it require(getTaskRoleUser(_id, TaskRole.Worker) == address(0x0), "task-worker-role-assigned"); uint256[] memory skills = colony.getExpenditureSlot(tasks[_id].expenditureId, uint256(TaskRole.Worker)).skills; @@ -378,23 +408,23 @@ contract Tasks is DSMath { setTaskRoleUser(_id, TaskRole.Worker, _user); } - function removeTaskEvaluatorRole(uint256 _id) public self { + function removeTaskEvaluatorRole(uint256 _id) public self(_id) { setTaskRoleUser(_id, TaskRole.Evaluator, address(0x0)); } - function removeTaskWorkerRole(uint256 _id) public self { + function removeTaskWorkerRole(uint256 _id) public self(_id) { setTaskRoleUser(_id, TaskRole.Worker, address(0x0)); } - function setTaskManagerPayout(uint256 _id, address _token, uint256 _amount) public self { + function setTaskManagerPayout(uint256 _id, address _token, uint256 _amount) public self(_id) { colony.setExpenditurePayout(_id, uint256(TaskRole.Manager), _token, _amount); } - function setTaskEvaluatorPayout(uint256 _id, address _token, uint256 _amount) public self { + function setTaskEvaluatorPayout(uint256 _id, address _token, uint256 _amount) public self(_id) { colony.setExpenditurePayout(_id, uint256(TaskRole.Evaluator), _token, _amount); } - function setTaskWorkerPayout(uint256 _id, address _token, uint256 _amount) public self { + function setTaskWorkerPayout(uint256 _id, address _token, uint256 _amount) public self(_id) { colony.setExpenditurePayout(_id, uint256(TaskRole.Worker), _token, _amount); } @@ -421,15 +451,14 @@ contract Tasks is DSMath { this.setTaskWorkerPayout(_id, _token, _workerAmount); } - function setTaskSkill(uint256 _id, uint256 _skillId) public self { + function setTaskSkill(uint256 _id, uint256 _skillId) public self(_id) { colony.setExpenditureSkill(tasks[_id].expenditureId, uint256(TaskRole.Worker), _skillId); } function setTaskBrief(uint256 _id, bytes32 _specificationHash) public - self + self(_id) taskExists(_id) - taskNotComplete(_id) { tasks[_id].specificationHash = _specificationHash; @@ -438,9 +467,8 @@ contract Tasks is DSMath { function setTaskDueDate(uint256 _id, uint256 _dueDate) public - self + self(_id) taskExists(_id) - taskNotComplete(_id) { tasks[_id].dueDate = _dueDate; @@ -450,6 +478,7 @@ contract Tasks is DSMath { function submitTaskDeliverable(uint256 _id, bytes32 _deliverableHash) public taskExists(_id) + taskSecure(_id) taskNotComplete(_id) confirmTaskRoleIdentity(_id, msg.sender, TaskRole.Worker) { @@ -468,6 +497,7 @@ contract Tasks is DSMath { function completeTask(uint256 _id) public taskExists(_id) + taskSecure(_id) taskNotComplete(_id) confirmTaskRoleIdentity(_id, msg.sender, TaskRole.Manager) { @@ -479,17 +509,17 @@ contract Tasks is DSMath { function cancelTask(uint256 _id) public - self + self(_id) taskExists(_id) - taskNotComplete(_id) { colony.cancelExpenditure(tasks[_id].expenditureId); } // Permissions pertain to the Arbitration role here - function finalizeTask(uint256 _permissionDomainId, uint256 _childSkillIndex, uint256 _id) + function finalizeSecureTask(uint256 _permissionDomainId, uint256 _childSkillIndex, uint256 _id) public taskExists(_id) + taskSecure(_id) taskComplete(_id) { colony.finalizeExpenditure(tasks[_id].expenditureId); @@ -510,6 +540,15 @@ contract Tasks is DSMath { } } + function finalizeManagedTask(uint256 _id) + public + taskExists(_id) + taskManaged(_id) + confirmTaskRoleIdentity(_id, msg.sender, TaskRole.Manager) + { + colony.finalizeExpenditure(tasks[_id].expenditureId); + } + function getTaskCount() public view returns (uint256) { return taskCount; } @@ -629,12 +668,14 @@ contract Tasks is DSMath { function taskWorkRatingsAssigned(uint256 _id) internal view returns (bool) { Role storage workerRole = taskRoles[_id][uint8(TaskRole.Worker)]; Role storage managerRole = taskRoles[_id][uint8(TaskRole.Manager)]; + return (workerRole.rating != TaskRatings.None) && (managerRole.rating != TaskRatings.None); } function taskWorkRatingsClosed(uint256 _id) internal view returns (bool) { assert(tasks[_id].completionTimestamp > 0); assert(ratingSecrets[_id].count <= 2); + if (ratingSecrets[_id].count == 2) { return sub(now, ratingSecrets[_id].timestamp) > RATING_REVEAL_TIMEOUT; } else { @@ -667,15 +708,8 @@ contract Tasks is DSMath { } } - function setTaskRoleUser(uint256 _id, TaskRole _role, address payable _user) - internal - taskExists(_id) - taskNotComplete(_id) - { - taskRoles[_id][uint8(_role)] = Role({ - rateFail: false, - rating: TaskRatings.None - }); + function setTaskRoleUser(uint256 _id, TaskRole _role, address payable _user) internal { + taskRoles[_id][uint8(_role)] = Role({ rateFail: false, rating: TaskRatings.None }); colony.setExpenditureRecipient(tasks[_id].expenditureId, uint256(_role), _user); } @@ -684,7 +718,15 @@ contract Tasks is DSMath { return _id > 0 && _id <= taskCount; } + function isTaskSecure(uint256 _id) internal view returns (bool) { + return tasks[_id].secure; + } + function isTaskComplete(uint256 _id) internal view returns (bool) { return tasks[_id].completionTimestamp > 0; } + + function managerCanCall(uint256 _id) internal view returns (bool) { + return !tasks[_id].secure && getTaskRoleUser(_id, TaskRole.Manager) == msg.sender; + } } diff --git a/test/extensions/tasks.js b/test/extensions/tasks.js index 1d81f19a41..1b8f9885c0 100644 --- a/test/extensions/tasks.js +++ b/test/extensions/tasks.js @@ -115,7 +115,7 @@ contract("Tasks extension", accounts => { describe("when creating tasks", () => { it("should allow admins to make a task", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); const task = await tasks.getTask(taskId); @@ -125,11 +125,11 @@ contract("Tasks extension", accounts => { }); it("should fail if a non-admin user tries to make a task", async () => { - await checkErrorRevert(tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: OTHER }), "task-not-admin"); + await checkErrorRevert(tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, true, { from: OTHER }), "task-not-admin"); }); it("should set the task creator as the manager and evaluator", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); const task = await tasks.getTask(taskId); @@ -148,7 +148,7 @@ contract("Tasks extension", accounts => { const newEvaluator = accounts[1]; expect(newEvaluator).to.not.equal(MANAGER); - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); let evaluator = await tasks.getTaskRoleUser(taskId, EVALUATOR_ROLE); @@ -183,7 +183,7 @@ contract("Tasks extension", accounts => { const taskCountBefore = await tasks.getTaskCount(); for (let i = 0; i < 5; i += 1) { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, true, { from: MANAGER }); } const taskCountAfter = await tasks.getTaskCount(); @@ -191,7 +191,7 @@ contract("Tasks extension", accounts => { }); it("should set the task domain correctly", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 2, 0, 0); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 2, 0, 0, true); const taskId = await tasks.getTaskCount(); const task = await tasks.getTask(taskId); @@ -200,14 +200,14 @@ contract("Tasks extension", accounts => { }); it("should log TaskAdded and TaskDueDateSet events", async () => { - await expectAllEvents(tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0), ["TaskAdded", "TaskDueDateSet"]); + await expectAllEvents(tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, true), ["TaskAdded", "TaskDueDateSet"]); }); it("should optionally set the skill and due date", async () => { const currTime = await currentBlockTime(); const dueDate = currTime + SECONDS_PER_DAY; - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, dueDate); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, dueDate, true); const taskId = await tasks.getTaskCount(); const task = await tasks.getTask(taskId); @@ -218,7 +218,7 @@ contract("Tasks extension", accounts => { }); it("should set the due date to 90 days from now if unspecified", async () => { - const tx = await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0); + const tx = await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, true); const taskId = await tasks.getTaskCount(); const task = await tasks.getTask(taskId); @@ -229,7 +229,7 @@ contract("Tasks extension", accounts => { describe("when updating tasks", () => { it("should not be able to `executeTaskRoleAssignment` on a nonexistent task", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await checkErrorRevert( @@ -245,13 +245,13 @@ contract("Tasks extension", accounts => { ); }); - it("should not be able to `executeTaskRoleAssignment` on a finalized task", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + it("should not be able to `executeTaskRoleAssignment` on a completed task", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); await submitDeliverableAndRatings({ tasks, taskId }); - await tasks.finalizeTask(1, 0, taskId, { from: MANAGER }); + await tasks.finalizeSecureTask(1, 0, taskId, { from: MANAGER }); await checkErrorRevert( executeSignedRoleAssignment({ @@ -262,12 +262,12 @@ contract("Tasks extension", accounts => { sigTypes: [0, 0], args: [taskId, WORKER] }), - "colony-task-finalized" + "task-complete" ); }); it("should not be able to pass unallowed function signature to `executeTaskRoleAssignment`", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await checkErrorRevert( @@ -284,7 +284,7 @@ contract("Tasks extension", accounts => { }); it("should not be able to send any ether while assigning a role", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); const { sigV, sigR, sigS, txData } = await getSigsAndTransactionData({ @@ -300,7 +300,7 @@ contract("Tasks extension", accounts => { }); it("should not be able to execute task change when the number of signature parts differ", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); const { sigV, sigR, sigS, txData } = await getSigsAndTransactionData({ @@ -319,7 +319,7 @@ contract("Tasks extension", accounts => { const newEvaluator = accounts[1]; expect(newEvaluator).to.not.equal(MANAGER); - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await executeSignedTaskChange({ @@ -357,7 +357,7 @@ contract("Tasks extension", accounts => { }); it("should not allow a worker to be assigned if the task has no skill", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await checkErrorRevert( @@ -395,7 +395,7 @@ contract("Tasks extension", accounts => { const newEvaluator = accounts[1]; expect(newEvaluator).to.not.equal(MANAGER); - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await executeSignedTaskChange({ @@ -433,7 +433,7 @@ contract("Tasks extension", accounts => { }); it("should not allow role to be assigned if it is already assigned to somebody", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await checkErrorRevert( @@ -471,7 +471,7 @@ contract("Tasks extension", accounts => { }); it("should allow role to be unassigned, as long as the current assigned address agrees", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await executeSignedRoleAssignment({ @@ -514,7 +514,7 @@ contract("Tasks extension", accounts => { }); it("should not allow role to be assigned if passed address is not equal to one of the signers", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await checkErrorRevert( @@ -531,7 +531,7 @@ contract("Tasks extension", accounts => { }); it("should allow manager to assign themself to a role", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await executeSignedRoleAssignment({ @@ -548,7 +548,7 @@ contract("Tasks extension", accounts => { }); it("should not allow anyone to assign themself to a role with one signature except manager", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await checkErrorRevert( @@ -565,7 +565,7 @@ contract("Tasks extension", accounts => { }); it("should allow different modes of signing when assigning roles", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await executeSignedRoleAssignment({ @@ -582,7 +582,7 @@ contract("Tasks extension", accounts => { }); it("should not allow role assignment if none of the signers is manager", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await checkErrorRevert( @@ -599,7 +599,7 @@ contract("Tasks extension", accounts => { }); it("should allow to change manager role if the user agrees", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await executeSignedRoleAssignment({ @@ -616,7 +616,7 @@ contract("Tasks extension", accounts => { }); it("should not allow one-signature assignment of manager to a role if signer is not manager", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await checkErrorRevert( @@ -633,7 +633,7 @@ contract("Tasks extension", accounts => { }); it("should not allow assignment of manager role if the user does not agree", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await checkErrorRevert( @@ -650,7 +650,7 @@ contract("Tasks extension", accounts => { }); it("should not allow assignment of manager role if user is not an admin", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await checkErrorRevert( @@ -667,7 +667,7 @@ contract("Tasks extension", accounts => { }); it("should not allow removal of manager role", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await checkErrorRevert( @@ -687,7 +687,7 @@ contract("Tasks extension", accounts => { const newEvaluator = accounts[1]; expect(newEvaluator).to.not.equal(MANAGER); - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); // Setting the worker @@ -734,10 +734,10 @@ contract("Tasks extension", accounts => { }); it("should correctly increment `taskChangeNonce` for multiple updates on multiple tasks", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, true, { from: MANAGER }); const taskId1 = await tasks.getTaskCount(); - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, true, { from: MANAGER }); const taskId2 = await tasks.getTaskCount(); // Change the task1 brief @@ -781,7 +781,7 @@ contract("Tasks extension", accounts => { }); it("should allow update of task brief signed by manager only when worker has not been assigned", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await executeSignedTaskChange({ @@ -798,7 +798,7 @@ contract("Tasks extension", accounts => { }); it("should allow update of task brief signed by manager and worker", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await executeSignedRoleAssignment({ @@ -824,7 +824,7 @@ contract("Tasks extension", accounts => { }); it("should allow update of task brief signed by manager and worker using Trezor-style signatures", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await executeSignedRoleAssignment({ @@ -850,7 +850,7 @@ contract("Tasks extension", accounts => { }); it("should allow update of task brief signed by manager and worker if one uses Trezor-style signatures and the other does not", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await executeSignedRoleAssignment({ @@ -876,7 +876,7 @@ contract("Tasks extension", accounts => { }); it("should not allow update of task brief signed by manager twice, with two different signature styles", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await executeSignedRoleAssignment({ @@ -903,7 +903,7 @@ contract("Tasks extension", accounts => { it("should allow update of task due date signed by manager and worker", async () => { const dueDate = await currentBlockTime(); - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await executeSignedRoleAssignment({ @@ -929,14 +929,14 @@ contract("Tasks extension", accounts => { }); it("should fail if a non-colony call is made to the task update functions", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await checkErrorRevert(tasks.setTaskBrief(taskId, SPECIFICATION_HASH_UPDATED, { from: OTHER }), "task-not-self"); }); it("should fail update of task brief signed by a non-registered role", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await checkErrorRevert( @@ -953,7 +953,7 @@ contract("Tasks extension", accounts => { }); it("should fail update of task brief signed by manager and evaluator", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await executeSignedRoleAssignment({ @@ -979,7 +979,7 @@ contract("Tasks extension", accounts => { }); it("should fail to execute task change for a non-registered function signature", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await checkErrorRevert( @@ -996,7 +996,7 @@ contract("Tasks extension", accounts => { }); it("should fail to execute change of task brief, using an invalid taskId", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); const taskCount = await tasks.getTaskCount(); const nonExistentTaskId = taskCount.addn(10); @@ -1031,7 +1031,7 @@ contract("Tasks extension", accounts => { }); it("should fail to execute task changes, when trying to set skill to 0", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await checkErrorRevert( @@ -1047,13 +1047,13 @@ contract("Tasks extension", accounts => { ); }); - it("should fail to execute task change, if the task is already finalized", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + it("should fail to execute task change, if the task is already complete", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); await submitDeliverableAndRatings({ tasks, taskId }); - await tasks.finalizeTask(1, 0, taskId, { from: MANAGER }); + await tasks.finalizeSecureTask(1, 0, taskId, { from: MANAGER }); await checkErrorRevert( executeSignedTaskChange({ @@ -1064,12 +1064,12 @@ contract("Tasks extension", accounts => { sigTypes: [0], args: [taskId, SPECIFICATION_HASH_UPDATED] }), - "colony-task-finalized" + "task-complete" ); }); it("should fail to change task manager, if the task is complete", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await forwardTime(90 * SECONDS_PER_DAY); @@ -1084,12 +1084,12 @@ contract("Tasks extension", accounts => { sigTypes: [0, 0], args: [taskId, ADMIN, 1, 0] }), - "olony-task-role-assignment-execution-failed" + "task-complete" ); }); it("should log a TaskBriefSet event, if the task brief gets changed", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await expectEvent( @@ -1106,7 +1106,7 @@ contract("Tasks extension", accounts => { }); it("should log a TaskDueDateSet event, if the task due date gets changed", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); const dueDate = await currentBlockTime(); @@ -1124,7 +1124,7 @@ contract("Tasks extension", accounts => { }); it("should fail to execute task change with a non zero value", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); const { sigV, sigR, sigS, txData } = await getSigsAndTransactionData({ @@ -1140,7 +1140,7 @@ contract("Tasks extension", accounts => { }); it("should fail to execute task change with a mismatched set of signature parts", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); const { sigV, sigR, sigS, txData } = await getSigsAndTransactionData({ @@ -1156,7 +1156,7 @@ contract("Tasks extension", accounts => { }); it("should fail to execute task change send for a task role assignment call (which should be using executeTaskRoleAssignment)", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); const { sigV, sigR, sigS, txData } = await getSigsAndTransactionData({ @@ -1172,7 +1172,7 @@ contract("Tasks extension", accounts => { }); it("should fail to execute task change with the wrong signatures, one signer", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); const { sigV, sigR, sigS, txData } = await getSigsAndTransactionData({ @@ -1188,7 +1188,7 @@ contract("Tasks extension", accounts => { }); it("should fail to execute task change with the wrong signatures, two signers", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await executeSignedRoleAssignment({ @@ -1215,7 +1215,7 @@ contract("Tasks extension", accounts => { describe("when submitting task deliverable", () => { it("should update task", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await assignRoles({ tasks, taskId, manager: MANAGER, evaluator: EVALUATOR, worker: WORKER }); @@ -1228,7 +1228,7 @@ contract("Tasks extension", accounts => { }); it("should fail if I try to submit work for a task that is complete", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await assignRoles({ tasks, taskId, manager: MANAGER, evaluator: EVALUATOR, worker: WORKER }); @@ -1239,18 +1239,18 @@ contract("Tasks extension", accounts => { }); it("should fail if I try to submit work for a task that is finalized", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); await submitDeliverableAndRatings({ tasks, taskId }); - await tasks.finalizeTask(1, 0, taskId, { from: MANAGER }); + await tasks.finalizeSecureTask(1, 0, taskId, { from: MANAGER }); await checkErrorRevert(tasks.submitTaskDeliverable(taskId, DELIVERABLE_HASH, { from: WORKER }), "task-complete"); }); it("should succeed if I try to submit work for a task that is past its due date but not yet marked as complete", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); @@ -1262,14 +1262,14 @@ contract("Tasks extension", accounts => { }); it("should fail if I try to submit work for a task using an invalid id", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await checkErrorRevert(tasks.submitTaskDeliverable(taskId.addn(1), DELIVERABLE_HASH), "task-does-not-exist"); }); it("should fail if I try to submit work twice", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); @@ -1279,7 +1279,7 @@ contract("Tasks extension", accounts => { }); it("should fail if I try to mark a taske complete after work is submitted", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); @@ -1289,7 +1289,7 @@ contract("Tasks extension", accounts => { }); it("should fail if I try to submit work if I'm not the assigned worker", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await assignRoles({ tasks, taskId, manager: MANAGER, worker: OTHER }); @@ -1297,7 +1297,7 @@ contract("Tasks extension", accounts => { }); it("should log a TaskDeliverableSubmitted event", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); @@ -1305,7 +1305,7 @@ contract("Tasks extension", accounts => { }); it("should fail if I try to complete the task before the due date", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); @@ -1315,14 +1315,14 @@ contract("Tasks extension", accounts => { describe("when evaluating a task", () => { it("should fail if I try to evaluate before work is submitted", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await checkErrorRevert(tasks.submitTaskWorkRating(taskId, WORKER_ROLE, RATING_2_SECRET, { from: EVALUATOR }), "task-not-complete"); }); it("should fail if I try to evaluate twice", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); @@ -1332,7 +1332,7 @@ contract("Tasks extension", accounts => { }); it("should fail if the wrong user tries to rate the wrong role", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); @@ -1345,7 +1345,7 @@ contract("Tasks extension", accounts => { }); it("can retreive rating secret information", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); @@ -1362,7 +1362,7 @@ contract("Tasks extension", accounts => { }); it("should fail if the user tries to rate too late", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); @@ -1374,7 +1374,7 @@ contract("Tasks extension", accounts => { }); it("should not allow a user to reveal after the deadline, with two secrets", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); @@ -1390,7 +1390,7 @@ contract("Tasks extension", accounts => { }); it("should not allow a user to reveal after the deadline, with one secret", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); @@ -1405,7 +1405,7 @@ contract("Tasks extension", accounts => { }); it("should not allow a user to reveal during the submission period", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); @@ -1419,7 +1419,7 @@ contract("Tasks extension", accounts => { }); it("should not allow a user to reveal a non-matching rating", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); @@ -1431,7 +1431,7 @@ contract("Tasks extension", accounts => { }); it("should not allow a user to reveal a rating of None", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); @@ -1445,12 +1445,12 @@ contract("Tasks extension", accounts => { describe("when finalizing a task", () => { it('should set the task "status" property to "finalized"', async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); await submitDeliverableAndRatings({ tasks, taskId }); - await tasks.finalizeTask(1, 0, taskId, { from: MANAGER }); + await tasks.finalizeSecureTask(1, 0, taskId, { from: MANAGER }); const task = await tasks.getTask(taskId); const expenditure = await colony.getExpenditure(task.expenditureId); @@ -1458,46 +1458,53 @@ contract("Tasks extension", accounts => { }); it("should fail if I try to finalize a task twice", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); await submitDeliverableAndRatings({ tasks, taskId }); - await tasks.finalizeTask(1, 0, taskId, { from: MANAGER }); + await tasks.finalizeSecureTask(1, 0, taskId, { from: MANAGER }); - await checkErrorRevert(tasks.finalizeTask(1, 0, taskId, { from: MANAGER }), "colony-expenditure-not-active"); + await checkErrorRevert(tasks.finalizeSecureTask(1, 0, taskId, { from: MANAGER }), "colony-expenditure-not-active"); + }); + + it("should fail if I try to finalize a secure task using finalizeManagedTask", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, true, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await checkErrorRevert(tasks.finalizeManagedTask(taskId), "task-not-managed"); }); it("should fail if I try to finalize a task that is not complete", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); - await checkErrorRevert(tasks.finalizeTask(1, 0, taskId), "task-not-complete"); + await checkErrorRevert(tasks.finalizeSecureTask(1, 0, taskId), "task-not-complete"); }); it("should fail if the task work ratings have not been assigned and they still have time to be", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); await tasks.submitTaskDeliverable(taskId, SPECIFICATION_HASH, { from: WORKER }); - await checkErrorRevert(tasks.finalizeTask(1, 0, taskId), "task-ratings-not-closed"); + await checkErrorRevert(tasks.finalizeSecureTask(1, 0, taskId), "task-ratings-not-closed"); }); it("should fail if the task work ratings have not been revealed and they still have time to be", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); await tasks.submitTaskDeliverableAndRating(taskId, SPECIFICATION_HASH, RATING_1_SECRET, { from: WORKER }); await tasks.submitTaskWorkRating(taskId, WORKER_ROLE, RATING_2_SECRET, { from: MANAGER }); - await checkErrorRevert(tasks.finalizeTask(1, 0, taskId), "task-ratings-not-closed"); + await checkErrorRevert(tasks.finalizeSecureTask(1, 0, taskId), "task-ratings-not-closed"); }); it("should finalize if the rate and reveal period have elapsed", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); @@ -1505,26 +1512,26 @@ contract("Tasks extension", accounts => { // No ratings submitted, so must wait for both rate and reveal periods to elapse await forwardTime(SECONDS_PER_DAY * 10 + 1); - await tasks.finalizeTask(1, 0, taskId, { from: MANAGER }); + await tasks.finalizeSecureTask(1, 0, taskId, { from: MANAGER }); }); it("should finalize if only the reveal period has elapsed after both secrets are submitted", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); await tasks.submitTaskDeliverableAndRating(taskId, SPECIFICATION_HASH, RATING_1_SECRET, { from: WORKER }); await tasks.submitTaskWorkRating(taskId, WORKER_ROLE, RATING_2_SECRET, { from: MANAGER }); - await checkErrorRevert(tasks.finalizeTask(1, 0, taskId), "task-ratings-not-closed"); + await checkErrorRevert(tasks.finalizeSecureTask(1, 0, taskId), "task-ratings-not-closed"); // Both secrets submitted, so we only have to wait for the reveal period to elapse await forwardTime(SECONDS_PER_DAY * 5 + 1); - await tasks.finalizeTask(1, 0, taskId, { from: MANAGER }); + await tasks.finalizeSecureTask(1, 0, taskId, { from: MANAGER }); }); it("should assign manager and worker maximum rating if unrated", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); const task = await tasks.getTask(taskId); @@ -1532,7 +1539,7 @@ contract("Tasks extension", accounts => { await tasks.submitTaskDeliverable(taskId, SPECIFICATION_HASH, { from: WORKER }); forwardTime(SECONDS_PER_DAY * 10 + 1); - await tasks.finalizeTask(1, 0, taskId, { from: MANAGER }); + await tasks.finalizeSecureTask(1, 0, taskId, { from: MANAGER }); const managerSlot = await colony.getExpenditureSlot(task.expenditureId, MANAGER_ROLE); const evaluatorSlot = await colony.getExpenditureSlot(task.expenditureId, EVALUATOR_ROLE); @@ -1544,7 +1551,7 @@ contract("Tasks extension", accounts => { }); it("should fail if it's not sufficiently funded to support all its payouts", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await executeSignedTaskChange({ @@ -1559,30 +1566,30 @@ contract("Tasks extension", accounts => { await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); await submitDeliverableAndRatings({ tasks, taskId }); - await checkErrorRevert(tasks.finalizeTask(1, 0, taskId), "colony-expenditure-not-funded"); + await checkErrorRevert(tasks.finalizeSecureTask(1, 0, taskId), "colony-expenditure-not-funded"); }); it("should fail if I try to accept a task that was finalized before", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); await submitDeliverableAndRatings({ tasks, taskId }); - await tasks.finalizeTask(1, 0, taskId, { from: MANAGER }); + await tasks.finalizeSecureTask(1, 0, taskId, { from: MANAGER }); - await checkErrorRevert(tasks.finalizeTask(1, 0, taskId), "colony-expenditure-not-active"); + await checkErrorRevert(tasks.finalizeSecureTask(1, 0, taskId), "colony-expenditure-not-active"); }); it("should fail if I try to accept a task using an invalid id", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); - await checkErrorRevert(tasks.finalizeTask(1, 0, taskId.addn(1)), "task-does-not-exist"); + await checkErrorRevert(tasks.finalizeSecureTask(1, 0, taskId.addn(1)), "task-does-not-exist"); }); it("should emit two negative reputation updates for a bad worker rating", async () => { await fundColonyWithTokens(colony, token, INITIAL_FUNDING); - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); const task = await tasks.getTask(taskId); const expenditure = await colony.getExpenditure(task.expenditureId); @@ -1601,7 +1608,7 @@ contract("Tasks extension", accounts => { await forwardTime(SECONDS_PER_DAY * 5 + 1); await tasks.revealTaskWorkRating(taskId, WORKER_ROLE, 1, RATING_2_SALT, { from: MANAGER }); await forwardTime(SECONDS_PER_DAY * 5 + 1); - await tasks.finalizeTask(1, 0, taskId, { from: MANAGER }); + await tasks.finalizeSecureTask(1, 0, taskId, { from: MANAGER }); const numEntriesAfter = await repCycle.getReputationUpdateLogLength(); expect(numEntriesAfter.sub(numEntriesBefore)).to.eq.BN(2); @@ -1621,7 +1628,7 @@ contract("Tasks extension", accounts => { it("should emit one negative reputation update for a bad manager/evaluator rating", async () => { await fundColonyWithTokens(colony, token, INITIAL_FUNDING); - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); const task = await tasks.getTask(taskId); const expenditure = await colony.getExpenditure(task.expenditureId); @@ -1634,7 +1641,7 @@ contract("Tasks extension", accounts => { await colony.moveFundsBetweenPots(1, 0, 0, domain1.fundingPotId, expenditure.fundingPotId, WAD, token.address); await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); await submitDeliverableAndRatings({ tasks, taskId, managerRating: 1 }); - await tasks.finalizeTask(1, 0, taskId, { from: MANAGER }); + await tasks.finalizeSecureTask(1, 0, taskId, { from: MANAGER }); const numEntriesAfter = await repCycle.getReputationUpdateLogLength(); expect(numEntriesAfter.sub(numEntriesBefore)).to.eq.BN(1); @@ -1648,7 +1655,7 @@ contract("Tasks extension", accounts => { describe("when cancelling a task", () => { it('should set the task "status" property to "cancelled"', async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await executeSignedTaskChange({ @@ -1666,7 +1673,7 @@ contract("Tasks extension", accounts => { }); it("should fail if manager tries to cancel a task that was completed", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); @@ -1681,12 +1688,12 @@ contract("Tasks extension", accounts => { sigTypes: [0, 0], args: [taskId] }), - "colony-task-change-execution-failed" + "task-complete" ); }); it("should fail if manager tries to cancel a task with invalid id", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await checkErrorRevert( @@ -1705,7 +1712,7 @@ contract("Tasks extension", accounts => { describe("when funding tasks", () => { it("should be able to set the task payouts for different roles", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await executeSignedRoleAssignment({ @@ -1793,7 +1800,7 @@ contract("Tasks extension", accounts => { }); it("should be able (if manager) to set all payments at once if evaluator and worker are manager or unassigned", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await checkErrorRevert( tasks.setAllTaskPayouts(taskId, ethers.constants.AddressZero, 5000, 1000, 98000, { from: OTHER }), @@ -1812,7 +1819,7 @@ contract("Tasks extension", accounts => { }); it("should not be able to set all payments at once if worker is assigned and is not the manager", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await executeSignedRoleAssignment({ @@ -1828,7 +1835,7 @@ contract("Tasks extension", accounts => { }); it("should not be able to set all payments at once if evaluator is assigned and is not the manager", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await executeSignedTaskChange({ @@ -1853,7 +1860,7 @@ contract("Tasks extension", accounts => { }); it("should correctly return the current total payout", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await tasks.setAllTaskPayouts(taskId, token.address, MANAGER_PAYOUT, EVALUATOR_PAYOUT, WORKER_PAYOUT); @@ -1867,7 +1874,7 @@ contract("Tasks extension", accounts => { it("should be possible to return funds back to the domain if cancelled", async () => { await fundColonyWithTokens(colony, token, INITIAL_FUNDING); - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await tasks.setAllTaskPayouts(taskId, token.address, 0, 0, WAD); @@ -1930,7 +1937,7 @@ contract("Tasks extension", accounts => { it("should payout agreed ether and tokens for a task", async () => { await fundColonyWithTokens(colony, token, INITIAL_FUNDING); - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); // Setup payouts @@ -1948,7 +1955,7 @@ contract("Tasks extension", accounts => { // Complete task await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); await submitDeliverableAndRatings({ tasks, taskId }); - await tasks.finalizeTask(1, 0, taskId, { from: MANAGER }); + await tasks.finalizeSecureTask(1, 0, taskId, { from: MANAGER }); // Claim payouts const workerEtherBalanceBefore = await web3GetBalance(WORKER); @@ -1967,7 +1974,7 @@ contract("Tasks extension", accounts => { it("should disburse nothing for unsatisfactory work, for manager and worker", async () => { await fundColonyWithTokens(colony, token, INITIAL_FUNDING); - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await tasks.setAllTaskPayouts(taskId, token.address, MANAGER_PAYOUT, EVALUATOR_PAYOUT, WORKER_PAYOUT); @@ -1979,7 +1986,7 @@ contract("Tasks extension", accounts => { await assignRoles({ tasks, taskId, manager: MANAGER, evaluator: EVALUATOR, worker: WORKER }); await submitDeliverableAndRatings({ tasks, taskId, managerRating: 1, workerRating: 1 }); - await tasks.finalizeTask(1, 0, taskId, { from: MANAGER }); + await tasks.finalizeSecureTask(1, 0, taskId, { from: MANAGER }); const managerSlot = await colony.getExpenditureSlot(task.expenditureId, MANAGER_ROLE); const evaluatorSlot = await colony.getExpenditureSlot(task.expenditureId, EVALUATOR_ROLE); @@ -2009,7 +2016,7 @@ contract("Tasks extension", accounts => { it("should disburse nothing for unsatisfactory work, for evaluator", async () => { await fundColonyWithTokens(colony, token, INITIAL_FUNDING); - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await tasks.setAllTaskPayouts(taskId, token.address, MANAGER_PAYOUT, EVALUATOR_PAYOUT, WORKER_PAYOUT); @@ -2025,7 +2032,7 @@ contract("Tasks extension", accounts => { await forwardTime(SECONDS_PER_DAY * 5 + 1); await tasks.revealTaskWorkRating(taskId, MANAGER_ROLE, MANAGER_RATING, RATING_1_SALT, { from: WORKER }); await forwardTime(SECONDS_PER_DAY * 5 + 1); - await tasks.finalizeTask(1, 0, taskId, { from: MANAGER }); + await tasks.finalizeSecureTask(1, 0, taskId, { from: MANAGER }); const managerSlot = await colony.getExpenditureSlot(task.expenditureId, MANAGER_ROLE); const evaluatorSlot = await colony.getExpenditureSlot(task.expenditureId, EVALUATOR_ROLE); @@ -2063,7 +2070,7 @@ contract("Tasks extension", accounts => { it("should automatically reclaim funds after unsatisfactory reviews", async () => { await fundColonyWithTokens(colony, token, INITIAL_FUNDING); - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); await tasks.setAllTaskPayouts(taskId, token.address, 0, 0, WORKER_PAYOUT); @@ -2074,7 +2081,7 @@ contract("Tasks extension", accounts => { await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); await submitDeliverableAndRatings({ tasks, taskId, workerRating: 1 }); - await tasks.finalizeTask(1, 0, taskId, { from: MANAGER }); + await tasks.finalizeSecureTask(1, 0, taskId, { from: MANAGER }); const balanceBefore = await colony.getFundingPotBalance(domain1.fundingPotId, token.address); await colony.claimExpenditurePayout(task.expenditureId, WORKER_ROLE, token.address); @@ -2083,11 +2090,128 @@ contract("Tasks extension", accounts => { }); it("should return error when task is not finalized", async () => { - await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, { from: MANAGER }); + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, true, { from: MANAGER }); const taskId = await tasks.getTaskCount(); const task = await tasks.getTask(taskId); await checkErrorRevert(colony.claimExpenditurePayout(task.expenditureId, MANAGER_ROLE, token.address), "colony-expenditure-not-finalized"); }); }); + + describe("managed tasks", () => { + it("should allow admins to make managed tasks", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, false, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + const task = await tasks.getTask(taskId); + expect(task.secure).to.be.false; + }); + + it("should allow managers to edit task attributes without multi-sig", async () => { + const currTime = await currentBlockTime(); + const dueDate = currTime + SECONDS_PER_DAY; + + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, false, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await tasks.setTaskManagerRole(taskId, ADMIN, 1, 0); + await tasks.setTaskManagerRole(taskId, MANAGER, 1, 0, { from: ADMIN }); + + await tasks.setTaskSkill(taskId, GLOBAL_SKILL_ID); + + // await tasks.setTaskEvaluatorRole(taskId, EVALUATOR); // Except this one! + await tasks.setTaskWorkerRole(taskId, WORKER); + + await tasks.setTaskBrief(taskId, SPECIFICATION_HASH_UPDATED); + await tasks.setTaskDueDate(taskId, dueDate); + + await tasks.setTaskManagerPayout(taskId, token.address, MANAGER_PAYOUT); + await tasks.setTaskEvaluatorPayout(taskId, token.address, EVALUATOR_PAYOUT); + await tasks.setTaskWorkerPayout(taskId, token.address, WORKER_PAYOUT); + + await tasks.removeTaskEvaluatorRole(taskId); + await tasks.removeTaskWorkerRole(taskId); + + await tasks.cancelTask(taskId); + }); + + it("should not allow managers to set an evaluator", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, false, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await checkErrorRevert(tasks.setTaskEvaluatorRole(taskId, EVALUATOR), "task-not-secure"); + }); + + it("should not allow participants to submit work or ratings", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, false, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await tasks.setTaskWorkerRole(taskId, WORKER); + + await checkErrorRevert(tasks.submitTaskDeliverable(taskId, DELIVERABLE_HASH, { from: WORKER }), "task-not-secure"); + await checkErrorRevert(tasks.submitTaskWorkRating(taskId, MANAGER_ROLE, RATING_1_SECRET, { from: WORKER }), "task-not-secure"); + await checkErrorRevert(tasks.submitTaskWorkRating(taskId, WORKER_ROLE, RATING_2_SECRET, { from: MANAGER }), "task-not-secure"); + }); + + it("should allow managers to finalize a task, with an implicit rating of 2", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, false, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await tasks.finalizeManagedTask(taskId); + + const task = await tasks.getTask(taskId); + const managerSlot = await colony.getExpenditureSlot(task.expenditureId, MANAGER_ROLE); + const evaluatorSlot = await colony.getExpenditureSlot(task.expenditureId, EVALUATOR_ROLE); + const workerSlot = await colony.getExpenditureSlot(task.expenditureId, WORKER_ROLE); + expect(managerSlot.payoutModifier).to.be.zero; // rating of 2 + expect(evaluatorSlot.payoutModifier).to.be.zero; // rating of 2 + expect(workerSlot.payoutModifier).to.be.zero; // rating of 2 + }); + + it("should allow managers to convert managed tasks to secure tasks (and back)", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, false, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + let task; + task = await tasks.getTask(taskId); + expect(task.secure).to.be.false; + + await tasks.setTaskWorkerRole(taskId, WORKER); + + await tasks.setTaskSecurity(taskId, true); + task = await tasks.getTask(taskId); + expect(task.secure).to.be.true; + + await executeSignedRoleAssignment({ + tasks, + taskId, + functionName: "setTaskEvaluatorRole", + signers: [MANAGER, EVALUATOR], + sigTypes: [0, 0], + args: [taskId, EVALUATOR] + }); + + await executeSignedTaskChange({ + tasks, + taskId, + functionName: "setTaskSecurity", + signers: [MANAGER, WORKER], + sigTypes: [0, 0], + args: [taskId, false] + }); + + task = await tasks.getTask(taskId); + expect(task.secure).to.be.false; + + // Setting to managed removes the evaluator + const evaluator = await tasks.getTaskRoleUser(taskId, EVALUATOR_ROLE); + expect(evaluator).to.equal(ethers.constants.AddressZero); + }); + + it("should not allow managers to finalize a managed task using finalizeSecureTask ", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, false, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await checkErrorRevert(tasks.finalizeSecureTask(1, 0, taskId), "task-not-secure"); + }); + }); });