diff --git a/.storage-layouts-normalized/contracts/colony/Colony.sol:Colony.json b/.storage-layouts-normalized/contracts/colony/Colony.sol:Colony.json index b22d40a278..bdaaa40296 100644 --- a/.storage-layouts-normalized/contracts/colony/Colony.sol:Colony.json +++ b/.storage-layouts-normalized/contracts/colony/Colony.sol:Colony.json @@ -1409,6 +1409,27 @@ "numberOfBytes": "32" } } + }, + { + "contract": "contracts/colony/Colony.sol:Colony", + "label": "domainReputationApproval", + "offset": 0, + "slot": "39", + "type": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "label": "mapping(uint256 => uint256)", + "numberOfBytes": "32", + "value": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + } + } } ] } \ No newline at end of file diff --git a/.storage-layouts-normalized/contracts/colony/ColonyArbitraryTransaction.sol:ColonyArbitraryTransaction.json b/.storage-layouts-normalized/contracts/colony/ColonyArbitraryTransaction.sol:ColonyArbitraryTransaction.json index 4ff8a22299..44da3bf9d0 100644 --- a/.storage-layouts-normalized/contracts/colony/ColonyArbitraryTransaction.sol:ColonyArbitraryTransaction.json +++ b/.storage-layouts-normalized/contracts/colony/ColonyArbitraryTransaction.sol:ColonyArbitraryTransaction.json @@ -1409,6 +1409,27 @@ "numberOfBytes": "32" } } + }, + { + "contract": "contracts/colony/ColonyArbitraryTransaction.sol:ColonyArbitraryTransaction", + "label": "domainReputationApproval", + "offset": 0, + "slot": "39", + "type": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "label": "mapping(uint256 => uint256)", + "numberOfBytes": "32", + "value": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + } + } } ] } \ No newline at end of file diff --git a/.storage-layouts-normalized/contracts/colony/ColonyDomains.sol:ColonyDomains.json b/.storage-layouts-normalized/contracts/colony/ColonyDomains.sol:ColonyDomains.json index d3bf27e9dc..b35aaabbd4 100644 --- a/.storage-layouts-normalized/contracts/colony/ColonyDomains.sol:ColonyDomains.json +++ b/.storage-layouts-normalized/contracts/colony/ColonyDomains.sol:ColonyDomains.json @@ -1409,6 +1409,27 @@ "numberOfBytes": "32" } } + }, + { + "contract": "contracts/colony/ColonyDomains.sol:ColonyDomains", + "label": "domainReputationApproval", + "offset": 0, + "slot": "39", + "type": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "label": "mapping(uint256 => uint256)", + "numberOfBytes": "32", + "value": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + } + } } ] } \ No newline at end of file diff --git a/.storage-layouts-normalized/contracts/colony/ColonyExpenditure.sol:ColonyExpenditure.json b/.storage-layouts-normalized/contracts/colony/ColonyExpenditure.sol:ColonyExpenditure.json index 8dee3d7dd0..d24e1daa9f 100644 --- a/.storage-layouts-normalized/contracts/colony/ColonyExpenditure.sol:ColonyExpenditure.json +++ b/.storage-layouts-normalized/contracts/colony/ColonyExpenditure.sol:ColonyExpenditure.json @@ -1409,6 +1409,27 @@ "numberOfBytes": "32" } } + }, + { + "contract": "contracts/colony/ColonyExpenditure.sol:ColonyExpenditure", + "label": "domainReputationApproval", + "offset": 0, + "slot": "39", + "type": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "label": "mapping(uint256 => uint256)", + "numberOfBytes": "32", + "value": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + } + } } ] } \ No newline at end of file diff --git a/.storage-layouts-normalized/contracts/colony/ColonyFunding.sol:ColonyFunding.json b/.storage-layouts-normalized/contracts/colony/ColonyFunding.sol:ColonyFunding.json index 2ba1c31e74..3fa29005de 100644 --- a/.storage-layouts-normalized/contracts/colony/ColonyFunding.sol:ColonyFunding.json +++ b/.storage-layouts-normalized/contracts/colony/ColonyFunding.sol:ColonyFunding.json @@ -1409,6 +1409,27 @@ "numberOfBytes": "32" } } + }, + { + "contract": "contracts/colony/ColonyFunding.sol:ColonyFunding", + "label": "domainReputationApproval", + "offset": 0, + "slot": "39", + "type": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "label": "mapping(uint256 => uint256)", + "numberOfBytes": "32", + "value": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + } + } } ] } \ No newline at end of file diff --git a/.storage-layouts-normalized/contracts/colony/ColonyRewards.sol:ColonyRewards.json b/.storage-layouts-normalized/contracts/colony/ColonyRewards.sol:ColonyRewards.json index bc29e4ae55..82d779f988 100644 --- a/.storage-layouts-normalized/contracts/colony/ColonyRewards.sol:ColonyRewards.json +++ b/.storage-layouts-normalized/contracts/colony/ColonyRewards.sol:ColonyRewards.json @@ -1409,6 +1409,27 @@ "numberOfBytes": "32" } } + }, + { + "contract": "contracts/colony/ColonyRewards.sol:ColonyRewards", + "label": "domainReputationApproval", + "offset": 0, + "slot": "39", + "type": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "label": "mapping(uint256 => uint256)", + "numberOfBytes": "32", + "value": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + } + } } ] } \ No newline at end of file diff --git a/.storage-layouts-normalized/contracts/colony/ColonyRoles.sol:ColonyRoles.json b/.storage-layouts-normalized/contracts/colony/ColonyRoles.sol:ColonyRoles.json index 1704317f0d..cffe687525 100644 --- a/.storage-layouts-normalized/contracts/colony/ColonyRoles.sol:ColonyRoles.json +++ b/.storage-layouts-normalized/contracts/colony/ColonyRoles.sol:ColonyRoles.json @@ -1409,6 +1409,27 @@ "numberOfBytes": "32" } } + }, + { + "contract": "contracts/colony/ColonyRoles.sol:ColonyRoles", + "label": "domainReputationApproval", + "offset": 0, + "slot": "39", + "type": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "label": "mapping(uint256 => uint256)", + "numberOfBytes": "32", + "value": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + } + } } ] } \ No newline at end of file diff --git a/.storage-layouts-normalized/contracts/colony/ColonyStorage.sol:ColonyStorage.json b/.storage-layouts-normalized/contracts/colony/ColonyStorage.sol:ColonyStorage.json index 0e353b88a3..3c77c7ae35 100644 --- a/.storage-layouts-normalized/contracts/colony/ColonyStorage.sol:ColonyStorage.json +++ b/.storage-layouts-normalized/contracts/colony/ColonyStorage.sol:ColonyStorage.json @@ -1409,6 +1409,27 @@ "numberOfBytes": "32" } } + }, + { + "contract": "contracts/colony/ColonyStorage.sol:ColonyStorage", + "label": "domainReputationApproval", + "offset": 0, + "slot": "39", + "type": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "label": "mapping(uint256 => uint256)", + "numberOfBytes": "32", + "value": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + } + } } ] } \ No newline at end of file diff --git a/.storage-layouts-normalized/contracts/colonyNetwork/ColonyNetwork.sol:ColonyNetwork.json b/.storage-layouts-normalized/contracts/colonyNetwork/ColonyNetwork.sol:ColonyNetwork.json index 43c45f5651..efa08ea81b 100644 --- a/.storage-layouts-normalized/contracts/colonyNetwork/ColonyNetwork.sol:ColonyNetwork.json +++ b/.storage-layouts-normalized/contracts/colonyNetwork/ColonyNetwork.sol:ColonyNetwork.json @@ -1132,6 +1132,17 @@ } } } + }, + { + "contract": "contracts/colonyNetwork/ColonyNetwork.sol:ColonyNetwork", + "label": "domainReceiverResolverAddress", + "offset": 0, + "slot": "50", + "type": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + } } ] } \ No newline at end of file diff --git a/.storage-layouts-normalized/contracts/colonyNetwork/ColonyNetworkAuction.sol:ColonyNetworkAuction.json b/.storage-layouts-normalized/contracts/colonyNetwork/ColonyNetworkAuction.sol:ColonyNetworkAuction.json index 1812851e6c..f09ac659e5 100644 --- a/.storage-layouts-normalized/contracts/colonyNetwork/ColonyNetworkAuction.sol:ColonyNetworkAuction.json +++ b/.storage-layouts-normalized/contracts/colonyNetwork/ColonyNetworkAuction.sol:ColonyNetworkAuction.json @@ -1132,6 +1132,17 @@ } } } + }, + { + "contract": "contracts/colonyNetwork/ColonyNetworkAuction.sol:ColonyNetworkAuction", + "label": "domainReceiverResolverAddress", + "offset": 0, + "slot": "50", + "type": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + } } ] } \ No newline at end of file diff --git a/.storage-layouts-normalized/contracts/colonyNetwork/ColonyNetworkDeployer.sol:ColonyNetworkDeployer.json b/.storage-layouts-normalized/contracts/colonyNetwork/ColonyNetworkDeployer.sol:ColonyNetworkDeployer.json index 7fb79ad881..205d16a0e5 100644 --- a/.storage-layouts-normalized/contracts/colonyNetwork/ColonyNetworkDeployer.sol:ColonyNetworkDeployer.json +++ b/.storage-layouts-normalized/contracts/colonyNetwork/ColonyNetworkDeployer.sol:ColonyNetworkDeployer.json @@ -1132,6 +1132,17 @@ } } } + }, + { + "contract": "contracts/colonyNetwork/ColonyNetworkDeployer.sol:ColonyNetworkDeployer", + "label": "domainReceiverResolverAddress", + "offset": 0, + "slot": "50", + "type": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + } } ] } \ No newline at end of file diff --git a/.storage-layouts-normalized/contracts/colonyNetwork/ColonyNetworkENS.sol:ColonyNetworkENS.json b/.storage-layouts-normalized/contracts/colonyNetwork/ColonyNetworkENS.sol:ColonyNetworkENS.json index 75279a954a..e884e616d7 100644 --- a/.storage-layouts-normalized/contracts/colonyNetwork/ColonyNetworkENS.sol:ColonyNetworkENS.json +++ b/.storage-layouts-normalized/contracts/colonyNetwork/ColonyNetworkENS.sol:ColonyNetworkENS.json @@ -1132,6 +1132,17 @@ } } } + }, + { + "contract": "contracts/colonyNetwork/ColonyNetworkENS.sol:ColonyNetworkENS", + "label": "domainReceiverResolverAddress", + "offset": 0, + "slot": "50", + "type": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + } } ] } \ No newline at end of file diff --git a/.storage-layouts-normalized/contracts/colonyNetwork/ColonyNetworkExtensions.sol:ColonyNetworkExtensions.json b/.storage-layouts-normalized/contracts/colonyNetwork/ColonyNetworkExtensions.sol:ColonyNetworkExtensions.json index ee28e7b452..a9bf72907d 100644 --- a/.storage-layouts-normalized/contracts/colonyNetwork/ColonyNetworkExtensions.sol:ColonyNetworkExtensions.json +++ b/.storage-layouts-normalized/contracts/colonyNetwork/ColonyNetworkExtensions.sol:ColonyNetworkExtensions.json @@ -1132,6 +1132,17 @@ } } } + }, + { + "contract": "contracts/colonyNetwork/ColonyNetworkExtensions.sol:ColonyNetworkExtensions", + "label": "domainReceiverResolverAddress", + "offset": 0, + "slot": "50", + "type": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + } } ] } \ No newline at end of file diff --git a/.storage-layouts-normalized/contracts/colonyNetwork/ColonyNetworkMining.sol:ColonyNetworkMining.json b/.storage-layouts-normalized/contracts/colonyNetwork/ColonyNetworkMining.sol:ColonyNetworkMining.json index 5b54957cde..b8e05c9bc9 100644 --- a/.storage-layouts-normalized/contracts/colonyNetwork/ColonyNetworkMining.sol:ColonyNetworkMining.json +++ b/.storage-layouts-normalized/contracts/colonyNetwork/ColonyNetworkMining.sol:ColonyNetworkMining.json @@ -1132,6 +1132,17 @@ } } } + }, + { + "contract": "contracts/colonyNetwork/ColonyNetworkMining.sol:ColonyNetworkMining", + "label": "domainReceiverResolverAddress", + "offset": 0, + "slot": "50", + "type": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + } } ] } \ No newline at end of file diff --git a/.storage-layouts-normalized/contracts/colonyNetwork/ColonyNetworkSkills.sol:ColonyNetworkSkills.json b/.storage-layouts-normalized/contracts/colonyNetwork/ColonyNetworkSkills.sol:ColonyNetworkSkills.json index 50f4099485..e31cc4d380 100644 --- a/.storage-layouts-normalized/contracts/colonyNetwork/ColonyNetworkSkills.sol:ColonyNetworkSkills.json +++ b/.storage-layouts-normalized/contracts/colonyNetwork/ColonyNetworkSkills.sol:ColonyNetworkSkills.json @@ -1132,6 +1132,17 @@ } } } + }, + { + "contract": "contracts/colonyNetwork/ColonyNetworkSkills.sol:ColonyNetworkSkills", + "label": "domainReceiverResolverAddress", + "offset": 0, + "slot": "50", + "type": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + } } ] } \ No newline at end of file diff --git a/.storage-layouts-normalized/contracts/colonyNetwork/ColonyNetworkStorage.sol:ColonyNetworkStorage.json b/.storage-layouts-normalized/contracts/colonyNetwork/ColonyNetworkStorage.sol:ColonyNetworkStorage.json index 3a616e1cf4..14053448f0 100644 --- a/.storage-layouts-normalized/contracts/colonyNetwork/ColonyNetworkStorage.sol:ColonyNetworkStorage.json +++ b/.storage-layouts-normalized/contracts/colonyNetwork/ColonyNetworkStorage.sol:ColonyNetworkStorage.json @@ -1132,6 +1132,17 @@ } } } + }, + { + "contract": "contracts/colonyNetwork/ColonyNetworkStorage.sol:ColonyNetworkStorage", + "label": "domainReceiverResolverAddress", + "offset": 0, + "slot": "50", + "type": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + } } ] } \ No newline at end of file diff --git a/.storage-layouts-normalized/contracts/common/DomainTokenReceiver.sol:DomainTokenReceiver.json b/.storage-layouts-normalized/contracts/common/DomainTokenReceiver.sol:DomainTokenReceiver.json new file mode 100644 index 0000000000..e7c1be7dd5 --- /dev/null +++ b/.storage-layouts-normalized/contracts/common/DomainTokenReceiver.sol:DomainTokenReceiver.json @@ -0,0 +1,48 @@ +{ + "storage": [ + { + "contract": "contracts/common/DomainTokenReceiver.sol:DomainTokenReceiver", + "label": "authority", + "offset": 0, + "slot": "0", + "type": { + "encoding": "inplace", + "label": "contract DSAuthority", + "numberOfBytes": "20" + } + }, + { + "contract": "contracts/common/DomainTokenReceiver.sol:DomainTokenReceiver", + "label": "owner", + "offset": 0, + "slot": "1", + "type": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + } + }, + { + "contract": "contracts/common/DomainTokenReceiver.sol:DomainTokenReceiver", + "label": "resolver", + "offset": 0, + "slot": "2", + "type": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + } + }, + { + "contract": "contracts/common/DomainTokenReceiver.sol:DomainTokenReceiver", + "label": "colony", + "offset": 0, + "slot": "3", + "type": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + } + } + ] +} \ No newline at end of file diff --git a/.storage-layouts-normalized/contracts/testHelpers/FunctionsNotAvailableOnColony.sol:FunctionsNotAvailableOnColony.json b/.storage-layouts-normalized/contracts/testHelpers/FunctionsNotAvailableOnColony.sol:FunctionsNotAvailableOnColony.json index 24a785490f..61de74236f 100644 --- a/.storage-layouts-normalized/contracts/testHelpers/FunctionsNotAvailableOnColony.sol:FunctionsNotAvailableOnColony.json +++ b/.storage-layouts-normalized/contracts/testHelpers/FunctionsNotAvailableOnColony.sol:FunctionsNotAvailableOnColony.json @@ -1409,6 +1409,27 @@ "numberOfBytes": "32" } } + }, + { + "contract": "contracts/testHelpers/FunctionsNotAvailableOnColony.sol:FunctionsNotAvailableOnColony", + "label": "domainReputationApproval", + "offset": 0, + "slot": "39", + "type": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "label": "mapping(uint256 => uint256)", + "numberOfBytes": "32", + "value": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + } + } } ] } \ No newline at end of file diff --git a/.storage-layouts-normalized/contracts/testHelpers/NoLimitSubdomains.sol:NoLimitSubdomains.json b/.storage-layouts-normalized/contracts/testHelpers/NoLimitSubdomains.sol:NoLimitSubdomains.json index 6b1c66e44e..5d5e24feb5 100644 --- a/.storage-layouts-normalized/contracts/testHelpers/NoLimitSubdomains.sol:NoLimitSubdomains.json +++ b/.storage-layouts-normalized/contracts/testHelpers/NoLimitSubdomains.sol:NoLimitSubdomains.json @@ -1409,6 +1409,27 @@ "numberOfBytes": "32" } } + }, + { + "contract": "contracts/testHelpers/NoLimitSubdomains.sol:NoLimitSubdomains", + "label": "domainReputationApproval", + "offset": 0, + "slot": "39", + "type": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "label": "mapping(uint256 => uint256)", + "numberOfBytes": "32", + "value": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + } + } } ] } \ No newline at end of file diff --git a/contracts/colony/Colony.sol b/contracts/colony/Colony.sol index 26112ab33d..5481d76c4c 100755 --- a/contracts/colony/Colony.sol +++ b/contracts/colony/Colony.sol @@ -317,11 +317,11 @@ contract Colony is BasicMetaTransaction, Multicall, ColonyStorage, PatriciaTreeP } function finishUpgrade() public always { - // Leaving as example for what is typically done here - // ColonyAuthority colonyAuthority = ColonyAuthority(address(authority)); - // bytes4 sig; - // sig = bytes4(keccak256("cancelExpenditureViaArbitration(uint256,uint256,uint256)")); - // colonyAuthority.setRoleCapability(uint8(ColonyRole.Arbitration), address(this), sig, true); + ColonyAuthority colonyAuthority = ColonyAuthority(address(authority)); + bytes4 sig; + + sig = bytes4(keccak256("editAllowedDomainReputationReceipt(uint256,uint256,bool)")); + colonyAuthority.setRoleCapability(uint8(ColonyRole.Root), address(this), sig, true); } function getMetatransactionNonce(address _user) public view override returns (uint256 nonce) { diff --git a/contracts/colony/ColonyAuthority.sol b/contracts/colony/ColonyAuthority.sol index 0af8c8b6a8..8ec4fefc4a 100644 --- a/contracts/colony/ColonyAuthority.sol +++ b/contracts/colony/ColonyAuthority.sol @@ -134,6 +134,9 @@ contract ColonyAuthority is CommonAuthority { addRoleCapability(ARBITRATION_ROLE, "finalizeExpenditureViaArbitration(uint256,uint256,uint256)"); addRoleCapability(ROOT_ROLE, "setColonyBridgeAddress(address)"); addRoleCapability(ROOT_ROLE, "initialiseReputationMining(uint256,bytes32,uint256)"); + + // Added in colony v?? + addRoleCapability(ROOT_ROLE, "editAllowedDomainReputationReceipt(uint256,uint256,bool)"); } function addRoleCapability(uint8 role, bytes memory sig) private { diff --git a/contracts/colony/ColonyDataTypes.sol b/contracts/colony/ColonyDataTypes.sol index 5afb227e68..6626761291 100755 --- a/contracts/colony/ColonyDataTypes.sol +++ b/contracts/colony/ColonyDataTypes.sol @@ -63,6 +63,14 @@ interface ColonyDataTypes { /// @param payoutRemainder The remaining funds moved to the top-level domain pot event ColonyFundsClaimed(address agent, address token, uint256 fee, uint256 payoutRemainder); + /// @notice Event logged when funds sent directly to a domain are claimed + /// @param agent The address that is responsible for triggering this event + /// @param token The token address + /// @param domainId The domain id + /// @param fee The fee deducted for rewards + /// @param payoutRemainder The remaining funds moved to the domain pot + event DomainFundsClaimed(address agent, address token, uint256 domainId, uint256 fee, uint256 payoutRemainder); + /// @notice Event logged when a new reward payout cycle has started /// @param agent The address that is responsible for triggering this event /// @param rewardPayoutId The reward payout cycle id diff --git a/contracts/colony/ColonyFunding.sol b/contracts/colony/ColonyFunding.sol index d4f533980f..6cb0d71ea8 100755 --- a/contracts/colony/ColonyFunding.sol +++ b/contracts/colony/ColonyFunding.sol @@ -23,6 +23,7 @@ import { ITokenLocking } from "./../tokenLocking/ITokenLocking.sol"; import { ColonyStorage } from "./ColonyStorage.sol"; import { ERC20Extended } from "./../common/ERC20Extended.sol"; import { IColonyNetwork } from "./../colonyNetwork/IColonyNetwork.sol"; +import { DomainTokenReceiver } from "./../common/DomainTokenReceiver.sol"; contract ColonyFunding is ColonyStorage // ignore-swc-123 @@ -106,6 +107,75 @@ contract ColonyFunding is emit ColonyFundsClaimed(msgSender(), _token, feeToPay, remainder); } + function claimDomainFunds(address _token, uint256 _domainId) public stoppable { + require(domainExists(_domainId), "colony-funding-domain-does-not-exist"); + address domainTokenReceiverAddress = IColonyNetwork(colonyNetworkAddress) + .idempotentDeployDomainTokenReceiver(_domainId); + uint256 fundingPotId = domains[_domainId].fundingPotId; + // It's deployed, so check current balance of pot + + uint256 claimAmount; + + if (_token == address(0x0)) { + claimAmount = address(domainTokenReceiverAddress).balance; + } else { + claimAmount = ERC20Extended(_token).balanceOf(address(domainTokenReceiverAddress)); + } + + uint256 feeToPay = claimAmount / 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. + uint256 remainder = claimAmount - feeToPay; + nonRewardPotsTotal[_token] += remainder; + + fundingPots[0].balance[_token] += feeToPay; + + uint256 approvedAmount = domainReputationApproval[_domainId]; + + if (tokenEarnsReputationOnPayout(_token)) { + uint256 transferrableAmount = min(approvedAmount, remainder); + uint256 untransferrableAmount = remainder - transferrableAmount; + + fundingPots[fundingPotId].balance[_token] += transferrableAmount; + domainReputationApproval[_domainId] -= transferrableAmount; + emit DomainFundsClaimed(msgSender(), _token, _domainId, feeToPay, transferrableAmount); + if (untransferrableAmount > 0) { + fundingPots[domains[1].fundingPotId].balance[_token] += untransferrableAmount; + emit ColonyFundsClaimed(msgSender(), _token, 0, untransferrableAmount); + } + } else { + fundingPots[fundingPotId].balance[_token] += remainder; + emit DomainFundsClaimed(msgSender(), _token, _domainId, feeToPay, remainder); + } + + // Claim funds + + DomainTokenReceiver(domainTokenReceiverAddress).transferToColony(_token); + } + + function tokenEarnsReputationOnPayout(address _token) internal view returns (bool) { + return _token == token; + } + + function editAllowedDomainReputationReceipt( + uint256 _domainId, + uint256 _amount, + bool _add + ) public stoppable auth { + require(domainExists(_domainId), "colony-funding-domain-does-not-exist"); + require(_domainId > 1, "colony-funding-root-domain"); + if (_add) { + domainReputationApproval[_domainId] += _amount; + } else { + domainReputationApproval[_domainId] -= _amount; + } + } + + function getAllowedDomainReputationReceipt(uint256 _domainId) public view returns (uint256) { + return domainReputationApproval[_domainId]; + } + function getNonRewardPotsTotal(address _token) public view returns (uint256) { return nonRewardPotsTotal[_token]; } diff --git a/contracts/colony/ColonyStorage.sol b/contracts/colony/ColonyStorage.sol index 850d8c653e..6975adb348 100755 --- a/contracts/colony/ColonyStorage.sol +++ b/contracts/colony/ColonyStorage.sol @@ -114,6 +114,9 @@ contract ColonyStorage is ColonyDataTypes, ColonyNetworkDataTypes, DSMath, Commo mapping(uint256 => bool) DEPRECATED_localSkills; // Storage slot 37 mapping(uint256 => LocalSkill) localSkills; // Storage slot 38 + // Mapping of domainId to allowed amount of reputation received tokens could generate if paid out + mapping(uint256 => uint256) domainReputationApproval; // Storage slot 39 + // Constants uint256 constant MAX_PAYOUT = 2 ** 128 - 1; // 340,282,366,920,938,463,463 WADs diff --git a/contracts/colony/IColony.sol b/contracts/colony/IColony.sol index d2cffee40e..d4b25f062c 100644 --- a/contracts/colony/IColony.sol +++ b/contracts/colony/IColony.sol @@ -837,6 +837,27 @@ interface IColony is IDSAuth, ColonyDataTypes, IRecovery, IBasicMetaTransaction, /// @param _token Address of the token, `0x0` value indicates Ether function claimColonyFunds(address _token) external; + /// @notice Move any funds received by the colony for a specific domain to that domain's pot + /// Currently no fees are taken + /// @param _token Address of the token, `0x0` value indicates Ether + /// @param _domainId Id of the domain + function claimDomainFunds(address _token, uint256 _domainId) external; + + /// @notice Add or remove an amount from the amount of a reputation earning token that a domain can receive + /// @param _domainId Id of the domain + /// @param _amount Amount to add or remove + /// @param _add Whether to add or remove the amount. True is add, false is remove + function editAllowedDomainReputationReceipt( + uint256 _domainId, + uint256 _amount, + bool _add + ) external; + + /// @notice Get the amount of a reputation earning token that a domain can receive + /// @param _domainId Id of the domain + /// @return uint256 amount Amount of the token that the domain can receive + function getAllowedDomainReputationReceipt(uint256 _domainId) external view returns (uint256); + /// @notice Get the total amount of tokens `_token` minus amount reserved to be paid to the reputation and token holders as rewards. /// @param _token Address of the token, `0x0` value indicates Ether /// @return amount Total amount of tokens in funding pots other than the rewards pot (id 0) diff --git a/contracts/colonyNetwork/ColonyNetworkDeployer.sol b/contracts/colonyNetwork/ColonyNetworkDeployer.sol index 09e31096c8..9afec8d524 100644 --- a/contracts/colonyNetwork/ColonyNetworkDeployer.sol +++ b/contracts/colonyNetwork/ColonyNetworkDeployer.sol @@ -19,6 +19,7 @@ pragma solidity 0.8.27; pragma experimental "ABIEncoderV2"; import { EtherRouter } from "./../common/EtherRouter.sol"; +import { Resolver } from "./../common/Resolver.sol"; import { ColonyAuthority } from "./../colony/ColonyAuthority.sol"; import { IColony } from "./../colony/IColony.sol"; import { ColonyNetworkStorage } from "./ColonyNetworkStorage.sol"; @@ -27,6 +28,8 @@ import { MetaTxToken } from "./../metaTxToken/MetaTxToken.sol"; import { DSAuth, DSAuthority } from "./../../lib/dappsys/auth.sol"; import { ICreateX } from "./../../lib/createx/src/ICreateX.sol"; import { EtherRouterCreate3 } from "./../common/EtherRouterCreate3.sol"; +import { IColonyBridge } from "./../bridging/IColonyBridge.sol"; +import { DomainTokenReceiver } from "./../common/DomainTokenReceiver.sol"; contract ColonyNetworkDeployer is ColonyNetworkStorage { address constant CREATEX_ADDRESS = 0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed; @@ -175,6 +178,81 @@ contract ColonyNetworkDeployer is ColonyNetworkStorage { // This is intentional, as we want to allow the same Colony to be deployed on different chains } + function setDomainTokenReceiverResolver(address _resolver) public stoppable auth { + domainReceiverResolverAddress = _resolver; + } + + function getDomainTokenReceiverResolver() public view returns (address) { + return domainReceiverResolverAddress; + } + + function idempotentDeployDomainTokenReceiver( + uint256 _domainId + ) public stoppable calledByColony returns (address domainTokenReceiverAddress) { + // Calculate the address the domain should be receiving funds at + domainTokenReceiverAddress = getDomainTokenReceiverAddress(msgSender(), _domainId); + + if (!isContract(domainTokenReceiverAddress)) { + // Then deploy the contract + bytes32 salt = getDomainTokenReceiverDeploySalt(msgSender(), _domainId); + address deployedAddress = deployEtherRouterViaCreateX(salt); + require( + deployedAddress == domainTokenReceiverAddress, + "colony-network-domain-receiver-deploy-wrong-address" + ); + + // Set up the deployed contract + EtherRouter(payable(domainTokenReceiverAddress)).setResolver(domainReceiverResolverAddress); + DomainTokenReceiver(domainTokenReceiverAddress).setColonyAddress(msgSender()); + } else { + // Contract is deployed, check it's got the right resolver + try EtherRouter(payable(domainTokenReceiverAddress)).resolver() returns (Resolver resolver) { + if (address(resolver) != domainReceiverResolverAddress) { + EtherRouter(payable(domainTokenReceiverAddress)).setResolver( + domainReceiverResolverAddress + ); + } + } catch { + revert("colony-network-domain-receiver-not-etherrouter"); + } + } + + return domainTokenReceiverAddress; + } + + function getDomainTokenReceiverAddress( + address _colony, + uint256 _domainId + ) public view returns (address) { + bytes32 salt = getDomainTokenReceiverDeploySalt(_colony, _domainId); + + // To get the correct address, we have to mimic the _guard functionality of CreateX + bytes32 guardedSalt = keccak256(abi.encode(bytes32(uint256(uint160(address(this)))), salt)); + return ICreateX(CREATEX_ADDRESS).computeCreate3Address(guardedSalt); + } + + function getDomainTokenReceiverDeploySalt( + address _colony, + uint256 _domainId + ) internal view returns (bytes32) { + // Calculate the address the domain should be receiving funds at + // We only want Colony Networks to be able to deploy to the same address, + // so we use the permissioned deploy protection feature of CreateX, and set + // the first 160 bits of the salt to the address of this contract. + + bytes32 salt = bytes32(uint256(uint160(address(this)))) << 96; + + bytes32 additionalSalt = keccak256(abi.encode(_colony, _domainId)); + // We use the first 88 bits of the additional salt, which is a function of the colony and domainId, + // to add entropy in the last 88 bits of the salt + salt = salt | (additionalSalt >> 168); + // We have set the first 160 bits, and the last 88 bits of the salt + // Note that this leaves byte 21 of the salt as zero (0x00), which disables cross-chain + // redeployment protection in createX. + // This is intentional, as we want to allow the same receiver to be deployed on different chains + return salt; + } + function deployColony(address _tokenAddress, uint256 _version) internal returns (address) { require(_tokenAddress != address(0x0), "colony-token-invalid-address"); require(colonyVersionResolver[_version] != address(0x00), "colony-network-invalid-version"); @@ -184,20 +262,12 @@ contract ColonyNetworkDeployer is ColonyNetworkStorage { // when it was created via a cross-chain call (to an as-yet unwritten function). bytes32 salt = getColonyCreationSalt(); // EtherRouter etherRouter = new EtherRouter(); - EtherRouter etherRouter = EtherRouter( - payable( - ICreateX(CREATEX_ADDRESS).deployCreate3AndInit( - salt, - type(EtherRouterCreate3).creationCode, - abi.encodeWithSignature("setOwner(address)", (address(this))), - ICreateX.Values(0, 0) - ) - ) - ); + address colonyAddress = deployEtherRouterViaCreateX(salt); - IColony colony = IColony(address(etherRouter)); + IColony colony = IColony(colonyAddress); address resolverForColonyVersion = colonyVersionResolver[_version]; // ignore-swc-107 + EtherRouter etherRouter = EtherRouter(payable(colonyAddress)); etherRouter.setResolver(resolverForColonyVersion); // ignore-swc-113 // Creating new instance of colony's authority @@ -240,4 +310,26 @@ contract ColonyNetworkDeployer is ColonyNetworkStorage { DSAuth dsauth = DSAuth(_colonyAddress); dsauth.setOwner(address(0x0)); } + + function deployEtherRouterViaCreateX(bytes32 _salt) internal returns (address) { + EtherRouter etherRouter = EtherRouter( + payable( + ICreateX(CREATEX_ADDRESS).deployCreate3AndInit( + _salt, + type(EtherRouterCreate3).creationCode, + abi.encodeWithSignature("setOwner(address)", (address(this))), + ICreateX.Values(0, 0) + ) + ) + ); + return address(etherRouter); + } + + function isContract(address addr) internal view returns (bool) { + uint256 size; + assembly { + size := extcodesize(addr) + } + return size > 0; + } } diff --git a/contracts/colonyNetwork/ColonyNetworkStorage.sol b/contracts/colonyNetwork/ColonyNetworkStorage.sol index c22a18d695..390c934ab2 100644 --- a/contracts/colonyNetwork/ColonyNetworkStorage.sol +++ b/contracts/colonyNetwork/ColonyNetworkStorage.sol @@ -129,6 +129,7 @@ contract ColonyNetworkStorage is ColonyNetworkDataTypes, DSMath, CommonStorage, // networkId -> colonyAddress -> updateCount -> update mapping(uint256 => mapping(address => mapping(uint256 => PendingReputationUpdate))) pendingReputationUpdates; // Storage slot 49 + address domainReceiverResolverAddress; // Storage slot 50 // Modifiers modifier calledByColony() { diff --git a/contracts/colonyNetwork/IColonyNetwork.sol b/contracts/colonyNetwork/IColonyNetwork.sol index dd4c754399..51df614574 100644 --- a/contracts/colonyNetwork/IColonyNetwork.sol +++ b/contracts/colonyNetwork/IColonyNetwork.sol @@ -620,4 +620,32 @@ interface IColonyNetwork is ColonyNetworkDataTypes, IRecovery, IBasicMetaTransac /// @param _chainId The chainId the update was bridged from /// @param _colony The colony being queried function addPendingReputationUpdate(uint256 _chainId, address _colony) external; + + /// @notice Function called by a colony to ensure that a DomainTokenReceiver has been deployed and set up correctly + /// for a particular domain. + /// @dev Should only be called by a colony. + /// @param _domainId The domainId of the domain to check the deployment for + /// @return domainTokenReceiverAddress The address of the DomainTokenReceiver + function idempotentDeployDomainTokenReceiver( + uint256 _domainId + ) external returns (address domainTokenReceiverAddress); + + /// @notice Function to set the resolver that should be used by DomainTokenReceivers + /// @dev The next time a claim for a domain is called, they will first be updated to this resolver + /// @param _resolver The address of the resolver to use + function setDomainTokenReceiverResolver(address _resolver) external; + + /// @notice Get the current DomainTokenReceiver resolver + /// @dev Note that some Receivers might be using an old resolver + /// @return resolver The address of the current resolver + function getDomainTokenReceiverResolver() external view returns (address resolver); + + /// @notice Get the DomainTokenReceiver address for a particular domain + /// @param _colonyAddress The address of the colony + /// @param _domainId The domainId of the domain + /// @return domainTokenReceiverAddress The address of the DomainTokenReceiver (which may or may not be deployed currently) + function getDomainTokenReceiverAddress( + address _colonyAddress, + uint256 _domainId + ) external view returns (address domainTokenReceiverAddress); } diff --git a/contracts/common/DomainTokenReceiver.sol b/contracts/common/DomainTokenReceiver.sol new file mode 100644 index 0000000000..6ef24d3576 --- /dev/null +++ b/contracts/common/DomainTokenReceiver.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +/* + 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.8.27; // ignore-swc-103 +import { ERC20Extended } from "./ERC20Extended.sol"; +import { DSAuth } from "./../../lib/dappsys/auth.sol"; + +contract DomainTokenReceiver is DSAuth { + address resolver; // Storage slot 2 (from DSAuth there is authority and owner at storage slots 0 and 1 respectively) + + address colony; + + function getColonyAddress() public view returns (address) { + return colony; + } + + function setColonyAddress(address _colony) public auth { + require(colony == address(0), "domain-token-receiver-colony-already-set"); + colony = _colony; + } + + function transferToColony(address tokenAddress) public { + // Transfer the token to the colony. + if (tokenAddress == address(0)) { + // slither-disable-next-line arbitrary-send-eth + payable(colony).transfer(address(this).balance); + return; + } else { + uint256 balanceToTransfer = ERC20Extended(tokenAddress).balanceOf(address(this)); + require( + ERC20Extended(tokenAddress).transfer(colony, balanceToTransfer), + "domain-token-receiver-transfer-failed" + ); + } + } +} diff --git a/docs/interfaces/icolony.md b/docs/interfaces/icolony.md index 910059b244..033da0286e 100644 --- a/docs/interfaces/icolony.md +++ b/docs/interfaces/icolony.md @@ -174,6 +174,19 @@ Move any funds received by the colony in `_token` denomination to the top-level |_token|address|Address of the token, `0x0` value indicates Ether +### ▸ `claimDomainFunds(address _token, uint256 _domainId)` + +Move any funds received by the colony for a specific domain to that domain's pot Currently no fees are taken + + +**Parameters** + +|Name|Type|Description| +|---|---|---| +|_token|address|Address of the token, `0x0` value indicates Ether +|_domainId|uint256|Id of the domain + + ### ▸ `claimExpenditurePayout(uint256 _id, uint256 _slot, address _token)` Claim the payout for an expenditure slot. Here the network receives a fee from each payout. @@ -260,6 +273,20 @@ Deprecate a local skill for the colony. Secured function to authorised members. |deprecated|bool|Deprecation status to set for the skill +### ▸ `editAllowedDomainReputationReceipt(uint256 _domainId, uint256 _amount, bool _add)` + +Add or remove an amount from the amount of a reputation earning token that a domain can receive + + +**Parameters** + +|Name|Type|Description| +|---|---|---| +|_domainId|uint256|Id of the domain +|_amount|uint256|Amount to add or remove +|_add|bool|Whether to add or remove the amount. True is add, false is remove + + ### ▸ `editColony(string memory _metadata)` Called to change the metadata associated with a colony. Expected to be a IPFS hash of a JSON blob, but not enforced to any degree by the contracts @@ -438,6 +465,23 @@ A function to be called after an upgrade has been done from v2 to v3. +### ▸ `getAllowedDomainReputationReceipt(uint256 _domainId):uint256 uint256` + +Get the amount of a reputation earning token that a domain can receive + + +**Parameters** + +|Name|Type|Description| +|---|---|---| +|_domainId|uint256|Id of the domain + +**Return Parameters** + +|Name|Type|Description| +|---|---|---| +|uint256|uint256|amount Amount of the token that the domain can receive + ### ▸ `getApproval(address _user, address _obligator, uint256 _domainId):uint256 approval` View an approval to obligate tokens. diff --git a/docs/interfaces/icolonynetwork.md b/docs/interfaces/icolonynetwork.md index 1e5198f38c..18dcd8f0e8 100644 --- a/docs/interfaces/icolonynetwork.md +++ b/docs/interfaces/icolonynetwork.md @@ -603,6 +603,37 @@ Returns the latest Colony contract version. This is the version used to create a |---|---|---| |_version|uint256|The current / latest Colony contract version +### ▸ `getDomainTokenReceiverAddress(address _colonyAddress, uint256 _domainId):address domainTokenReceiverAddress` + +Get the DomainTokenReceiver address for a particular domain + + +**Parameters** + +|Name|Type|Description| +|---|---|---| +|_colonyAddress|address|The address of the colony +|_domainId|uint256|The domainId of the domain + +**Return Parameters** + +|Name|Type|Description| +|---|---|---| +|domainTokenReceiverAddress|address|The address of the DomainTokenReceiver (which may or may not be deployed currently) + +### ▸ `getDomainTokenReceiverResolver():address resolver` + +Get the current DomainTokenReceiver resolver + +*Note: Note that some Receivers might be using an old resolver* + + +**Return Parameters** + +|Name|Type|Description| +|---|---|---| +|resolver|address|The address of the current resolver + ### ▸ `getENSRegistrar():address _address` Returns the address of the ENSRegistrar for the Network. @@ -994,6 +1025,24 @@ Get token locking contract address. |---|---|---| |_lockingAddress|address|Token locking contract address +### ▸ `idempotentDeployDomainTokenReceiver(uint256 _domainId):address domainTokenReceiverAddress` + +Function called by a colony to ensure that a DomainTokenReceiver has been deployed and set up correctly for a particular domain. + +*Note: Should only be called by a colony.* + +**Parameters** + +|Name|Type|Description| +|---|---|---| +|_domainId|uint256|The domainId of the domain to check the deployment for + +**Return Parameters** + +|Name|Type|Description| +|---|---|---| +|domainTokenReceiverAddress|address|The address of the DomainTokenReceiver + ### ▸ `initialise(address _resolver, uint256 _version)` Initialises the colony network by setting the first Colony version resolver to `_resolver` address. @@ -1202,6 +1251,19 @@ Called to set the address of the colony bridge contract |_bridgeAddress|address|The address of the bridge +### ▸ `setDomainTokenReceiverResolver(address _resolver)` + +Function to set the resolver that should be used by DomainTokenReceivers + +*Note: The next time a claim for a domain is called, they will first be updated to this resolver* + +**Parameters** + +|Name|Type|Description| +|---|---|---| +|_resolver|address|The address of the resolver to use + + ### ▸ `setFeeInverse(uint256 _feeInverse)` Set the colony network fee to pay. e.g. if the fee is 1% (or 0.01), pass 100 as `_feeInverse`. diff --git a/docs/interfaces/imetacolony.md b/docs/interfaces/imetacolony.md index 8146d9f269..cab1e6ea16 100644 --- a/docs/interfaces/imetacolony.md +++ b/docs/interfaces/imetacolony.md @@ -197,6 +197,19 @@ Move any funds received by the colony in `_token` denomination to the top-level |_token|address|Address of the token, `0x0` value indicates Ether +### ▸ `claimDomainFunds(address _token, uint256 _domainId)` + +Move any funds received by the colony for a specific domain to that domain's pot Currently no fees are taken + + +**Parameters** + +|Name|Type|Description| +|---|---|---| +|_token|address|Address of the token, `0x0` value indicates Ether +|_domainId|uint256|Id of the domain + + ### ▸ `claimExpenditurePayout(uint256 _id, uint256 _slot, address _token)` Claim the payout for an expenditure slot. Here the network receives a fee from each payout. @@ -283,6 +296,20 @@ Deprecate a local skill for the colony. Secured function to authorised members. |deprecated|bool|Deprecation status to set for the skill +### ▸ `editAllowedDomainReputationReceipt(uint256 _domainId, uint256 _amount, bool _add)` + +Add or remove an amount from the amount of a reputation earning token that a domain can receive + + +**Parameters** + +|Name|Type|Description| +|---|---|---| +|_domainId|uint256|Id of the domain +|_amount|uint256|Amount to add or remove +|_add|bool|Whether to add or remove the amount. True is add, false is remove + + ### ▸ `editColony(string memory _metadata)` Called to change the metadata associated with a colony. Expected to be a IPFS hash of a JSON blob, but not enforced to any degree by the contracts @@ -461,6 +488,23 @@ A function to be called after an upgrade has been done from v2 to v3. +### ▸ `getAllowedDomainReputationReceipt(uint256 _domainId):uint256 uint256` + +Get the amount of a reputation earning token that a domain can receive + + +**Parameters** + +|Name|Type|Description| +|---|---|---| +|_domainId|uint256|Id of the domain + +**Return Parameters** + +|Name|Type|Description| +|---|---|---| +|uint256|uint256|amount Amount of the token that the domain can receive + ### ▸ `getApproval(address _user, address _obligator, uint256 _domainId):uint256 approval` View an approval to obligate tokens. diff --git a/helpers/test-helper.js b/helpers/test-helper.js index 0195413646..ea1ffcb554 100644 --- a/helpers/test-helper.js +++ b/helpers/test-helper.js @@ -1347,7 +1347,7 @@ exports.isXdai = async function isXdai() { return chainId === XDAI_CHAINID || chainId === FORKED_XDAI_CHAINID; }; -exports.deployCreateXIfNeeded = async function deployCreateXIfNeeded() { +exports.idempotentDeployCreateX = async function idempotentDeployCreateX() { // Deploy CreateX if it's not already deployed const createXCode = await web3.eth.getCode(CREATEX_ADDRESS); if (createXCode === "0x") { diff --git a/helpers/upgradable-contracts.js b/helpers/upgradable-contracts.js index d65c032ddb..abf1a55ba1 100644 --- a/helpers/upgradable-contracts.js +++ b/helpers/upgradable-contracts.js @@ -158,6 +158,16 @@ exports.setupUpgradableTokenLocking = async function setupUpgradableTokenLocking assert.equal(registeredResolver, resolver.address); }; +exports.setupDomainTokenReceiverResolver = async function setupDomainTokenReceiver(colonyNetwork, domainTokenReceiver, resolver) { + const deployedImplementations = {}; + deployedImplementations.DomainTokenReceiver = domainTokenReceiver.address; + await exports.setupEtherRouter("domainTokenReceiver", "DomainTokenReceiver", deployedImplementations, resolver); + + await colonyNetwork.setDomainTokenReceiverResolver(resolver.address); + const registeredResolver = await colonyNetwork.getDomainTokenReceiverResolver(); + assert.equal(registeredResolver, resolver.address); +}; + exports.setupReputationMiningCycleResolver = async function setupReputationMiningCycleResolver( reputationMiningCycle, reputationMiningCycleRespond, diff --git a/scripts/check-recovery.js b/scripts/check-recovery.js index 6ef94c8ff8..c3bb09cf88 100755 --- a/scripts/check-recovery.js +++ b/scripts/check-recovery.js @@ -38,6 +38,7 @@ walkSync("./contracts/").forEach((contractName) => { "contracts/common/IBasicMetaTransaction.sol", "contracts/common/CommonAuthority.sol", "contracts/common/DomainRoles.sol", + "contracts/common/DomainTokenReceiver.sol", "contracts/common/ERC20Extended.sol", "contracts/common/EtherRouter.sol", "contracts/common/EtherRouterCreate3.sol", diff --git a/scripts/check-storage.js b/scripts/check-storage.js index 7159b5f7f6..bf0056bebf 100755 --- a/scripts/check-storage.js +++ b/scripts/check-storage.js @@ -24,6 +24,7 @@ walkSync("./contracts/").forEach((contractName) => { "contracts/common/CommonAuthority.sol", "contracts/common/CommonStorage.sol", "contracts/common/DomainRoles.sol", + "contracts/common/DomainTokenReceiver.sol", "contracts/common/EtherRouter.sol", "contracts/common/Resolver.sol", "contracts/common/TokenAuthority.sol", // Imported from colonyToken repo diff --git a/test/contracts-network/colony-expenditure.js b/test/contracts-network/colony-expenditure.js index 913564b02d..f34d185ae5 100644 --- a/test/contracts-network/colony-expenditure.js +++ b/test/contracts-network/colony-expenditure.js @@ -479,11 +479,14 @@ contract("Colony Expenditure", (accounts) => { await checkErrorRevert(colony.setExpenditurePayout(expenditureId, SLOT0, token.address, WAD), "colony-expenditure-not-owner"); }); - it("should allow owners to add a slot payout", async () => { + it("should allow only owners to add a slot payout", async () => { await colony.setExpenditurePayout(expenditureId, SLOT0, token.address, WAD, { from: ADMIN }); const payout = await colony.getExpenditureSlotPayout(expenditureId, SLOT0, token.address); expect(payout).to.eq.BN(WAD); + + await colony.transferExpenditure(expenditureId, USER, { from: ADMIN }); + await checkErrorRevert(colony.setExpenditurePayout(expenditureId, SLOT0, token.address, WAD, { from: ADMIN }), "colony-expenditure-not-owner"); }); it("should be able to add multiple payouts in different tokens", async () => { diff --git a/test/contracts-network/colony-funding.js b/test/contracts-network/colony-funding.js index a21136406e..b03ab547c3 100755 --- a/test/contracts-network/colony-funding.js +++ b/test/contracts-network/colony-funding.js @@ -14,10 +14,13 @@ const { SLOT0, SLOT1, SLOT2, + ROOT_ROLE, + ADDRESS_ZERO, } = require("../../helpers/constants"); const { fundColonyWithTokens, setupRandomColony, makeExpenditure, setupFundedExpenditure } = require("../../helpers/test-data-generator"); -const { getTokenArgs, checkErrorRevert, web3GetBalance, removeSubdomainLimit } = require("../../helpers/test-helper"); +const { getTokenArgs, checkErrorRevert, web3GetBalance, removeSubdomainLimit, expectEvent, rolesToBytes32 } = require("../../helpers/test-helper"); +const { setupDomainTokenReceiverResolver } = require("../../helpers/upgradable-contracts"); const { expect } = chai; chai.use(bnChai(web3.utils.BN)); @@ -26,6 +29,8 @@ const EtherRouter = artifacts.require("EtherRouter"); const IColonyNetwork = artifacts.require("IColonyNetwork"); const IMetaColony = artifacts.require("IMetaColony"); const Token = artifacts.require("Token"); +const Resolver = artifacts.require("Resolver"); +const DomainTokenReceiver = artifacts.require("DomainTokenReceiver"); contract("Colony Funding", (accounts) => { const MANAGER = accounts[0]; @@ -418,7 +423,7 @@ contract("Colony Funding", (accounts) => { it("should correctly send whitelisted tokens to the Metacolony", async () => { await fundColonyWithTokens(colony, token, INITIAL_FUNDING); - + const currentFee = await colonyNetwork.getFeeInverse(); await metaColony.setNetworkFeeInverse(1); // 100% to fees const expenditureId = await setupFundedExpenditure({ colonyNetwork, colony }); @@ -435,6 +440,8 @@ contract("Colony Funding", (accounts) => { await colony.claimExpenditurePayout(expenditureId, SLOT2, token.address); const metaColonyBalanceAfter = await token.balanceOf(metaColony.address); expect(metaColonyBalanceAfter.sub(metaColonyBalanceBefore)).to.eq.BN(WORKER_PAYOUT); + + await metaColony.setNetworkFeeInverse(currentFee); // Restore fees }); }); @@ -549,5 +556,237 @@ contract("Colony Funding", (accounts) => { expect(colonyRewardPotBalance).to.eq.BN(3); expect(nonRewardPotsTotal).to.eq.BN(297); }); + + it("should allow native coins to be directly sent to a domain", async () => { + // Get address for domain 2 + await colony.addDomain(1, UINT256_MAX, 1); + const receiverAddress = await colonyNetwork.getDomainTokenReceiverAddress(colony.address, 2); + + // Send 100 wei + await web3.eth.sendTransaction({ from: MANAGER, to: receiverAddress, value: 100, gas: 1000000 }); + + const domain = await colony.getDomain(2); + const domainPotBalanceBefore = await colony.getFundingPotBalance(domain.fundingPotId, ethers.constants.AddressZero); + const nonRewardPotsTotalBefore = await colony.getNonRewardPotsTotal(ethers.constants.AddressZero); + + // Claim the funds + + const tx = await colony.claimDomainFunds(ethers.constants.AddressZero, 2); + await expectEvent(tx, "DomainFundsClaimed", [MANAGER, ethers.constants.AddressZero, 2, 1, 99]); + + const domainPotBalanceAfter = await colony.getFundingPotBalance(domain.fundingPotId, ethers.constants.AddressZero); + const nonRewardPotsTotalAfter = await colony.getNonRewardPotsTotal(ethers.constants.AddressZero); + + // Check the balance of the domain + expect(domainPotBalanceAfter.sub(domainPotBalanceBefore)).to.eq.BN(99); + expect(nonRewardPotsTotalAfter.sub(nonRewardPotsTotalBefore)).to.eq.BN(99); + }); + + it("should allow a token to be directly sent to a domain", async () => { + // Get address for domain 2 + await colony.addDomain(1, UINT256_MAX, 1); + const receiverAddress = await colonyNetwork.getDomainTokenReceiverAddress(colony.address, 2); + + // Send 100 wei + await otherToken.mint(receiverAddress, 100); + + const domain = await colony.getDomain(2); + const domainPotBalanceBefore = await colony.getFundingPotBalance(domain.fundingPotId, otherToken.address); + const nonRewardPotsTotalBefore = await colony.getNonRewardPotsTotal(otherToken.address); + + // Claim the funds + const tx = await colony.claimDomainFunds(otherToken.address, 2); + await expectEvent(tx, "DomainFundsClaimed", [MANAGER, otherToken.address, 2, 1, 99]); + + const domainPotBalanceAfter = await colony.getFundingPotBalance(domain.fundingPotId, otherToken.address); + const nonRewardPotsTotalAfter = await colony.getNonRewardPotsTotal(otherToken.address); + + // Check the balance of the domain + expect(domainPotBalanceAfter.sub(domainPotBalanceBefore)).to.eq.BN(99); + expect(nonRewardPotsTotalAfter.sub(nonRewardPotsTotalBefore)).to.eq.BN(99); + }); + + it("should not allow even the colonyNetwork to call setColonyAddress once it's set on domainTokenReceiver", async () => { + await colony.addDomain(1, UINT256_MAX, 1); + const receiverAddress = await colonyNetwork.getDomainTokenReceiverAddress(colony.address, 2); + await colony.claimDomainFunds(ethers.constants.AddressZero, 2); + const receiver = await DomainTokenReceiver.at(receiverAddress); + + await checkErrorRevert(receiver.setColonyAddress(ADDRESS_ZERO, { from: colonyNetwork.address }), "domain-token-receiver-colony-already-set"); + }); + + it("when receiving native (reputation-earning) token, if no approval present for domain, all are received by root domain", async () => { + // Get address for domain 2 + await colony.addDomain(1, UINT256_MAX, 1); + const receiverAddress = await colonyNetwork.getDomainTokenReceiverAddress(colony.address, 2); + await colony.mintTokens(WAD.muln(100)); + await colony.claimColonyFunds(token.address); + const domain1 = await colony.getDomain(1); + + // Send an arbitrary transaction to mint tokens for receiverAddress + const txData = token.contract.methods["mint(address,uint256)"](receiverAddress, 100).encodeABI(); + await colony.makeArbitraryTransaction(token.address, txData); + + // Now test what happens when we claim them + + const domain = await colony.getDomain(2); + const domainPotBalanceBefore = await colony.getFundingPotBalance(domain.fundingPotId, token.address); + const nonRewardPotsTotalBefore = await colony.getNonRewardPotsTotal(token.address); + const rootDomainPotBalanceBefore = await colony.getFundingPotBalance(domain1.fundingPotId, token.address); + + // Claim the funds + await colony.claimDomainFunds(token.address, 2); + + const domainPotBalanceAfter = await colony.getFundingPotBalance(domain.fundingPotId, token.address); + const nonRewardPotsTotalAfter = await colony.getNonRewardPotsTotal(token.address); + const rootDomainPotBalanceAfter = await colony.getFundingPotBalance(domain1.fundingPotId, token.address); + + // Check the balance of the domain + expect(domainPotBalanceAfter.sub(domainPotBalanceBefore)).to.eq.BN(0); + expect(nonRewardPotsTotalAfter.sub(nonRewardPotsTotalBefore)).to.eq.BN(99); + expect(rootDomainPotBalanceAfter.sub(rootDomainPotBalanceBefore)).to.eq.BN(99); + }); + + it(`when receiving native (reputation-earning) token, if partial approval present for domain, + tokens are split between intended domain and root`, async () => { + // Get address for domain 2 + await colony.addDomain(1, UINT256_MAX, 1); + const receiverAddress = await colonyNetwork.getDomainTokenReceiverAddress(colony.address, 2); + await colony.mintTokens(WAD.muln(100)); + await colony.claimColonyFunds(token.address); + const domain1 = await colony.getDomain(1); + + // Send an arbitrary transaction to mint tokens for receiverAddress + const txData = token.contract.methods["mint(address,uint256)"](receiverAddress, 100).encodeABI(); + await colony.makeArbitraryTransaction(token.address, txData); + + // Approve 70 for the domain + await colony.editAllowedDomainReputationReceipt(2, 70, true); + let allowedReceipt = await colony.getAllowedDomainReputationReceipt(2); + expect(allowedReceipt).to.eq.BN(70); + + // Now test what happens when we claim them + + const domain = await colony.getDomain(2); + const domainPotBalanceBefore = await colony.getFundingPotBalance(domain.fundingPotId, token.address); + const nonRewardPotsTotalBefore = await colony.getNonRewardPotsTotal(token.address); + const rootDomainPotBalanceBefore = await colony.getFundingPotBalance(domain1.fundingPotId, token.address); + + // Claim the funds + await colony.claimDomainFunds(token.address, 2); + + const domainPotBalanceAfter = await colony.getFundingPotBalance(domain.fundingPotId, token.address); + const nonRewardPotsTotalAfter = await colony.getNonRewardPotsTotal(token.address); + const rootDomainPotBalanceAfter = await colony.getFundingPotBalance(domain1.fundingPotId, token.address); + + // Check the balance of the domain + expect(domainPotBalanceAfter.sub(domainPotBalanceBefore)).to.eq.BN(70); + expect(nonRewardPotsTotalAfter.sub(nonRewardPotsTotalBefore)).to.eq.BN(99); + expect(rootDomainPotBalanceAfter.sub(rootDomainPotBalanceBefore)).to.eq.BN(29); + + allowedReceipt = await colony.getAllowedDomainReputationReceipt(2); + expect(allowedReceipt).to.eq.BN(0); + }); + + it(`root permission is required to call editAllowedDomainReputationReceipt`, async () => { + await colony.addDomain(1, UINT256_MAX, 1); + await checkErrorRevert(colony.editAllowedDomainReputationReceipt(2, 70, true, { from: WORKER }), "ds-auth-unauthorized"); + const rootRole = rolesToBytes32([ROOT_ROLE]); + + await colony.setUserRoles(1, UINT256_MAX, WORKER, 1, rootRole); + await colony.editAllowedDomainReputationReceipt(2, 70, true, { from: WORKER }); + }); + + it(`cannot editAllowedDomainReputationReceipt for a domain that does not exist`, async () => { + await checkErrorRevert(colony.editAllowedDomainReputationReceipt(2, 70, true), "colony-funding-domain-does-not-exist"); + }); + + it(`can add and remove allowed domain token receipts as expected`, async () => { + await colony.addDomain(1, UINT256_MAX, 1); + await colony.editAllowedDomainReputationReceipt(2, 70, true); + let allowedReceipt = await colony.getAllowedDomainReputationReceipt(2); + expect(allowedReceipt).to.eq.BN(70); + + await colony.editAllowedDomainReputationReceipt(2, 20, false); + allowedReceipt = await colony.getAllowedDomainReputationReceipt(2); + expect(allowedReceipt).to.eq.BN(50); + }); + + it(`cannot editAllowedDomainReputationReceipt for the root domain`, async () => { + await checkErrorRevert(colony.editAllowedDomainReputationReceipt(1, 70, true), "colony-funding-root-domain"); + }); + + it(`when receiving native (reputation-earning) token, if full approval present for domain, + tokens are received by domain`, async () => { + // Get address for domain 2 + await colony.addDomain(1, UINT256_MAX, 1); + const receiverAddress = await colonyNetwork.getDomainTokenReceiverAddress(colony.address, 2); + await colony.mintTokens(WAD.muln(100)); + await colony.claimColonyFunds(token.address); + const domain1 = await colony.getDomain(1); + + // Send an arbitrary transaction to mint tokens for receiverAddress + const txData = token.contract.methods["mint(address,uint256)"](receiverAddress, 100).encodeABI(); + await colony.makeArbitraryTransaction(token.address, txData); + + // Approve 250 for the domain + await colony.editAllowedDomainReputationReceipt(2, 250, true); + let allowedReceipt = await colony.getAllowedDomainReputationReceipt(2); + expect(allowedReceipt).to.eq.BN(250); + + // Now test what happens when we claim them + + const domain = await colony.getDomain(2); + const domainPotBalanceBefore = await colony.getFundingPotBalance(domain.fundingPotId, token.address); + const nonRewardPotsTotalBefore = await colony.getNonRewardPotsTotal(token.address); + const rootDomainPotBalanceBefore = await colony.getFundingPotBalance(domain1.fundingPotId, token.address); + + // Claim the funds + await colony.claimDomainFunds(token.address, 2); + + const domainPotBalanceAfter = await colony.getFundingPotBalance(domain.fundingPotId, token.address); + const nonRewardPotsTotalAfter = await colony.getNonRewardPotsTotal(token.address); + const rootDomainPotBalanceAfter = await colony.getFundingPotBalance(domain1.fundingPotId, token.address); + + // Check the balance of the domain + expect(domainPotBalanceAfter.sub(domainPotBalanceBefore)).to.eq.BN(99); + expect(nonRewardPotsTotalAfter.sub(nonRewardPotsTotalBefore)).to.eq.BN(99); + expect(rootDomainPotBalanceAfter.sub(rootDomainPotBalanceBefore)).to.eq.BN(0); + + allowedReceipt = await colony.getAllowedDomainReputationReceipt(2); + expect(allowedReceipt).to.eq.BN(151); + }); + + it("should not be able to claim funds for a domain that does not exist", async () => { + await checkErrorRevert(colony.claimDomainFunds(ethers.constants.AddressZero, 2), "colony-funding-domain-does-not-exist"); + }); + + it("only a colony can call idempotentDeployDomainTokenReceiver on Network", async () => { + await checkErrorRevert(colonyNetwork.idempotentDeployDomainTokenReceiver(2), "colony-caller-must-be-colony"); + }); + + it("If the receiver resolver is updated, then the resolver is updated at the next claim", async () => { + await colony.addDomain(1, UINT256_MAX, 1); + const receiverAddress = await colonyNetwork.getDomainTokenReceiverAddress(colony.address, 2); + // Send 100 wei + await otherToken.mint(receiverAddress, 100); + await colony.claimDomainFunds(otherToken.address, 2); + + const receiverAsEtherRouter = await EtherRouter.at(receiverAddress); + const resolver = await receiverAsEtherRouter.resolver(); + + // Update the resolver + const newResolver = await Resolver.new(); + const domainTokenReceiver = await DomainTokenReceiver.new(); + + await setupDomainTokenReceiverResolver(colonyNetwork, domainTokenReceiver, newResolver); + + await otherToken.mint(receiverAddress, 50); + await colony.claimDomainFunds(otherToken.address, 2); + + const resolverAfter = await receiverAsEtherRouter.resolver(); + expect(resolverAfter).to.not.equal(resolver); + expect(resolverAfter).to.equal(newResolver.address); + }); }); }); diff --git a/test/contracts-network/colony-network-recovery.js b/test/contracts-network/colony-network-recovery.js index 24208347fe..4f4f139972 100644 --- a/test/contracts-network/colony-network-recovery.js +++ b/test/contracts-network/colony-network-recovery.js @@ -199,7 +199,8 @@ contract("Colony Network Recovery", (accounts) => { await checkErrorRevert(colonyNetwork.addReputationUpdateLogFromBridge(ADDRESS_ZERO, ADDRESS_ZERO, 0, 0, 0), "colony-in-recovery-mode"); await checkErrorRevert(colonyNetwork.addPendingReputationUpdate(0, ADDRESS_ZERO), "colony-in-recovery-mode"); await checkErrorRevert(colonyNetwork.setReputationRootHashFromBridge(HASHZERO, 0, 0), "colony-in-recovery-mode"); - + await checkErrorRevert(colonyNetwork.setDomainTokenReceiverResolver(ADDRESS_ZERO), "colony-in-recovery-mode"); + await checkErrorRevert(colonyNetwork.idempotentDeployDomainTokenReceiver(ADDRESS_ZERO), "colony-in-recovery-mode"); await colonyNetwork.approveExitRecovery(); await colonyNetwork.exitRecoveryMode(); }); diff --git a/test/contracts-network/colony-network.js b/test/contracts-network/colony-network.js index 6df1a16654..f84384cc4d 100755 --- a/test/contracts-network/colony-network.js +++ b/test/contracts-network/colony-network.js @@ -812,4 +812,14 @@ contract("Colony Network", (accounts) => { expect(owner).to.equal(accounts[1]); }); }); + + describe("when working with DomainTokenReceivers", () => { + it("should only allow owner to set the DomainTokenReceiverResolver", async () => { + await checkErrorRevert(colonyNetwork.setDomainTokenReceiverResolver(ADDRESS_ZERO, { from: accounts[1] }), "ds-auth-unauthorized"); + const cnAsEtherRouter = await EtherRouter.at(colonyNetwork.address); + const owner = await cnAsEtherRouter.owner(); + expect(owner).to.equal(accounts[0]); + await colonyNetwork.setDomainTokenReceiverResolver(ADDRESS_ZERO); + }); + }); }); diff --git a/test/contracts-network/colony-recovery.js b/test/contracts-network/colony-recovery.js index aeb4793e15..b785f7d102 100644 --- a/test/contracts-network/colony-recovery.js +++ b/test/contracts-network/colony-recovery.js @@ -213,6 +213,8 @@ contract("Colony Recovery", (accounts) => { await checkErrorRevert(metaColony.makeSingleArbitraryTransaction(ADDRESS_ZERO, HASHZERO), "colony-in-recovery-mode"); await checkErrorRevert(metaColony.updateApprovalAmount(ADDRESS_ZERO, ADDRESS_ZERO), "colony-in-recovery-mode"); await checkErrorRevert(metaColony.finalizeRewardPayout(1), "colony-in-recovery-mode"); + await checkErrorRevert(metaColony.claimDomainFunds(ADDRESS_ZERO, 1), "colony-in-recovery-mode"); + await checkErrorRevert(metaColony.editAllowedDomainReputationReceipt(1, 1, true), "colony-in-recovery-mode"); }); it("recovery functions should be permissioned", async () => { diff --git a/test/truffle-fixture.js b/test/truffle-fixture.js index f5a60df48d..bb22dafa57 100644 --- a/test/truffle-fixture.js +++ b/test/truffle-fixture.js @@ -14,6 +14,8 @@ const ColonyRoles = artifacts.require("ColonyRoles"); const ColonyArbitraryTransaction = artifacts.require("ColonyArbitraryTransaction"); const IMetaColony = artifacts.require("IMetaColony"); +const DomainTokenReceiver = artifacts.require("DomainTokenReceiver"); + const ColonyNetworkAuthority = artifacts.require("ColonyNetworkAuthority"); const ColonyNetwork = artifacts.require("ColonyNetwork"); const ColonyNetworkDeployer = artifacts.require("ColonyNetworkDeployer"); @@ -77,9 +79,10 @@ const { setupReputationMiningCycleResolver, setupENSRegistrar, setupEtherRouter, + setupDomainTokenReceiverResolver, } = require("../helpers/upgradable-contracts"); const { FORKED_XDAI_CHAINID, XDAI_CHAINID, UINT256_MAX, CREATEX_ADDRESS } = require("../helpers/constants"); -const { getChainId, hardhatRevert, hardhatSnapshot, deployCreateXIfNeeded, isXdai } = require("../helpers/test-helper"); +const { getChainId, hardhatRevert, hardhatSnapshot, idempotentDeployCreateX, isXdai } = require("../helpers/test-helper"); module.exports = async () => { if (postFixtureSnapshotId) { @@ -94,6 +97,7 @@ module.exports = async () => { await setupColony(); await setupTokenLocking(); await setupMiningCycle(); + await setupDomainTokenReceiver(); await setupEnsRegistry(); await setupMetaColony(); await setupExtensions(); @@ -141,7 +145,7 @@ async function deployContracts() { const reputationMiningCycleBinarySearch = await ReputationMiningCycleBinarySearch.new(); ReputationMiningCycleBinarySearch.setAsDeployed(reputationMiningCycleBinarySearch); - await deployCreateXIfNeeded(); + await idempotentDeployCreateX(); } async function setupColonyNetwork() { @@ -246,6 +250,15 @@ async function setupTokenLocking() { await tokenLocking.setColonyNetwork(colonyNetwork.address); } +async function setupDomainTokenReceiver() { + const colonyNetworkRouter = await EtherRouter.deployed(); + const colonyNetwork = await IColonyNetwork.at(colonyNetworkRouter.address); + + const domainTokenReceiverImplementation = await DomainTokenReceiver.new(); + const domainTokenReceiverResolver = await Resolver.new(); + await setupDomainTokenReceiverResolver(colonyNetwork, domainTokenReceiverImplementation, domainTokenReceiverResolver); +} + async function setupMiningCycle() { const reputationMiningCycle = await ReputationMiningCycle.deployed(); const reputationMiningCycleRespond = await ReputationMiningCycleRespond.deployed();