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/contracts/extensions/Tasks.sol b/contracts/extensions/Tasks.sol new file mode 100644 index 0000000000..8d6874f3f9 --- /dev/null +++ b/contracts/extensions/Tasks.sol @@ -0,0 +1,732 @@ +/* + 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 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 + 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; + bool secure; + } + + 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("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]; + // 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(uint256 _id) { + require(managerCanCall(_id) || address(this) == msg.sender, "task-not-self"); + _; + } + + ColonyDataTypes.ColonyRole constant ADMIN = ColonyDataTypes.ColonyRole.Administration; + modifier isAdmin(address _user, uint256 _permissionDomainId, uint256 _childSkillIndex, uint256 _domainId) { + require(colony.hasInheritedUserRole(_user, _permissionDomainId, ADMIN, _childSkillIndex, _domainId), "task-not-admin"); + _; + } + + modifier taskExists(uint256 _id) { + require(doesTaskExist(_id), "task-does-not-exist"); + _; + } + + 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"); + _; + } + + modifier taskNotComplete(uint256 _id) { + require(!isTaskComplete(_id), "task-complete"); + _; + } + + modifier confirmTaskRoleIdentity(uint256 _id, address _user, TaskRole _role) { + require(getTaskRoleUser(_id, _role) == msg.sender, "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, "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(doesTaskExist(taskId), "task-does-not-exist"); + require(!isTaskComplete(taskId), "task-complete"); + require(!roleAssignmentSigs[sig], "task-change-is-role-assign"); + + 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, "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, + "task-sigs-no-match-reviewer-1" + ); + + if (nSignaturesRequired == 2) { + require(reviewerAddresses[0] != reviewerAddresses[1], "task-duplicate-reviewers"); + require( + reviewerAddresses[1] == taskRole1User || reviewerAddresses[1] == taskRole2User, + "task-sigs-no-match-reviewer-2" + ); + } + + tasks[taskId].changeNonce += 1; + require(executeCall(address(this), _value, _data), "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, "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(doesTaskExist(taskId), "task-does-not-exist"); + require(!isTaskComplete(taskId), "task-complete"); + require(roleAssignmentSigs[sig], "task-change-is-not-role-assign"); + + 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, "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, "task-role-assign-no-manager-sig"); + } else { + // One of signers must be a manager + require( + reviewerAddresses[0] == manager || + reviewerAddresses[1] == 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], + "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], "task-role-assign-duplicate-sigs"); + } + + tasks[taskId].changeNonce += 1; + require(executeCall(address(this), _value, _data), "task-role-assign-exec-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, + bool _secure + ) + 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? + tasks[taskCount].secure = _secure; + + setTaskRoleUser(taskCount, TaskRole.Manager, msg.sender); + + if (_secure) { + 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 + taskExists(_id) + taskSecure(_id) + taskComplete(_id) + { + if (_role == TaskRole.Manager) { // Manager rated by worker + 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), "task-user-cannot-rate-worker"); + } else { + revert("task-unsupported-role-to-rate"); + } + + 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; + 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, "task-secret-reveal-closed"); + } else { + uint taskCompletionTime = tasks[_id].completionTimestamp; + 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)], "task-secret-mismatch"); + + TaskRatings rating = TaskRatings(_rating); + require(rating != TaskRatings.None, "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]; + } + + 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(_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(_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(_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; + require(skills.length > 0 && skills[0] > 0, "task-skill-not-set"); // ignore-swc-110 + setTaskRoleUser(_id, TaskRole.Worker, _user); + } + + function removeTaskEvaluatorRole(uint256 _id) public self(_id) { + setTaskRoleUser(_id, TaskRole.Evaluator, address(0x0)); + } + + function removeTaskWorkerRole(uint256 _id) public self(_id) { + setTaskRoleUser(_id, TaskRole.Worker, address(0x0)); + } + + 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(_id) { + colony.setExpenditurePayout(_id, uint256(TaskRole.Evaluator), _token, _amount); + } + + function setTaskWorkerPayout(uint256 _id, address _token, uint256 _amount) public self(_id) { + 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), "task-evaluator-already-set"); + require(worker == manager || worker == address(0x0), "task-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(_id) { + colony.setExpenditureSkill(tasks[_id].expenditureId, uint256(TaskRole.Worker), _skillId); + } + + function setTaskBrief(uint256 _id, bytes32 _specificationHash) + public + self(_id) + taskExists(_id) + { + tasks[_id].specificationHash = _specificationHash; + + emit TaskBriefSet(_id, _specificationHash); + } + + function setTaskDueDate(uint256 _id, uint256 _dueDate) + public + self(_id) + taskExists(_id) + { + tasks[_id].dueDate = _dueDate; + + emit TaskDueDateSet(_id, _dueDate); + } + + function submitTaskDeliverable(uint256 _id, bytes32 _deliverableHash) + public + taskExists(_id) + taskSecure(_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) + taskSecure(_id) + taskNotComplete(_id) + confirmTaskRoleIdentity(_id, msg.sender, TaskRole.Manager) + { + require(now >= tasks[_id].dueDate, "task-due-date-in-future"); + tasks[_id].completionTimestamp = now; + + emit TaskCompleted(_id); + } + + function cancelTask(uint256 _id) + public + self(_id) + taskExists(_id) + { + colony.cancelExpenditure(tasks[_id].expenditureId); + } + + // Permissions pertain to the Arbitration role here + function finalizeSecureTask(uint256 _permissionDomainId, uint256 _childSkillIndex, uint256 _id) + public + taskExists(_id) + taskSecure(_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 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; + } + + 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), "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) internal { + taskRoles[_id][uint8(_role)] = Role({ rateFail: false, rating: TaskRatings.None }); + + colony.setExpenditureRecipient(tasks[_id].expenditureId, uint256(_role), _user); + } + + function doesTaskExist(uint256 _id) internal view returns (bool) { + 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/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/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/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/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); diff --git a/test/extensions/tasks.js b/test/extensions/tasks.js new file mode 100644 index 0000000000..1b8f9885c0 --- /dev/null +++ b/test/extensions/tasks.js @@ -0,0 +1,2217 @@ +/* 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, true, { 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, 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, true, { 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, true, { 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, true, { 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, true); + 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, 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, true); + 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, true); + 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, true, { 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] + }), + "task-does-not-exist" + ); + }); + + 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.finalizeSecureTask(1, 0, taskId, { from: MANAGER }); + + await checkErrorRevert( + executeSignedRoleAssignment({ + tasks, + taskId, + functionName: "setTaskWorkerRole", + signers: [MANAGER, WORKER], + sigTypes: [0, 0], + args: [taskId, WORKER] + }), + "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, true, { 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] + }), + "task-change-is-not-role-assign" + ); + }); + + 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, true, { 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), "task-role-assign-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, true, { 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), "task-role-assign-sig-count-no-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, true, { 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, true, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await checkErrorRevert( + executeSignedRoleAssignment({ + tasks, + taskId, + functionName: "setTaskWorkerRole", + signers: [MANAGER, WORKER], + sigTypes: [0, 0], + args: [taskId, WORKER] + }), + "task-role-assign-exec-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, true, { 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] + }), + "task-role-assign-wrong-num-sigs" + ); + + await checkErrorRevert( + executeSignedRoleAssignment({ + tasks, + taskId, + functionName: "setTaskWorkerRole", + signers: [MANAGER], + sigTypes: [0], + args: [taskId, WORKER] + }), + "task-role-assign-wrong-num-sigs" + ); + }); + + 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, true, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await checkErrorRevert( + executeSignedRoleAssignment({ + tasks, + taskId, + functionName: "setTaskEvaluatorRole", + signers: [MANAGER, OTHER], + sigTypes: [0, 0], + args: [taskId, OTHER] + }), + "task-role-assign-exec-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] + }), + "task-role-assign-exec-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, true, { 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] + }), + "task-role-assign-no-new-user-sig" + ); + + // 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, true, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await checkErrorRevert( + executeSignedRoleAssignment({ + tasks, + taskId, + functionName: "setTaskWorkerRole", + signers: [MANAGER, OTHER], + sigTypes: [0, 0], + args: [taskId, WORKER] + }), + "task-role-assign-no-new-user-sig" + ); + }); + + 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, true, { 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, true, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await checkErrorRevert( + executeSignedRoleAssignment({ + tasks, + taskId, + functionName: "setTaskWorkerRole", + signers: [WORKER], + sigTypes: [0], + args: [taskId, WORKER] + }), + "task-role-assign-wrong-num-sigs" + ); + }); + + 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, true, { 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, true, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await checkErrorRevert( + executeSignedRoleAssignment({ + tasks, + taskId, + functionName: "setTaskWorkerRole", + signers: [WORKER, OTHER], + sigTypes: [0, 0], + args: [taskId, WORKER] + }), + "task-role-assign-no-manager-sig" + ); + }); + + it("should allow to change manager role if the user agrees", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, true, { 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, true, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await checkErrorRevert( + executeSignedRoleAssignment({ + tasks, + taskId, + functionName: "setTaskWorkerRole", + signers: [OTHER], + sigTypes: [0], + args: [taskId, MANAGER] + }), + "task-role-assign-no-manager-sig" + ); + }); + + 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, true, { 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] + }), + "task-role-assign-no-new-user-sig" + ); + }); + + 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, true, { 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] + }), + "task-role-assign-exec-failed" + ); + }); + + it("should not allow removal of manager role", async () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, true, { 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] + }), + "task-role-assign-no-new-user-sig" + ); + }); + + 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, true, { 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] + }), + "task-role-assign-no-manager-sig" + ); + }); + + it("should correctly increment `taskChangeNonce` for multiple updates on multiple tasks", async () => { + 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, true, { 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, true, { 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, true, { 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, true, { 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, true, { 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, true, { 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] + }), + "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, true, { 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, 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, true, { 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] + }), + "task-change-wrong-num-sigs" + ); + }); + + 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, true, { 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] + }), + "task-change-wrong-num-sigs" + ); + }); + + 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, true, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await checkErrorRevert( + executeSignedTaskChange({ + tasks, + taskId, + functionName: "getTaskRole", + signers: [MANAGER, EVALUATOR], + sigTypes: [0, 0], + args: [taskId, 0] + }), + "task-change-wrong-num-sigs" + ); + }); + + 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, true, { 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] + }), + "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] + }), + "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, true, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await checkErrorRevert( + executeSignedTaskChange({ + tasks, + taskId, + functionName: "setTaskSkill", + signers: [MANAGER], + sigTypes: [0], + args: [taskId, 0] + }), + "task-change-execution-failed" + ); + }); + + 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.finalizeSecureTask(1, 0, taskId, { from: MANAGER }); + + await checkErrorRevert( + executeSignedTaskChange({ + tasks, + taskId, + functionName: "setTaskBrief", + signers: [MANAGER], + sigTypes: [0], + args: [taskId, SPECIFICATION_HASH_UPDATED] + }), + "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, true, { 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] + }), + "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, true, { 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, true, { 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, true, { 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), "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, true, { 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), "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 () => { + 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({ + tasks, + taskId, + functionName: "setTaskEvaluatorRole", + signers: [MANAGER], + sigTypes: [0], + args: [taskId, "0x29738B9BB168790211D84C99c4AEAd215c34D731"] + }); + + 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 () => { + 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({ + tasks, + taskId, + functionName: "setTaskBrief", + signers: [OTHER], + sigTypes: [0], + args: [taskId, SPECIFICATION_HASH_UPDATED] + }); + + 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 () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, GLOBAL_SKILL_ID, 0, true, { 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), "task-sigs-no-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, true, { 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, true, { 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), "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, true, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); + await submitDeliverableAndRatings({ tasks, taskId }); + 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, true, { 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, 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, true, { 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 }), "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, true, { 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 }), "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, true, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await assignRoles({ tasks, taskId, manager: MANAGER, worker: OTHER }); + await checkErrorRevert(tasks.submitTaskDeliverable(taskId, SPECIFICATION_HASH, { from: WORKER }), "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, true, { 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, true, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); + await checkErrorRevert(tasks.completeTask(taskId, { from: MANAGER }), "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, 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, true, { 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 }), "task-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, true, { 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 }), "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 () => { + 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 }); + + 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, true, { 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 }), "task-secret-submissions-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, true, { 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 }), + "task-secret-reveal-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, true, { 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 }), + "task-secret-reveal-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, true, { 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 }), + "task-secret-reveal-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, true, { 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 }), "task-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, true, { 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 }), "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, true, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); + await submitDeliverableAndRatings({ tasks, taskId }); + await tasks.finalizeSecureTask(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, true, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); + await submitDeliverableAndRatings({ tasks, taskId }); + await tasks.finalizeSecureTask(1, 0, taskId, { from: MANAGER }); + + 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, true, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + 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, 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.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, 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.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, true, { 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.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, 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.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.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, true, { 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.finalizeSecureTask(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, true, { 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.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, true, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await assignRoles({ tasks, taskId, manager: MANAGER, worker: WORKER }); + await submitDeliverableAndRatings({ tasks, taskId }); + await tasks.finalizeSecureTask(1, 0, taskId, { from: MANAGER }); + + 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, true, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + 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, true, { 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.finalizeSecureTask(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, true, { 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.finalizeSecureTask(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, true, { 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, 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( + executeSignedTaskChange({ + tasks, + taskId, + functionName: "cancelTask", + signers: [MANAGER, WORKER], + sigTypes: [0, 0], + args: [taskId] + }), + "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, true, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + + await checkErrorRevert( + executeSignedTaskChange({ + tasks, + taskId, + functionName: "cancelTask", + signers: [MANAGER], + sigTypes: [0], + args: [taskId.addn(1)] + }), + "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, true, { 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, true, { from: MANAGER }); + const taskId = await tasks.getTaskCount(); + await checkErrorRevert( + tasks.setAllTaskPayouts(taskId, ethers.constants.AddressZero, 5000, 1000, 98000, { from: OTHER }), + "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, true, { 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), "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 () => { + await tasks.makeTask(1, 0, 1, 0, SPECIFICATION_HASH, 1, 0, 0, true, { 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), "task-evaluator-already-set"); + }); + + it("should correctly return the current total payout", async () => { + 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); + + 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, true, { 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, true, { 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.finalizeSecureTask(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, true, { 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.finalizeSecureTask(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, true, { 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.finalizeSecureTask(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, true, { 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.finalizeSecureTask(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, 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"); + }); + }); +});