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