diff --git a/contracts/colony/ColonyFunding.sol b/contracts/colony/ColonyFunding.sol index 8230abbfd5..5ddbb5face 100755 --- a/contracts/colony/ColonyFunding.sol +++ b/contracts/colony/ColonyFunding.sol @@ -22,16 +22,79 @@ import "./../tokenLocking/ITokenLocking.sol"; import "./ColonyStorage.sol"; -contract ColonyFunding is ColonyStorage, PatriciaTreeProofs { // ignore-swc-123 - function lockToken() public stoppable onlyOwnExtension returns (uint256) { - uint256 lockId = ITokenLocking(tokenLockingAddress).lockToken(token); - tokenLocks[msgSender()][lockId] = true; - return lockId; +contract ColonyFunding is ColonyStorage { // ignore-swc-123 + + // Public + + function moveFundsBetweenPots( + uint256 _permissionDomainId, + uint256 _childSkillIndex, + uint256 _domainId, + uint256 _fromChildSkillIndex, + uint256 _toChildSkillIndex, + uint256 _fromPot, + uint256 _toPot, + uint256 _amount, + address _token + ) + public + stoppable + domainNotDeprecated(getDomainFromFundingPot(_toPot)) + authDomain(_permissionDomainId, _childSkillIndex, _domainId) + validFundingTransfer(_fromPot, _toPot) + { + require(validateDomainInheritance(_domainId, _fromChildSkillIndex, getDomainFromFundingPot(_fromPot)), "colony-invalid-domain-inheritence"); + require(validateDomainInheritance(_domainId, _toChildSkillIndex, getDomainFromFundingPot(_toPot)), "colony-invalid-domain-inheritence"); + + moveFundsBetweenPotsFunctionality(_fromPot, _toPot, _amount, _token); } - function unlockTokenForUser(address _user, uint256 _lockId) public stoppable onlyOwnExtension { - require(tokenLocks[msgSender()][_lockId], "colony-bad-lock-id"); - ITokenLocking(tokenLockingAddress).unlockTokenForUser(token, _user, _lockId); + function moveFundsBetweenPots( + uint256 _permissionDomainId, + uint256 _fromChildSkillIndex, + uint256 _toChildSkillIndex, + uint256 _fromPot, + uint256 _toPot, + uint256 _amount, + address _token + ) + public + stoppable + domainNotDeprecated(getDomainFromFundingPot(_toPot)) + authDomain(_permissionDomainId, _fromChildSkillIndex, getDomainFromFundingPot(_fromPot)) + authDomain(_permissionDomainId, _toChildSkillIndex, getDomainFromFundingPot(_toPot)) + validFundingTransfer(_fromPot, _toPot) + { + moveFundsBetweenPotsFunctionality(_fromPot, _toPot, _amount, _token); + } + + function claimColonyFunds(address _token) public stoppable { + uint toClaim; + uint feeToPay; + uint remainder; + if (_token == address(0x0)) { + // It's ether + toClaim = sub(sub(address(this).balance, nonRewardPotsTotal[_token]), fundingPots[0].balance[_token]); + } else { + // Assume it's an ERC 20 token. + ERC20Extended targetToken = ERC20Extended(_token); + toClaim = sub(sub(targetToken.balanceOf(address(this)), nonRewardPotsTotal[_token]), fundingPots[0].balance[_token]); // ignore-swc-123 + } + + feeToPay = toClaim / getRewardInverse(); // ignore-swc-110 . This variable is set when the colony is + // initialised to MAX_UINT, and cannot be set to zero via setRewardInverse, so this is a false positive. It *can* be set + // to 0 via recovery mode, but a) That's not why MythX is balking here and b) There's only so much we can stop people being + // able to do with recovery mode. + remainder = sub(toClaim, feeToPay); + nonRewardPotsTotal[_token] = add(nonRewardPotsTotal[_token], remainder); + fundingPots[1].balance[_token] = add(fundingPots[1].balance[_token], remainder); + fundingPots[0].balance[_token] = add(fundingPots[0].balance[_token], feeToPay); + + emit ColonyFundsClaimed(msgSender(), _token, feeToPay, remainder); + } + + function getNonRewardPotsTotal(address _token) public view returns (uint256) { + return nonRewardPotsTotal[_token]; } function setTaskManagerPayout(uint256 _id, address _token, uint256 _amount) public stoppable self { @@ -49,37 +112,6 @@ contract ColonyFunding is ColonyStorage, PatriciaTreeProofs { // ignore-swc-123 emit TaskPayoutSet(_id, TaskRole.Worker, _token, _amount); } - function setAllTaskPayouts( - uint256 _id, - address _token, - uint256 _managerAmount, - uint256 _evaluatorAmount, - uint256 _workerAmount - ) - public - stoppable - confirmTaskRoleIdentity(_id, TaskRole.Manager) - { - Task storage task = tasks[_id]; - address manager = task.roles[uint8(TaskRole.Manager)].user; - address evaluator = task.roles[uint8(TaskRole.Evaluator)].user; - address worker = task.roles[uint8(TaskRole.Worker)].user; - - require( - evaluator == manager || - evaluator == address(0x0), - "colony-funding-evaluator-already-set"); - - require( - worker == manager || - worker == address(0x0), - "colony-funding-worker-already-set"); - - this.setTaskManagerPayout(_id, _token, _managerAmount); - this.setTaskEvaluatorPayout(_id, _token, _evaluatorAmount); - this.setTaskWorkerPayout(_id, _token, _workerAmount); - } - // To get all payouts for a task iterate over roles.length function getTaskPayout(uint256 _id, uint8 _role, address _token) public view returns (uint256) { Task storage task = tasks[_id]; @@ -106,6 +138,27 @@ contract ColonyFunding is ColonyStorage, PatriciaTreeProofs { // ignore-swc-123 } } + function setExpenditurePayouts(uint256 _id, uint256[] memory _slots, address _token, uint256[] memory _amounts) + public + stoppable + expenditureExists(_id) + expenditureDraft(_id) + expenditureOnlyOwner(_id) + { + setExpenditurePayoutsInternal(_id, _slots, _token, _amounts); + } + + function setExpenditurePayout(uint256 _id, uint256 _slot, address _token, uint256 _amount) + public + stoppable + { + uint256[] memory slots = new uint256[](1); + slots[0] = _slot; + uint256[] memory amounts = new uint256[](1); + amounts[0] = _amount; + setExpenditurePayouts(_id, slots, _token, amounts); + } + int256 constant MAX_PAYOUT_MODIFIER = int256(WAD); int256 constant MIN_PAYOUT_MODIFIER = -int256(WAD); @@ -157,17 +210,6 @@ contract ColonyFunding is ColonyStorage, PatriciaTreeProofs { // ignore-swc-123 processPayout(expenditure.fundingPotId, _token, tokenPayout, slot.recipient); } - function claimPayment(uint256 _id, address _token) public - stoppable - paymentFinalized(_id) - { - Payment storage payment = payments[_id]; - FundingPot storage fundingPot = fundingPots[payment.fundingPotId]; - assert(fundingPot.balance[_token] >= fundingPot.payouts[_token]); - - processPayout(payment.fundingPotId, _token, fundingPot.payouts[_token], payment.recipient); - } - function setPaymentPayout(uint256 _permissionDomainId, uint256 _childSkillIndex, uint256 _id, address _token, uint256 _amount) public stoppable authDomain(_permissionDomainId, _childSkillIndex, payments[_id].domainId) @@ -186,6 +228,19 @@ contract ColonyFunding is ColonyStorage, PatriciaTreeProofs { // ignore-swc-123 emit PaymentPayoutSet(msgSender(), _id, _token, _amount); } + function claimPayment(uint256 _id, address _token) public + stoppable + paymentFinalized(_id) + { + Payment storage payment = payments[_id]; + FundingPot storage fundingPot = fundingPots[payment.fundingPotId]; + assert(fundingPot.balance[_token] >= fundingPot.payouts[_token]); + + processPayout(payment.fundingPotId, _token, fundingPot.payouts[_token], payment.recipient); + } + + // View + function getFundingPotCount() public view returns (uint256 count) { return fundingPotCount; } @@ -205,252 +260,30 @@ contract ColonyFunding is ColonyStorage, PatriciaTreeProofs { // ignore-swc-123 return (fundingPot.associatedType, fundingPot.associatedTypeId, fundingPot.payoutsWeCannotMake); } - function moveFundsBetweenPots( - uint256 _permissionDomainId, - uint256 _childSkillIndex, - uint256 _domainId, - uint256 _fromChildSkillIndex, - uint256 _toChildSkillIndex, - uint256 _fromPot, - uint256 _toPot, - uint256 _amount, - address _token - ) - public - stoppable - domainNotDeprecated(getDomainFromFundingPot(_toPot)) - authDomain(_permissionDomainId, _childSkillIndex, _domainId) - validFundingTransfer(_fromPot, _toPot) - { - require(validateDomainInheritance(_domainId, _fromChildSkillIndex, getDomainFromFundingPot(_fromPot)), "colony-invalid-domain-inheritence"); - require(validateDomainInheritance(_domainId, _toChildSkillIndex, getDomainFromFundingPot(_toPot)), "colony-invalid-domain-inheritence"); - - moveFundsBetweenPotsFunctionality(_fromPot, _toPot, _amount, _token); - } - - function moveFundsBetweenPots( - uint256 _permissionDomainId, - uint256 _fromChildSkillIndex, - uint256 _toChildSkillIndex, - uint256 _fromPot, - uint256 _toPot, - uint256 _amount, - address _token - ) - public - stoppable - domainNotDeprecated(getDomainFromFundingPot(_toPot)) - authDomain(_permissionDomainId, _fromChildSkillIndex, getDomainFromFundingPot(_fromPot)) - authDomain(_permissionDomainId, _toChildSkillIndex, getDomainFromFundingPot(_toPot)) - validFundingTransfer(_fromPot, _toPot) - { - moveFundsBetweenPotsFunctionality(_fromPot, _toPot, _amount, _token); - } + function getDomainFromFundingPot(uint256 _fundingPotId) public view returns (uint256 domainId) { + require(_fundingPotId <= fundingPotCount, "colony-funding-nonexistent-pot"); + FundingPot storage fundingPot = fundingPots[_fundingPotId]; - function claimColonyFunds(address _token) public stoppable { - uint toClaim; - uint feeToPay; - uint remainder; - if (_token == address(0x0)) { - // It's ether - toClaim = sub(sub(address(this).balance, nonRewardPotsTotal[_token]), fundingPots[0].balance[_token]); + if (fundingPot.associatedType == FundingPotAssociatedType.Domain) { + domainId = fundingPot.associatedTypeId; + } else if (fundingPot.associatedType == FundingPotAssociatedType.Task) { + domainId = tasks[fundingPot.associatedTypeId].domainId; + } else if (fundingPot.associatedType == FundingPotAssociatedType.Payment) { + domainId = payments[fundingPot.associatedTypeId].domainId; + } else if (fundingPot.associatedType == FundingPotAssociatedType.Expenditure) { + domainId = expenditures[fundingPot.associatedTypeId].domainId; } else { - // Assume it's an ERC 20 token. - ERC20Extended targetToken = ERC20Extended(_token); - toClaim = sub(sub(targetToken.balanceOf(address(this)), nonRewardPotsTotal[_token]), fundingPots[0].balance[_token]); // ignore-swc-123 + // If rewards pot, return root domain. + assert(_fundingPotId == 0); + domainId = 1; } - - feeToPay = toClaim / getRewardInverse(); // ignore-swc-110 . This variable is set when the colony is - // initialised to MAX_UINT, and cannot be set to zero via setRewardInverse, so this is a false positive. It *can* be set - // to 0 via recovery mode, but a) That's not why MythX is balking here and b) There's only so much we can stop people being - // able to do with recovery mode. - remainder = sub(toClaim, feeToPay); - nonRewardPotsTotal[_token] = add(nonRewardPotsTotal[_token], remainder); - fundingPots[1].balance[_token] = add(fundingPots[1].balance[_token], remainder); - fundingPots[0].balance[_token] = add(fundingPots[0].balance[_token], feeToPay); - - emit ColonyFundsClaimed(msgSender(), _token, feeToPay, remainder); - } - - function getNonRewardPotsTotal(address _token) public view returns (uint256) { - return nonRewardPotsTotal[_token]; - } - - function startNextRewardPayout(address _token, bytes memory key, bytes memory value, uint256 branchMask, bytes32[] memory siblings) - public stoppable auth - { - ITokenLocking tokenLocking = ITokenLocking(tokenLockingAddress); - uint256 totalLockCount = tokenLocking.lockToken(token); - uint256 thisPayoutAmount = sub(fundingPots[0].balance[_token], pendingRewardPayments[_token]); - require(thisPayoutAmount > 0, "colony-reward-payout-no-rewards"); - pendingRewardPayments[_token] = add(pendingRewardPayments[_token], thisPayoutAmount); - - uint256 totalTokens = sub(ERC20Extended(token).totalSupply(), ERC20Extended(token).balanceOf(address(this))); - require(totalTokens > 0, "colony-reward-payout-invalid-total-tokens"); - - bytes32 rootHash = IColonyNetwork(colonyNetworkAddress).getReputationRootHash(); - uint256 colonyWideReputation = checkReputation( - rootHash, - domains[1].skillId, - address(0x0), - key, - value, - branchMask, - siblings - ); - require(colonyWideReputation > 0, "colony-reward-payout-invalid-colony-wide-reputation"); - - rewardPayoutCycles[totalLockCount] = RewardPayoutCycle( - rootHash, - colonyWideReputation, - totalTokens, - thisPayoutAmount, - _token, - block.timestamp, - thisPayoutAmount, - false - ); - - emit RewardPayoutCycleStarted(msgSender(), totalLockCount); - } - - // slither-disable-next-line reentrancy-no-eth - function claimRewardPayout( - uint256 _payoutId, - uint256[7] memory _squareRoots, - bytes memory key, - bytes memory value, - uint256 branchMask, - bytes32[] memory siblings - ) public stoppable - { - uint256 userReputation = checkReputation( - rewardPayoutCycles[_payoutId].reputationState, - domains[1].skillId, - msgSender(), - key, - value, - branchMask, - siblings - ); - - address tokenAddress; - uint256 reward; - (tokenAddress, reward) = calculateRewardForUser(_payoutId, _squareRoots, userReputation); - - ITokenLocking(tokenLockingAddress).unlockTokenForUser(token, msgSender(), _payoutId); - - uint fee = calculateNetworkFeeForPayout(reward); - uint remainder = sub(reward, fee); - - fundingPots[0].balance[tokenAddress] = sub(fundingPots[0].balance[tokenAddress], reward); - pendingRewardPayments[rewardPayoutCycles[_payoutId].tokenAddress] = sub( - pendingRewardPayments[rewardPayoutCycles[_payoutId].tokenAddress], - reward - ); - rewardPayoutCycles[_payoutId].amountRemaining = sub(rewardPayoutCycles[_payoutId].amountRemaining, reward); - - assert(ERC20Extended(tokenAddress).transfer(msgSender(), remainder)); - assert(ERC20Extended(tokenAddress).transfer(colonyNetworkAddress, fee)); - - emit RewardPayoutClaimed(_payoutId, msgSender(), fee, remainder); - } - - function finalizeRewardPayout(uint256 _payoutId) public stoppable { - RewardPayoutCycle memory payout = rewardPayoutCycles[_payoutId]; - require(payout.reputationState != 0x00, "colony-reward-payout-does-not-exist"); - require(!payout.finalized, "colony-reward-payout-already-finalized"); - require(block.timestamp - payout.blockTimestamp > 60 days, "colony-reward-payout-active"); - - rewardPayoutCycles[_payoutId].finalized = true; - pendingRewardPayments[payout.tokenAddress] = sub(pendingRewardPayments[payout.tokenAddress], payout.amountRemaining); - - emit RewardPayoutCycleEnded(msgSender(), _payoutId); - } - - function getRewardPayoutInfo(uint256 _payoutId) public view returns (RewardPayoutCycle memory rewardPayoutCycle) { - rewardPayoutCycle = rewardPayoutCycles[_payoutId]; - } - - function setRewardInverse(uint256 _rewardInverse) public - stoppable - auth - { - require(_rewardInverse > 0, "colony-reward-inverse-cannot-be-zero"); - rewardInverse = _rewardInverse; - - emit ColonyRewardInverseSet(msgSender(), _rewardInverse); } function getRewardInverse() public view returns (uint256) { return rewardInverse; } - function checkReputation( - bytes32 rootHash, - uint256 skillId, - address userAddress, - bytes memory key, - bytes memory value, - uint256 branchMask, - bytes32[] memory siblings - ) internal view returns (uint256) - { - bytes32 impliedRoot = getImpliedRootHashKey(key, value, branchMask, siblings); - require(rootHash == impliedRoot, "colony-reputation-invalid-root-hash"); - - uint256 reputationValue; - address keyColonyAddress; - uint256 keySkill; - address keyUserAddress; - - assembly { - reputationValue := mload(add(value, 32)) - keyColonyAddress := mload(add(key, 20)) - keySkill := mload(add(key, 52)) - keyUserAddress := mload(add(key, 72)) - } - - require(keyColonyAddress == address(this), "colony-reputation-invalid-colony-address"); - require(keySkill == skillId, "colony-reputation-invalid-skill-id"); - require(keyUserAddress == userAddress, "colony-reputation-invalid-user-address"); - - return reputationValue; - } - - function calculateRewardForUser(uint256 payoutId, uint256[7] memory squareRoots, uint256 userReputation) internal returns (address, uint256) { - RewardPayoutCycle memory payout = rewardPayoutCycles[payoutId]; - - // Checking if payout is active - require(block.timestamp - payout.blockTimestamp <= 60 days, "colony-reward-payout-not-active"); - - uint256 userTokens = ITokenLocking(tokenLockingAddress).getUserLock(token, msgSender()).balance; - require(userTokens > 0, "colony-reward-payout-invalid-user-tokens"); - require(userReputation > 0, "colony-reward-payout-invalid-user-reputation"); - - // squareRoots[0] - square root of userReputation - // squareRoots[1] - square root of userTokens (deposited in TokenLocking) - // squareRoots[2] - square root of payout.colonyWideReputation - // squareRoots[3] - square root of totalTokens - // squareRoots[4] - square root of numerator - // squareRoots[5] - square root of denominator - // squareRoots[6] - square root of payout.amount - - require(mul(squareRoots[0], squareRoots[0]) <= userReputation, "colony-reward-payout-invalid-parameter-user-reputation"); - require(mul(squareRoots[1], squareRoots[1]) <= userTokens, "colony-reward-payout-invalid-parameter-user-token"); - require(mul(squareRoots[2], squareRoots[2]) >= payout.colonyWideReputation, "colony-reward-payout-invalid-parameter-total-reputation"); - require(mul(squareRoots[3], squareRoots[3]) >= payout.totalTokens, "colony-reward-payout-invalid-parameter-total-tokens"); - require(mul(squareRoots[6], squareRoots[6]) <= payout.amount, "colony-reward-payout-invalid-parameter-amount"); - uint256 numerator = mul(squareRoots[0], squareRoots[1]); - uint256 denominator = mul(squareRoots[2], squareRoots[3]); - - require(mul(squareRoots[4], squareRoots[4]) <= numerator, "colony-reward-payout-invalid-parameter-numerator"); - require(mul(squareRoots[5], squareRoots[5]) >= denominator, "colony-reward-payout-invalid-parameter-denominator"); - - uint256 reward = (mul(squareRoots[4], squareRoots[6]) / squareRoots[5]) ** 2; - - return (payout.tokenAddress, reward); - } + // Internal function moveFundsBetweenPotsFunctionality( uint256 _fromPot, @@ -458,7 +291,8 @@ contract ColonyFunding is ColonyStorage, PatriciaTreeProofs { // ignore-swc-123 uint256 _amount, address _token ) - internal { + internal + { FundingPot storage fromPot = fundingPots[_fromPot]; FundingPot storage toPot = fundingPots[_toPot]; @@ -514,8 +348,6 @@ contract ColonyFunding is ColonyStorage, PatriciaTreeProofs { // ignore-swc-123 emit ColonyFundsMovedBetweenFundingPots(msgSender(), _fromPot, _toPot, _amount, _token); } - - function updatePayoutsWeCannotMakeAfterPotChange(uint256 _fundingPotId, address _token, uint _prev) internal { FundingPot storage tokenPot = fundingPots[_fundingPotId]; @@ -544,31 +376,8 @@ contract ColonyFunding is ColonyStorage, PatriciaTreeProofs { // ignore-swc-123 } } - function getDomainFromFundingPot(uint256 _fundingPotId) public view returns (uint256 domainId) { - require(_fundingPotId <= fundingPotCount, "colony-funding-nonexistent-pot"); - FundingPot storage fundingPot = fundingPots[_fundingPotId]; - - if (fundingPot.associatedType == FundingPotAssociatedType.Domain) { - domainId = fundingPot.associatedTypeId; - } else if (fundingPot.associatedType == FundingPotAssociatedType.Task) { - domainId = tasks[fundingPot.associatedTypeId].domainId; - } else if (fundingPot.associatedType == FundingPotAssociatedType.Payment) { - domainId = payments[fundingPot.associatedTypeId].domainId; - } else if (fundingPot.associatedType == FundingPotAssociatedType.Expenditure) { - domainId = expenditures[fundingPot.associatedTypeId].domainId; - } else { - // If rewards pot, return root domain. - assert(_fundingPotId == 0); - domainId = 1; - } - } - - function setExpenditurePayouts(uint256 _id, uint256[] memory _slots, address _token, uint256[] memory _amounts) - public - stoppable - expenditureExists(_id) - expenditureDraft(_id) - expenditureOnlyOwner(_id) + function setExpenditurePayoutsInternal(uint256 _id, uint256[] memory _slots, address _token, uint256[] memory _amounts) + internal { require(_slots.length == _amounts.length, "colony-expenditure-bad-slots"); @@ -592,17 +401,6 @@ contract ColonyFunding is ColonyStorage, PatriciaTreeProofs { // ignore-swc-123 updatePayoutsWeCannotMakeAfterBudgetChange(expenditures[_id].fundingPotId, _token, currentTotal); } - function setExpenditurePayout(uint256 _id, uint256 _slot, address _token, uint256 _amount) - public - stoppable - { - uint256[] memory slots = new uint256[](1); - slots[0] = _slot; - uint256[] memory amounts = new uint256[](1); - amounts[0] = _amount; - setExpenditurePayouts(_id, slots, _token, amounts); - } - function setTaskPayout(uint256 _id, TaskRole _role, address _token, uint256 _amount) private taskExists(_id) taskNotComplete(_id) @@ -622,15 +420,15 @@ contract ColonyFunding is ColonyStorage, PatriciaTreeProofs { // ignore-swc-123 } function processPayout(uint256 _fundingPotId, address _token, uint256 _payout, address payable _user) private { + IColonyNetwork colonyNetworkContract = IColonyNetwork(colonyNetworkAddress); + address payable metaColonyAddress = colonyNetworkContract.getMetaColony(); + fundingPots[_fundingPotId].balance[_token] = sub(fundingPots[_fundingPotId].balance[_token], _payout); + fundingPots[_fundingPotId].payouts[_token] = sub(fundingPots[_fundingPotId].payouts[_token], _payout); nonRewardPotsTotal[_token] = sub(nonRewardPotsTotal[_token], _payout); uint fee = isOwnExtension(_user) ? 0 : calculateNetworkFeeForPayout(_payout); uint remainder = sub(_payout, fee); - fundingPots[_fundingPotId].payouts[_token] = sub(fundingPots[_fundingPotId].payouts[_token], _payout); - - IColonyNetwork colonyNetworkContract = IColonyNetwork(colonyNetworkAddress); - address payable metaColonyAddress = colonyNetworkContract.getMetaColony(); if (_token == address(0x0)) { // Payout ether @@ -652,17 +450,4 @@ contract ColonyFunding is ColonyStorage, PatriciaTreeProofs { // ignore-swc-123 emit PayoutClaimed(msgSender(), _fundingPotId, _token, remainder); } - - function calculateNetworkFeeForPayout(uint256 _payout) private view returns (uint256 fee) { - IColonyNetwork colonyNetworkContract = IColonyNetwork(colonyNetworkAddress); - uint256 feeInverse = colonyNetworkContract.getFeeInverse(); - - // slither-disable-next-line incorrect-equality - if (_payout == 0 || feeInverse == 1) { - fee = _payout; - } else { - fee = _payout/feeInverse + 1; - } - } - -} +} \ No newline at end of file diff --git a/contracts/colony/ColonyRewards.sol b/contracts/colony/ColonyRewards.sol new file mode 100644 index 0000000000..37b738ec1b --- /dev/null +++ b/contracts/colony/ColonyRewards.sol @@ -0,0 +1,208 @@ +/* + 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.7.3; +pragma experimental "ABIEncoderV2"; + +import "./../tokenLocking/ITokenLocking.sol"; +import "./ColonyStorage.sol"; + + +contract ColonyRewards is ColonyStorage, PatriciaTreeProofs { // ignore-swc-123 + function lockToken() public stoppable onlyOwnExtension returns (uint256) { + uint256 lockId = ITokenLocking(tokenLockingAddress).lockToken(token); + tokenLocks[msgSender()][lockId] = true; + return lockId; + } + + function unlockTokenForUser(address _user, uint256 _lockId) public stoppable onlyOwnExtension { + require(tokenLocks[msgSender()][_lockId], "colony-bad-lock-id"); + ITokenLocking(tokenLockingAddress).unlockTokenForUser(token, _user, _lockId); + } + + function startNextRewardPayout(address _token, bytes memory key, bytes memory value, uint256 branchMask, bytes32[] memory siblings) + public stoppable auth + { + ITokenLocking tokenLocking = ITokenLocking(tokenLockingAddress); + uint256 totalLockCount = tokenLocking.lockToken(token); + uint256 thisPayoutAmount = sub(fundingPots[0].balance[_token], pendingRewardPayments[_token]); + require(thisPayoutAmount > 0, "colony-reward-payout-no-rewards"); + pendingRewardPayments[_token] = add(pendingRewardPayments[_token], thisPayoutAmount); + + uint256 totalTokens = sub(ERC20Extended(token).totalSupply(), ERC20Extended(token).balanceOf(address(this))); + require(totalTokens > 0, "colony-reward-payout-invalid-total-tokens"); + + bytes32 rootHash = IColonyNetwork(colonyNetworkAddress).getReputationRootHash(); + uint256 colonyWideReputation = checkReputation( + rootHash, + domains[1].skillId, + address(0x0), + key, + value, + branchMask, + siblings + ); + require(colonyWideReputation > 0, "colony-reward-payout-invalid-colony-wide-reputation"); + + rewardPayoutCycles[totalLockCount] = RewardPayoutCycle( + rootHash, + colonyWideReputation, + totalTokens, + thisPayoutAmount, + _token, + block.timestamp, + thisPayoutAmount, + false + ); + + emit RewardPayoutCycleStarted(msgSender(), totalLockCount); + } + + // slither-disable-next-line reentrancy-no-eth + function claimRewardPayout( + uint256 _payoutId, + uint256[7] memory _squareRoots, + bytes memory key, + bytes memory value, + uint256 branchMask, + bytes32[] memory siblings + ) public stoppable + { + uint256 userReputation = checkReputation( + rewardPayoutCycles[_payoutId].reputationState, + domains[1].skillId, + msgSender(), + key, + value, + branchMask, + siblings + ); + + address tokenAddress; + uint256 reward; + (tokenAddress, reward) = calculateRewardForUser(_payoutId, _squareRoots, userReputation); + + ITokenLocking(tokenLockingAddress).unlockTokenForUser(token, msgSender(), _payoutId); + + uint fee = calculateNetworkFeeForPayout(reward); + uint remainder = sub(reward, fee); + + fundingPots[0].balance[tokenAddress] = sub(fundingPots[0].balance[tokenAddress], reward); + pendingRewardPayments[rewardPayoutCycles[_payoutId].tokenAddress] = sub( + pendingRewardPayments[rewardPayoutCycles[_payoutId].tokenAddress], + reward + ); + rewardPayoutCycles[_payoutId].amountRemaining = sub(rewardPayoutCycles[_payoutId].amountRemaining, reward); + + assert(ERC20Extended(tokenAddress).transfer(msgSender(), remainder)); + assert(ERC20Extended(tokenAddress).transfer(colonyNetworkAddress, fee)); + + emit RewardPayoutClaimed(_payoutId, msgSender(), fee, remainder); + } + + function finalizeRewardPayout(uint256 _payoutId) public stoppable { + RewardPayoutCycle memory payout = rewardPayoutCycles[_payoutId]; + require(payout.reputationState != 0x00, "colony-reward-payout-does-not-exist"); + require(!payout.finalized, "colony-reward-payout-already-finalized"); + require(block.timestamp - payout.blockTimestamp > 60 days, "colony-reward-payout-active"); + + rewardPayoutCycles[_payoutId].finalized = true; + pendingRewardPayments[payout.tokenAddress] = sub(pendingRewardPayments[payout.tokenAddress], payout.amountRemaining); + + emit RewardPayoutCycleEnded(msgSender(), _payoutId); + } + + function getRewardPayoutInfo(uint256 _payoutId) public view returns (RewardPayoutCycle memory rewardPayoutCycle) { + rewardPayoutCycle = rewardPayoutCycles[_payoutId]; + } + + function setRewardInverse(uint256 _rewardInverse) public + stoppable + auth + { + require(_rewardInverse > 0, "colony-reward-inverse-cannot-be-zero"); + rewardInverse = _rewardInverse; + + emit ColonyRewardInverseSet(msgSender(), _rewardInverse); + } + + function checkReputation( + bytes32 rootHash, + uint256 skillId, + address userAddress, + bytes memory key, + bytes memory value, + uint256 branchMask, + bytes32[] memory siblings + ) internal view returns (uint256) + { + bytes32 impliedRoot = getImpliedRootHashKey(key, value, branchMask, siblings); + require(rootHash == impliedRoot, "colony-reputation-invalid-root-hash"); + + uint256 reputationValue; + address keyColonyAddress; + uint256 keySkill; + address keyUserAddress; + + assembly { + reputationValue := mload(add(value, 32)) + keyColonyAddress := mload(add(key, 20)) + keySkill := mload(add(key, 52)) + keyUserAddress := mload(add(key, 72)) + } + + require(keyColonyAddress == address(this), "colony-reputation-invalid-colony-address"); + require(keySkill == skillId, "colony-reputation-invalid-skill-id"); + require(keyUserAddress == userAddress, "colony-reputation-invalid-user-address"); + + return reputationValue; + } + + function calculateRewardForUser(uint256 payoutId, uint256[7] memory squareRoots, uint256 userReputation) internal returns (address, uint256) { + RewardPayoutCycle memory payout = rewardPayoutCycles[payoutId]; + + // Checking if payout is active + require(block.timestamp - payout.blockTimestamp <= 60 days, "colony-reward-payout-not-active"); + + uint256 userTokens = ITokenLocking(tokenLockingAddress).getUserLock(token, msgSender()).balance; + require(userTokens > 0, "colony-reward-payout-invalid-user-tokens"); + require(userReputation > 0, "colony-reward-payout-invalid-user-reputation"); + + // squareRoots[0] - square root of userReputation + // squareRoots[1] - square root of userTokens (deposited in TokenLocking) + // squareRoots[2] - square root of payout.colonyWideReputation + // squareRoots[3] - square root of totalTokens + // squareRoots[4] - square root of numerator + // squareRoots[5] - square root of denominator + // squareRoots[6] - square root of payout.amount + + require(mul(squareRoots[0], squareRoots[0]) <= userReputation, "colony-reward-payout-invalid-parameter-user-reputation"); + require(mul(squareRoots[1], squareRoots[1]) <= userTokens, "colony-reward-payout-invalid-parameter-user-token"); + require(mul(squareRoots[2], squareRoots[2]) >= payout.colonyWideReputation, "colony-reward-payout-invalid-parameter-total-reputation"); + require(mul(squareRoots[3], squareRoots[3]) >= payout.totalTokens, "colony-reward-payout-invalid-parameter-total-tokens"); + require(mul(squareRoots[6], squareRoots[6]) <= payout.amount, "colony-reward-payout-invalid-parameter-amount"); + uint256 numerator = mul(squareRoots[0], squareRoots[1]); + uint256 denominator = mul(squareRoots[2], squareRoots[3]); + + require(mul(squareRoots[4], squareRoots[4]) <= numerator, "colony-reward-payout-invalid-parameter-numerator"); + require(mul(squareRoots[5], squareRoots[5]) >= denominator, "colony-reward-payout-invalid-parameter-denominator"); + + uint256 reward = (mul(squareRoots[4], squareRoots[6]) / squareRoots[5]) ** 2; + + return (payout.tokenAddress, reward); + } +} \ No newline at end of file diff --git a/contracts/colony/ColonyStorage.sol b/contracts/colony/ColonyStorage.sol index 268c1e4020..266aaf9a37 100755 --- a/contracts/colony/ColonyStorage.sol +++ b/contracts/colony/ColonyStorage.sol @@ -330,6 +330,17 @@ contract ColonyStorage is ColonyDataTypes, ColonyNetworkDataTypes, DSMath, Commo return domainId > 0 && domainId <= domainCount; } + function calculateNetworkFeeForPayout(uint256 _payout) internal view returns (uint256 fee) { + uint256 feeInverse = IColonyNetwork(colonyNetworkAddress).getFeeInverse(); + + // slither-disable-next-line incorrect-equality + if (_payout == 0 || feeInverse == 1) { + fee = _payout; + } else { + fee = _payout / feeInverse + 1; + } + } + function executeCall(address to, uint256 value, bytes memory data) internal returns (bool success) { assembly { // call contract at address a with input mem[in…(in+insize)) diff --git a/contracts/colony/ColonyTask.sol b/contracts/colony/ColonyTask.sol index f8c2748744..50c3682dfd 100755 --- a/contracts/colony/ColonyTask.sol +++ b/contracts/colony/ColonyTask.sol @@ -19,6 +19,7 @@ pragma solidity 0.7.3; pragma experimental "ABIEncoderV2"; import "./ColonyStorage.sol"; +import "./IColony.sol"; contract ColonyTask is ColonyStorage { @@ -371,6 +372,37 @@ contract ColonyTask is ColonyStorage { emit TaskDueDateSet(_id, _dueDate); } + function setAllTaskPayouts( + uint256 _id, + address _token, + uint256 _managerAmount, + uint256 _evaluatorAmount, + uint256 _workerAmount + ) + public + stoppable + confirmTaskRoleIdentity(_id, TaskRole.Manager) + { + Task storage task = tasks[_id]; + address manager = task.roles[uint8(TaskRole.Manager)].user; + address evaluator = task.roles[uint8(TaskRole.Evaluator)].user; + address worker = task.roles[uint8(TaskRole.Worker)].user; + + require( + evaluator == manager || + evaluator == address(0x0), + "colony-funding-evaluator-already-set"); + + require( + worker == manager || + worker == address(0x0), + "colony-funding-worker-already-set"); + + IColony(address(this)).setTaskManagerPayout(_id, _token, _managerAmount); + IColony(address(this)).setTaskEvaluatorPayout(_id, _token, _evaluatorAmount); + IColony(address(this)).setTaskWorkerPayout(_id, _token, _workerAmount); + } + function submitTaskDeliverable(uint256 _id, bytes32 _deliverableHash) public stoppable taskExists(_id) @@ -610,4 +642,4 @@ contract ColonyTask is ColonyStorage { emit TaskRoleUserSet(_id, _role, _user); } -} +} \ No newline at end of file diff --git a/helpers/upgradable-contracts.js b/helpers/upgradable-contracts.js index d3f6dc10e8..d568081e24 100644 --- a/helpers/upgradable-contracts.js +++ b/helpers/upgradable-contracts.js @@ -74,10 +74,11 @@ exports.setupColonyVersionResolver = async function setupColonyVersionResolver( colony, colonyDomains, colonyExpenditure, - colonyTask, - colonyPayment, colonyFunding, + colonyPayment, + colonyRewards, colonyRoles, + colonyTask, contractRecovery, colonyArbitraryTransaction, resolver @@ -86,10 +87,11 @@ exports.setupColonyVersionResolver = async function setupColonyVersionResolver( deployedImplementations.Colony = colony.address; deployedImplementations.ColonyDomains = colonyDomains.address; deployedImplementations.ColonyExpenditure = colonyExpenditure.address; - deployedImplementations.ColonyTask = colonyTask.address; - deployedImplementations.ColonyRoles = colonyRoles.address; - deployedImplementations.ColonyPayment = colonyPayment.address; deployedImplementations.ColonyFunding = colonyFunding.address; + deployedImplementations.ColonyPayment = colonyPayment.address; + deployedImplementations.ColonyRewards = colonyRewards.address; + deployedImplementations.ColonyRoles = colonyRoles.address; + deployedImplementations.ColonyTask = colonyTask.address; deployedImplementations.ContractRecovery = contractRecovery.address; deployedImplementations.ColonyArbitraryTransaction = colonyArbitraryTransaction.address; diff --git a/migrations/4_setup_colony_version_resolver.js b/migrations/4_setup_colony_version_resolver.js index 60eeebb511..c59913978c 100644 --- a/migrations/4_setup_colony_version_resolver.js +++ b/migrations/4_setup_colony_version_resolver.js @@ -4,8 +4,9 @@ const { setupColonyVersionResolver } = require("../helpers/upgradable-contracts" const Colony = artifacts.require("./Colony"); const ColonyDomains = artifacts.require("./ColonyDomains"); -const ColonyFunding = artifacts.require("./ColonyFunding"); const ColonyExpenditure = artifacts.require("./ColonyExpenditure"); +const ColonyFunding = artifacts.require("./ColonyFunding"); +const ColonyRewards = artifacts.require("./ColonyRewards"); const ColonyRoles = artifacts.require("./ColonyRoles"); const ColonyTask = artifacts.require("./ColonyTask"); const ColonyPayment = artifacts.require("./ColonyPayment"); @@ -20,11 +21,12 @@ module.exports = async function (deployer) { // Create a new Colony (version) and setup a new Resolver for it const colony = await Colony.new(); const colonyDomains = await ColonyDomains.new(); - const colonyFunding = await ColonyFunding.new(); const colonyExpenditure = await ColonyExpenditure.new(); + const colonyFunding = await ColonyFunding.new(); + const colonyPayment = await ColonyPayment.new(); + const colonyRewards = await ColonyRewards.new(); const colonyRoles = await ColonyRoles.new(); const colonyTask = await ColonyTask.new(); - const colonyPayment = await ColonyPayment.new(); const colonyArbitraryTransaction = await ColonyArbitraryTransaction.new(); const contractRecovery = await ContractRecovery.deployed(); const version = await colony.version(); @@ -38,10 +40,11 @@ module.exports = async function (deployer) { colony, colonyDomains, colonyExpenditure, - colonyTask, - colonyPayment, colonyFunding, + colonyPayment, + colonyRewards, colonyRoles, + colonyTask, contractRecovery, colonyArbitraryTransaction, resolver diff --git a/migrations/8_setup_meta_colony.js b/migrations/8_setup_meta_colony.js index 8dc71fdb20..af2c77bc27 100644 --- a/migrations/8_setup_meta_colony.js +++ b/migrations/8_setup_meta_colony.js @@ -57,21 +57,23 @@ module.exports = async function (deployer, network, accounts) { // Set up functional resolvers that identify correctly as previous versions. const Colony = artifacts.require("./Colony"); const ColonyDomains = artifacts.require("./ColonyDomains"); - const ColonyFunding = artifacts.require("./ColonyFunding"); const ColonyExpenditure = artifacts.require("./ColonyExpenditure"); + const ColonyFunding = artifacts.require("./ColonyFunding"); + const ColonyPayment = artifacts.require("./ColonyPayment"); + const ColonyRewards = artifacts.require("./ColonyRewards"); const ColonyRoles = artifacts.require("./ColonyRoles"); const ColonyTask = artifacts.require("./ColonyTask"); - const ColonyPayment = artifacts.require("./ColonyPayment"); const ContractRecovery = artifacts.require("./ContractRecovery"); const ColonyArbitraryTransaction = artifacts.require("./ColonyArbitraryTransaction"); const colony = await Colony.new(); const colonyDomains = await ColonyDomains.new(); - const colonyFunding = await ColonyFunding.new(); const colonyExpenditure = await ColonyExpenditure.new(); + const colonyFunding = await ColonyFunding.new(); + const colonyPayment = await ColonyPayment.new(); + const colonyRewards = await ColonyRewards.new(); const colonyRoles = await ColonyRoles.new(); const colonyTask = await ColonyTask.new(); - const colonyPayment = await ColonyPayment.new(); const contractRecovery = await ContractRecovery.deployed(); const colonyArbitraryTransaction = await ColonyArbitraryTransaction.new(); @@ -80,10 +82,11 @@ module.exports = async function (deployer, network, accounts) { colony, colonyDomains, colonyExpenditure, - colonyTask, - colonyPayment, colonyFunding, + colonyPayment, + colonyRewards, colonyRoles, + colonyTask, contractRecovery, colonyArbitraryTransaction, resolver3 @@ -97,10 +100,11 @@ module.exports = async function (deployer, network, accounts) { colony, colonyDomains, colonyExpenditure, - colonyTask, - colonyPayment, colonyFunding, + colonyPayment, + colonyRewards, colonyRoles, + colonyTask, contractRecovery, colonyArbitraryTransaction, resolver4 diff --git a/test-smoke/colony-storage-consistent.js b/test-smoke/colony-storage-consistent.js index 27530a21d1..eb748bf6f9 100644 --- a/test-smoke/colony-storage-consistent.js +++ b/test-smoke/colony-storage-consistent.js @@ -152,11 +152,11 @@ contract("Contract Storage", (accounts) => { console.log("miningCycleStateHash:", miningCycleAccount.stateRoot.toString("hex")); console.log("tokenLockingStateHash:", tokenLockingAccount.stateRoot.toString("hex")); - expect(colonyNetworkAccount.stateRoot.toString("hex")).to.equal("d83bafbde471d922f744f0045e6b50936c62edb219d95977a16c509560c0858f"); - expect(colonyAccount.stateRoot.toString("hex")).to.equal("38408842ba839e25a312bee736ae2f20950add8abdb3c1e72c057e53af6b0a89"); - expect(metaColonyAccount.stateRoot.toString("hex")).to.equal("0329b7be521126a90466fd821d1e1fd9d21b0ec9237b207bd8f14ae891d98d5f"); - expect(miningCycleAccount.stateRoot.toString("hex")).to.equal("9ee0d346e2a597ee9083711e9c7f6f414bff6b9f0ae7f88f7db534a2280964c0"); - expect(tokenLockingAccount.stateRoot.toString("hex")).to.equal("9484aed2f25183c3d88967bb90ef988c5d83a2813aa7cf931a89a741f4a845c0"); + expect(colonyNetworkAccount.stateRoot.toString("hex")).to.equal("5dcdd30b5bbe5ad24c63280ead8f1307bc7b0816a4acad1f60b96b3feed2ea16"); + expect(colonyAccount.stateRoot.toString("hex")).to.equal("e33a1c0ec683d21d76ad195541cdabda65a5bbee4aaa4e3ad6d9a64554388bcc"); + expect(metaColonyAccount.stateRoot.toString("hex")).to.equal("58f1833f0b94c47c028c91ededb70d6697624ecf98bc2cc7930bf55f40d2d931"); + expect(miningCycleAccount.stateRoot.toString("hex")).to.equal("1f3909ac9098d953ec1d197e6d7924384e96209770f445466ea2f0c0c39f4834"); + expect(tokenLockingAccount.stateRoot.toString("hex")).to.equal("7ec700a44aef86af735adcb205136940a73bd0507d07d88e93e629dee06f05c3"); }); }); }); diff --git a/test-upgrade/colony-upgrade.js b/test-upgrade/colony-upgrade.js index 490873555e..493b03573a 100644 --- a/test-upgrade/colony-upgrade.js +++ b/test-upgrade/colony-upgrade.js @@ -10,10 +10,11 @@ const EtherRouter = artifacts.require("EtherRouter"); const Resolver = artifacts.require("Resolver"); const ColonyDomains = artifacts.require("ColonyDomains"); const ColonyExpenditure = artifacts.require("ColonyExpenditure"); -const ColonyTask = artifacts.require("ColonyTask"); -const ColonyPayment = artifacts.require("ColonyPayment"); const ColonyFunding = artifacts.require("ColonyFunding"); +const ColonyPayment = artifacts.require("ColonyPayment"); +const ColonyRewards = artifacts.require("ColonyRewards"); const ColonyRoles = artifacts.require("ColonyRoles"); +const ColonyTask = artifacts.require("ColonyTask"); const ContractRecovery = artifacts.require("ContractRecovery"); const ColonyArbitraryTransaction = artifacts.require("ColonyArbitraryTransaction"); const UpdatedColony = artifacts.require("UpdatedColony"); @@ -42,10 +43,11 @@ contract("Colony contract upgrade", (accounts) => { const colonyDomains = await ColonyDomains.new(); const colonyExpenditure = await ColonyExpenditure.new(); - const colonyTask = await ColonyTask.new(); - const colonyPayment = await ColonyPayment.new(); const colonyFunding = await ColonyFunding.new(); + const colonyPayment = await ColonyPayment.new(); const colonyRoles = await ColonyRoles.new(); + const colonyRewards = await ColonyRewards.new(); + const colonyTask = await ColonyTask.new(); const contractRecovery = await ContractRecovery.new(); const colonyArbitraryTransaction = await ColonyArbitraryTransaction.new(); @@ -61,10 +63,11 @@ contract("Colony contract upgrade", (accounts) => { updatedColonyContract, colonyDomains, colonyExpenditure, - colonyTask, - colonyPayment, colonyFunding, + colonyPayment, + colonyRewards, colonyRoles, + colonyTask, contractRecovery, colonyArbitraryTransaction, resolver