From 417d0c2f5217eed395b08e8684d20bc308d3ab94 Mon Sep 17 00:00:00 2001 From: Alex Rea Date: Wed, 11 Sep 2024 16:41:34 +0100 Subject: [PATCH 01/16] Allow funds to be sent directly to domains --- .../ColonyNetwork.sol:ColonyNetwork.json | 11 + ...tworkAuction.sol:ColonyNetworkAuction.json | 11 + ...orkDeployer.sol:ColonyNetworkDeployer.json | 11 + ...ColonyNetworkENS.sol:ColonyNetworkENS.json | 11 + ...xtensions.sol:ColonyNetworkExtensions.json | 11 + ...NetworkMining.sol:ColonyNetworkMining.json | 11 + ...NetworkSkills.sol:ColonyNetworkSkills.json | 11 + ...tworkStorage.sol:ColonyNetworkStorage.json | 11 + ...TokenReceiver.sol:DomainTokenReceiver.json | 48 ++++ contracts/colony/ColonyFunding.sol | 38 +++ contracts/colony/IColony.sol | 6 + .../colonyNetwork/ColonyNetworkDeployer.sol | 86 +++++++ .../colonyNetwork/ColonyNetworkStorage.sol | 1 + contracts/colonyNetwork/IColonyNetwork.sol | 28 ++ contracts/common/DomainTokenReceiver.sol | 52 ++++ docs/interfaces/icolony.md | 25 +- docs/interfaces/icolonynetwork.md | 242 +++++------------- docs/interfaces/imetacolony.md | 25 +- helpers/upgradable-contracts.js | 10 + scripts/check-recovery.js | 1 + scripts/check-storage.js | 1 + test/contracts-network/colony-funding.js | 75 ++++++ .../colony-network-recovery.js | 3 +- test/contracts-network/colony-network.js | 10 + test/contracts-network/colony-recovery.js | 1 + test/truffle-fixture.js | 13 + 26 files changed, 548 insertions(+), 205 deletions(-) create mode 100644 .storage-layouts-normalized/contracts/common/DomainTokenReceiver.sol:DomainTokenReceiver.json create mode 100644 contracts/common/DomainTokenReceiver.sol 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/contracts/colony/ColonyFunding.sol b/contracts/colony/ColonyFunding.sol index d4f533980f..867ee587ae 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,43 @@ 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)); + } + + fundingPots[fundingPotId].balance[_token] += claimAmount; + + // Claim funds + + DomainTokenReceiver(domainTokenReceiverAddress).transferToColony(_token); + + // Add to funding pot + + uint256 balanceAfter = getFundingPotBalance(fundingPotId, _token); + + assert(balanceAfter - balanceBefore == claimAmount); + + uint256 thisBalanceAfter; + if (_token == address(0x0)) { + thisBalanceAfter = address(this).balance; + } else { + thisBalanceAfter = ERC20Extended(_token).balanceOf(address(this)); + } + + assert(thisBalanceAfter - thisBalanceBefore == claimAmount); + } + function getNonRewardPotsTotal(address _token) public view returns (uint256) { return nonRewardPotsTotal[_token]; } diff --git a/contracts/colony/IColony.sol b/contracts/colony/IColony.sol index d2cffee40e..175c3e3493 100644 --- a/contracts/colony/IColony.sol +++ b/contracts/colony/IColony.sol @@ -837,6 +837,12 @@ 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 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..a9075b8ff0 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,89 @@ 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" + ); + } + + // 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"); + } + + // Check it's set up correctly + + if (DomainTokenReceiver(domainTokenReceiverAddress).getColonyAddress() != msgSender()) { + DomainTokenReceiver(domainTokenReceiverAddress).setColonyAddress(msgSender()); + } + + return domainTokenReceiverAddress; + } + + function isContract(address addr) internal view returns (bool) { + uint256 size; + assembly { + size := extcodesize(addr) + } + return size > 0; + } + + 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"); 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..b85561b95a --- /dev/null +++ b/contracts/common/DomainTokenReceiver.sol @@ -0,0 +1,52 @@ +// 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 { + 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 { + require( + ERC20Extended(tokenAddress).transfer( + colony, + ERC20Extended(tokenAddress).balanceOf(address(this)) + ), + "domain-token-receiver-transfer-failed" + ); + } + } +} diff --git a/docs/interfaces/icolony.md b/docs/interfaces/icolony.md index 910059b244..236f4d9617 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. @@ -1540,18 +1553,6 @@ Set new colony funding role. Can be called by root role or architecture role. |_setTo|bool|The state of the role permission (true assign the permission, false revokes it) -### ▸ `setOwner(address owner_)` - -Set the owner of the contract - - -**Parameters** - -|Name|Type|Description| -|---|---|---| -|owner_|address|The new owner of the contract - - ### ▸ `setRecoveryRole(address _user)` Set new colony recovery role. Can be called by root. diff --git a/docs/interfaces/icolonynetwork.md b/docs/interfaces/icolonynetwork.md index 1e5198f38c..2e230638b5 100644 --- a/docs/interfaces/icolonynetwork.md +++ b/docs/interfaces/icolonynetwork.md @@ -36,48 +36,6 @@ Add a new extension resolver to the Extensions repository. |_resolver|address|The deployed resolver containing the extension contract logic -### ▸ `addPendingReputationUpdate(uint256 _chainId, address _colony)` - -Try to emit the next reputation update that was bridged but previously failed, if any - - -**Parameters** - -|Name|Type|Description| -|---|---|---| -|_chainId|uint256|The chainId the update was bridged from -|_colony|address|The colony being queried - - -### ▸ `addPendingSkill(uint256 _skillId)` - -Called to add a bridged skill that wasn't next when it was bridged, but now is - - -**Parameters** - -|Name|Type|Description| -|---|---|---| -|_skillId|uint256|The skillId of the skill being bridged - - -### ▸ `addReputationUpdateLogFromBridge(address _colony, address _user, int _amount, uint _skillId, uint256 _updateNumber)` - -Adds a reputation update entry to log. - -*Note: Errors if it is called by anyone but a known bridge* - -**Parameters** - -|Name|Type|Description| -|---|---|---| -|_colony|address|The colony the reputation is being awarded in -|_user|address|The address of the user for the reputation update -|_amount|int|The amount of reputation change for the update, this can be a negative as well as a positive value -|_skillId|uint|The skill for the reputation update -|_updateNumber|uint256|The counter used for ordering bridged updates - - ### ▸ `addSkill(uint256 _parentSkillId):uint256 _skillId` Adds a new skill to the domain or local skills tree, under skill `_parentSkillId`. Any colony is allowed to add a local skill and which is associated with a new domain via `IColony.addDomain`. @@ -96,19 +54,6 @@ Adds a new skill to the domain or local skills tree, under skill `_parentSkillId |---|---|---| |_skillId|uint256|Id of the added skill -### ▸ `addSkillFromBridge(uint256 _parentSkillId, uint256 _skillCount)` - -Function called by bridge transactions to add a new skill - - -**Parameters** - -|Name|Type|Description| -|---|---|---| -|_parentSkillId|uint256|The parent id of the new skill -|_skillCount|uint256|The number of the new skill being created - - ### ▸ `addr(bytes32 _node):address _address` Returns the address the supplied node resolves do, if we are the resolver. @@ -148,43 +93,6 @@ Indicate approval to exit recovery mode. Can only be called by user with recover -### ▸ `bridgeCurrentRootHash(uint256 chainId)` - -Initiate a cross-chain update of the current reputation state - - -**Parameters** - -|Name|Type|Description| -|---|---|---| -|chainId|uint256|The chainid we want to bridge to - - -### ▸ `bridgePendingReputationUpdate(address _colony, uint256 _updateNumber)` - -Try to bridge a reputation update that (previously) failed - - -**Parameters** - -|Name|Type|Description| -|---|---|---| -|_colony|address|The colony being queried -|_updateNumber|uint256|the emission index to bridge - - -### ▸ `bridgeSkillIfNotMiningChain(uint256 skillId)` - -Called to re-send the bridging transaction for a skill to the - - -**Parameters** - -|Name|Type|Description| -|---|---|---| -|skillId|uint256|The skillId we're bridging the creation of - - ### ▸ `burnUnneededRewards(uint256 _amount)` Used to burn tokens that are not needed to pay out rewards (because not every possible defence was made for all submissions) @@ -467,42 +375,6 @@ Exit recovery mode, can be called by anyone if enough whitelist approvals are gi -### ▸ `getBridgedReputationUpdateCount(uint256 _chainId, address _colony):uint256 bridgedReputationCount` - -Get the (currently bridged) reputation update count of a chain - -*Note: On the non-mining chain, this tracks the number of reputation updates that have either been bridged, or attempted to be bridged (and failed, and are now pending bridging). On the mining chain, it tracks how many have been successfully bridged and added to the log.* - -**Parameters** - -|Name|Type|Description| -|---|---|---| -|_chainId|uint256|The chainid of the chain -|_colony|address|The colony being queried - -**Return Parameters** - -|Name|Type|Description| -|---|---|---| -|bridgedReputationCount|uint256|The bridge reputation count of the corresponding chain - -### ▸ `getBridgedSkillCounts(uint256 _chainId):uint256 skillCount` - -Get the (currently bridged) skill count of another chain - - -**Parameters** - -|Name|Type|Description| -|---|---|---| -|_chainId|uint256|The chainid of foreign chain - -**Return Parameters** - -|Name|Type|Description| -|---|---|---| -|skillCount|uint256|The skillCount of the corresponding chain - ### ▸ `getChildSkillId(uint256 _skillId, uint256 _childSkillIndex):uint256 _childSkillId` Get the id of the child skill at index `_childSkillIndex` for skill with Id `_skillId`. @@ -603,6 +475,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. @@ -785,43 +688,6 @@ Get a token's status in the payout whitelist |---|---|---| |_status|bool|Will be `true` if token is whitelisted -### ▸ `getPendingReputationUpdate(uint256 _chainId, address _colony, uint256 _updateNumber):PendingReputationUpdate update` - -Get the details of a reputation update that was bridged but was not added to the log because it was bridged out of order - - -**Parameters** - -|Name|Type|Description| -|---|---|---| -|_chainId|uint256|The chainId the update was bridged from -|_colony|address|The colony being queried -|_updateNumber|uint256|the updatenumber being queries - -**Return Parameters** - -|Name|Type|Description| -|---|---|---| -|update|PendingReputationUpdate|The update stored for that chain/colony/updateNumber - -### ▸ `getPendingSkillAddition(uint256 _chainId, uint256 _skillCount):uint256 parentId` - -Called to get the information about a skill that has been bridged out of order - - -**Parameters** - -|Name|Type|Description| -|---|---|---| -|_chainId|uint256|The chainId we're bridging from -|_skillCount|uint256|The skill count - -**Return Parameters** - -|Name|Type|Description| -|---|---|---| -|parentId|uint256|The parent id of the skill being added - ### ▸ `getProfileDBAddress(bytes32 _node):string _orbitdb` Retrieve the orbitdb address corresponding to a registered account. @@ -994,6 +860,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 +1086,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`. @@ -1311,21 +1208,6 @@ Set a new Reputation root hash and starts a new mining cycle. Can only be called |_stakers|address[]|Array of users who submitted or backed the hash, being accepted here as the new reputation root hash -### ▸ `setReputationRootHashFromBridge(bytes32 newHash, uint256 newNLeaves, uint256 nonce)` - -Update the reputation on a foreign chain from the mining chain - -*Note: Should error if called by anyone other than the known bridge from the mining chain* - -**Parameters** - -|Name|Type|Description| -|---|---|---| -|newHash|bytes32|The new root hash -|newNLeaves|uint256|The new nLeaves in the root hash -|nonce|uint256|The nonce to ensure these txs can't be replayed - - ### ▸ `setStorageSlotRecovery(uint256 _slot, bytes32 _value)` Update value of arbitrary storage variable. Can only be called by user with recovery role. diff --git a/docs/interfaces/imetacolony.md b/docs/interfaces/imetacolony.md index 8146d9f269..d3150c737d 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. @@ -1603,18 +1616,6 @@ Set the Colony Network fee inverse amount. |_feeInverse|uint256|Nonzero amount for the fee inverse -### ▸ `setOwner(address owner_)` - -Set the owner of the contract - - -**Parameters** - -|Name|Type|Description| -|---|---|---| -|owner_|address|The new owner of the contract - - ### ▸ `setPayoutWhitelist(address _token, bool _status)` Set a token's status in the payout whitelist on the Colony Network 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-funding.js b/test/contracts-network/colony-funding.js index a21136406e..96e34aaf17 100755 --- a/test/contracts-network/colony-funding.js +++ b/test/contracts-network/colony-funding.js @@ -18,6 +18,7 @@ const { const { fundColonyWithTokens, setupRandomColony, makeExpenditure, setupFundedExpenditure } = require("../../helpers/test-data-generator"); const { getTokenArgs, checkErrorRevert, web3GetBalance, removeSubdomainLimit } = require("../../helpers/test-helper"); +const { setupDomainTokenReceiverResolver } = require("../../helpers/upgradable-contracts"); const { expect } = chai; chai.use(bnChai(web3.utils.BN)); @@ -26,6 +27,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]; @@ -549,5 +552,77 @@ 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); + + // Claim the funds + await colony.claimDomainFunds(ethers.constants.AddressZero, 2); + + const domainPotBalanceAfter = await colony.getFundingPotBalance(domain.fundingPotId, ethers.constants.AddressZero); + + // Check the balance of the domain + expect(domainPotBalanceAfter.sub(domainPotBalanceBefore)).to.eq.BN(100); + }); + + 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); + + // Claim the funds + await colony.claimDomainFunds(otherToken.address, 2); + + const domainPotBalanceAfter = await colony.getFundingPotBalance(domain.fundingPotId, otherToken.address); + + // Check the balance of the domain + expect(domainPotBalanceAfter.sub(domainPotBalanceBefore)).to.eq.BN(100); + }); + + 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..38f3ac192a 100644 --- a/test/contracts-network/colony-recovery.js +++ b/test/contracts-network/colony-recovery.js @@ -213,6 +213,7 @@ 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"); }); it("recovery functions should be permissioned", async () => { diff --git a/test/truffle-fixture.js b/test/truffle-fixture.js index f5a60df48d..1bac822b62 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,6 +79,7 @@ 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"); @@ -94,6 +97,7 @@ module.exports = async () => { await setupColony(); await setupTokenLocking(); await setupMiningCycle(); + await setupDomainTokenReceiver(); await setupEnsRegistry(); await setupMetaColony(); await setupExtensions(); @@ -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(); From 94d001e457ed59272229439beb35b05e51a5ef98 Mon Sep 17 00:00:00 2001 From: Alex Rea Date: Mon, 16 Sep 2024 11:13:33 +0100 Subject: [PATCH 02/16] Respect reward pots for domain-level claims; add event --- contracts/colony/ColonyDataTypes.sol | 8 ++++++++ contracts/colony/ColonyFunding.sol | 25 ++++++++++-------------- test/contracts-network/colony-funding.js | 19 +++++++++++++----- 3 files changed, 32 insertions(+), 20 deletions(-) diff --git a/contracts/colony/ColonyDataTypes.sol b/contracts/colony/ColonyDataTypes.sol index 1b2215d25c..8649cc073c 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 867ee587ae..d8e655086f 100755 --- a/contracts/colony/ColonyFunding.sol +++ b/contracts/colony/ColonyFunding.sol @@ -122,26 +122,21 @@ contract ColonyFunding is claimAmount = ERC20Extended(_token).balanceOf(address(domainTokenReceiverAddress)); } - fundingPots[fundingPotId].balance[_token] += claimAmount; + 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; + fundingPots[fundingPotId].balance[_token] += remainder; // Claim funds DomainTokenReceiver(domainTokenReceiverAddress).transferToColony(_token); - // Add to funding pot - - uint256 balanceAfter = getFundingPotBalance(fundingPotId, _token); - - assert(balanceAfter - balanceBefore == claimAmount); - - uint256 thisBalanceAfter; - if (_token == address(0x0)) { - thisBalanceAfter = address(this).balance; - } else { - thisBalanceAfter = ERC20Extended(_token).balanceOf(address(this)); - } - - assert(thisBalanceAfter - thisBalanceBefore == claimAmount); + emit DomainFundsClaimed(msgSender(), _token, _domainId, feeToPay, remainder); } function getNonRewardPotsTotal(address _token) public view returns (uint256) { diff --git a/test/contracts-network/colony-funding.js b/test/contracts-network/colony-funding.js index 96e34aaf17..ff62ba7b4e 100755 --- a/test/contracts-network/colony-funding.js +++ b/test/contracts-network/colony-funding.js @@ -17,7 +17,7 @@ const { } = 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 } = require("../../helpers/test-helper"); const { setupDomainTokenReceiverResolver } = require("../../helpers/upgradable-contracts"); const { expect } = chai; @@ -563,14 +563,19 @@ contract("Colony Funding", (accounts) => { 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 - await colony.claimDomainFunds(ethers.constants.AddressZero, 2); + + 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(100); + 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 () => { @@ -583,14 +588,18 @@ contract("Colony Funding", (accounts) => { 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 - await colony.claimDomainFunds(otherToken.address, 2); + 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(100); + expect(domainPotBalanceAfter.sub(domainPotBalanceBefore)).to.eq.BN(99); + expect(nonRewardPotsTotalAfter.sub(nonRewardPotsTotalBefore)).to.eq.BN(99); }); it("should not be able to claim funds for a domain that does not exist", async () => { From 6e710d485f14f7bf789ba0a3aef9f7ede725db88 Mon Sep 17 00:00:00 2001 From: Alex Rea Date: Thu, 26 Sep 2024 10:35:28 +0100 Subject: [PATCH 03/16] Remove overly cautious checks in claimDomainFunds --- .../colonyNetwork/ColonyNetworkDeployer.sol | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/contracts/colonyNetwork/ColonyNetworkDeployer.sol b/contracts/colonyNetwork/ColonyNetworkDeployer.sol index a9075b8ff0..61fcf5941a 100644 --- a/contracts/colonyNetwork/ColonyNetworkDeployer.sol +++ b/contracts/colonyNetwork/ColonyNetworkDeployer.sol @@ -270,20 +270,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 @@ -326,4 +318,18 @@ 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); + } } From 73879f8a5ad666cd0ba1587b37ee7a174cffeca0 Mon Sep 17 00:00:00 2001 From: Alex Rea Date: Thu, 26 Sep 2024 18:04:56 +0100 Subject: [PATCH 04/16] Rename deployCreateXIfNeeded --- docs/interfaces/icolony.md | 12 ++ docs/interfaces/icolonynetwork.md | 180 ++++++++++++++++++++++++++++++ docs/interfaces/imetacolony.md | 12 ++ helpers/test-helper.js | 2 +- test/truffle-fixture.js | 4 +- 5 files changed, 207 insertions(+), 3 deletions(-) diff --git a/docs/interfaces/icolony.md b/docs/interfaces/icolony.md index 236f4d9617..916d26f5ca 100644 --- a/docs/interfaces/icolony.md +++ b/docs/interfaces/icolony.md @@ -1553,6 +1553,18 @@ Set new colony funding role. Can be called by root role or architecture role. |_setTo|bool|The state of the role permission (true assign the permission, false revokes it) +### ▸ `setOwner(address owner_)` + +Set the owner of the contract + + +**Parameters** + +|Name|Type|Description| +|---|---|---| +|owner_|address|The new owner of the contract + + ### ▸ `setRecoveryRole(address _user)` Set new colony recovery role. Can be called by root. diff --git a/docs/interfaces/icolonynetwork.md b/docs/interfaces/icolonynetwork.md index 2e230638b5..18dcd8f0e8 100644 --- a/docs/interfaces/icolonynetwork.md +++ b/docs/interfaces/icolonynetwork.md @@ -36,6 +36,48 @@ Add a new extension resolver to the Extensions repository. |_resolver|address|The deployed resolver containing the extension contract logic +### ▸ `addPendingReputationUpdate(uint256 _chainId, address _colony)` + +Try to emit the next reputation update that was bridged but previously failed, if any + + +**Parameters** + +|Name|Type|Description| +|---|---|---| +|_chainId|uint256|The chainId the update was bridged from +|_colony|address|The colony being queried + + +### ▸ `addPendingSkill(uint256 _skillId)` + +Called to add a bridged skill that wasn't next when it was bridged, but now is + + +**Parameters** + +|Name|Type|Description| +|---|---|---| +|_skillId|uint256|The skillId of the skill being bridged + + +### ▸ `addReputationUpdateLogFromBridge(address _colony, address _user, int _amount, uint _skillId, uint256 _updateNumber)` + +Adds a reputation update entry to log. + +*Note: Errors if it is called by anyone but a known bridge* + +**Parameters** + +|Name|Type|Description| +|---|---|---| +|_colony|address|The colony the reputation is being awarded in +|_user|address|The address of the user for the reputation update +|_amount|int|The amount of reputation change for the update, this can be a negative as well as a positive value +|_skillId|uint|The skill for the reputation update +|_updateNumber|uint256|The counter used for ordering bridged updates + + ### ▸ `addSkill(uint256 _parentSkillId):uint256 _skillId` Adds a new skill to the domain or local skills tree, under skill `_parentSkillId`. Any colony is allowed to add a local skill and which is associated with a new domain via `IColony.addDomain`. @@ -54,6 +96,19 @@ Adds a new skill to the domain or local skills tree, under skill `_parentSkillId |---|---|---| |_skillId|uint256|Id of the added skill +### ▸ `addSkillFromBridge(uint256 _parentSkillId, uint256 _skillCount)` + +Function called by bridge transactions to add a new skill + + +**Parameters** + +|Name|Type|Description| +|---|---|---| +|_parentSkillId|uint256|The parent id of the new skill +|_skillCount|uint256|The number of the new skill being created + + ### ▸ `addr(bytes32 _node):address _address` Returns the address the supplied node resolves do, if we are the resolver. @@ -93,6 +148,43 @@ Indicate approval to exit recovery mode. Can only be called by user with recover +### ▸ `bridgeCurrentRootHash(uint256 chainId)` + +Initiate a cross-chain update of the current reputation state + + +**Parameters** + +|Name|Type|Description| +|---|---|---| +|chainId|uint256|The chainid we want to bridge to + + +### ▸ `bridgePendingReputationUpdate(address _colony, uint256 _updateNumber)` + +Try to bridge a reputation update that (previously) failed + + +**Parameters** + +|Name|Type|Description| +|---|---|---| +|_colony|address|The colony being queried +|_updateNumber|uint256|the emission index to bridge + + +### ▸ `bridgeSkillIfNotMiningChain(uint256 skillId)` + +Called to re-send the bridging transaction for a skill to the + + +**Parameters** + +|Name|Type|Description| +|---|---|---| +|skillId|uint256|The skillId we're bridging the creation of + + ### ▸ `burnUnneededRewards(uint256 _amount)` Used to burn tokens that are not needed to pay out rewards (because not every possible defence was made for all submissions) @@ -375,6 +467,42 @@ Exit recovery mode, can be called by anyone if enough whitelist approvals are gi +### ▸ `getBridgedReputationUpdateCount(uint256 _chainId, address _colony):uint256 bridgedReputationCount` + +Get the (currently bridged) reputation update count of a chain + +*Note: On the non-mining chain, this tracks the number of reputation updates that have either been bridged, or attempted to be bridged (and failed, and are now pending bridging). On the mining chain, it tracks how many have been successfully bridged and added to the log.* + +**Parameters** + +|Name|Type|Description| +|---|---|---| +|_chainId|uint256|The chainid of the chain +|_colony|address|The colony being queried + +**Return Parameters** + +|Name|Type|Description| +|---|---|---| +|bridgedReputationCount|uint256|The bridge reputation count of the corresponding chain + +### ▸ `getBridgedSkillCounts(uint256 _chainId):uint256 skillCount` + +Get the (currently bridged) skill count of another chain + + +**Parameters** + +|Name|Type|Description| +|---|---|---| +|_chainId|uint256|The chainid of foreign chain + +**Return Parameters** + +|Name|Type|Description| +|---|---|---| +|skillCount|uint256|The skillCount of the corresponding chain + ### ▸ `getChildSkillId(uint256 _skillId, uint256 _childSkillIndex):uint256 _childSkillId` Get the id of the child skill at index `_childSkillIndex` for skill with Id `_skillId`. @@ -688,6 +816,43 @@ Get a token's status in the payout whitelist |---|---|---| |_status|bool|Will be `true` if token is whitelisted +### ▸ `getPendingReputationUpdate(uint256 _chainId, address _colony, uint256 _updateNumber):PendingReputationUpdate update` + +Get the details of a reputation update that was bridged but was not added to the log because it was bridged out of order + + +**Parameters** + +|Name|Type|Description| +|---|---|---| +|_chainId|uint256|The chainId the update was bridged from +|_colony|address|The colony being queried +|_updateNumber|uint256|the updatenumber being queries + +**Return Parameters** + +|Name|Type|Description| +|---|---|---| +|update|PendingReputationUpdate|The update stored for that chain/colony/updateNumber + +### ▸ `getPendingSkillAddition(uint256 _chainId, uint256 _skillCount):uint256 parentId` + +Called to get the information about a skill that has been bridged out of order + + +**Parameters** + +|Name|Type|Description| +|---|---|---| +|_chainId|uint256|The chainId we're bridging from +|_skillCount|uint256|The skill count + +**Return Parameters** + +|Name|Type|Description| +|---|---|---| +|parentId|uint256|The parent id of the skill being added + ### ▸ `getProfileDBAddress(bytes32 _node):string _orbitdb` Retrieve the orbitdb address corresponding to a registered account. @@ -1208,6 +1373,21 @@ Set a new Reputation root hash and starts a new mining cycle. Can only be called |_stakers|address[]|Array of users who submitted or backed the hash, being accepted here as the new reputation root hash +### ▸ `setReputationRootHashFromBridge(bytes32 newHash, uint256 newNLeaves, uint256 nonce)` + +Update the reputation on a foreign chain from the mining chain + +*Note: Should error if called by anyone other than the known bridge from the mining chain* + +**Parameters** + +|Name|Type|Description| +|---|---|---| +|newHash|bytes32|The new root hash +|newNLeaves|uint256|The new nLeaves in the root hash +|nonce|uint256|The nonce to ensure these txs can't be replayed + + ### ▸ `setStorageSlotRecovery(uint256 _slot, bytes32 _value)` Update value of arbitrary storage variable. Can only be called by user with recovery role. diff --git a/docs/interfaces/imetacolony.md b/docs/interfaces/imetacolony.md index d3150c737d..7679ece549 100644 --- a/docs/interfaces/imetacolony.md +++ b/docs/interfaces/imetacolony.md @@ -1616,6 +1616,18 @@ Set the Colony Network fee inverse amount. |_feeInverse|uint256|Nonzero amount for the fee inverse +### ▸ `setOwner(address owner_)` + +Set the owner of the contract + + +**Parameters** + +|Name|Type|Description| +|---|---|---| +|owner_|address|The new owner of the contract + + ### ▸ `setPayoutWhitelist(address _token, bool _status)` Set a token's status in the payout whitelist on the Colony Network 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/test/truffle-fixture.js b/test/truffle-fixture.js index 1bac822b62..bb22dafa57 100644 --- a/test/truffle-fixture.js +++ b/test/truffle-fixture.js @@ -82,7 +82,7 @@ const { 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) { @@ -145,7 +145,7 @@ async function deployContracts() { const reputationMiningCycleBinarySearch = await ReputationMiningCycleBinarySearch.new(); ReputationMiningCycleBinarySearch.setAsDeployed(reputationMiningCycleBinarySearch); - await deployCreateXIfNeeded(); + await idempotentDeployCreateX(); } async function setupColonyNetwork() { From a8ab2f5fd6f4f67dc78853d4350697530622f501 Mon Sep 17 00:00:00 2001 From: Alex Rea Date: Fri, 11 Oct 2024 17:55:13 +0100 Subject: [PATCH 05/16] Root has to allow domains to receive reputation-earning tokens --- contracts/colony/Colony.sol | 10 +- contracts/colony/ColonyAuthority.sol | 3 + contracts/colony/ColonyFunding.sol | 50 ++++++- contracts/colony/ColonyStorage.sol | 3 + contracts/colony/IColony.sol | 21 +++ docs/interfaces/icolony.md | 33 +++++ test/contracts-network/colony-funding.js | 158 ++++++++++++++++++++++- 7 files changed, 269 insertions(+), 9 deletions(-) diff --git a/contracts/colony/Colony.sol b/contracts/colony/Colony.sol index 26112ab33d..7bde99f014 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("editAllowedDomainTokenReceipt(uint256,address,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..95e5c6ed23 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, "editAllowedDomainTokenReceipt(uint256,address,uint256,bool)"); } function addRoleCapability(uint8 role, bytes memory sig) private { diff --git a/contracts/colony/ColonyFunding.sol b/contracts/colony/ColonyFunding.sol index d8e655086f..328b0eaeb0 100755 --- a/contracts/colony/ColonyFunding.sol +++ b/contracts/colony/ColonyFunding.sol @@ -130,13 +130,59 @@ contract ColonyFunding is nonRewardPotsTotal[_token] += remainder; fundingPots[0].balance[_token] += feeToPay; - fundingPots[fundingPotId].balance[_token] += remainder; + + if (tokenEarnsReputationOnPayout(_token)) { + // If token earns reputation, we only allow up to the approved amount to be received + uint256 approvedAmount = domainReputationTokenApprovals[_domainId][_token]; + if (approvedAmount < remainder) { + fundingPots[fundingPotId].balance[_token] += approvedAmount; + Domain storage rootDomain = domains[1]; + // And the rest goes to the root pot + fundingPots[rootDomain.fundingPotId].balance[_token] += remainder - approvedAmount; + domainReputationTokenApprovals[_domainId][_token] = 0; + emit DomainFundsClaimed(msgSender(), _token, _domainId, feeToPay, approvedAmount); + emit ColonyFundsClaimed(msgSender(), _token, 0, remainder - approvedAmount); + } else { + fundingPots[fundingPotId].balance[_token] += remainder; + domainReputationTokenApprovals[_domainId][_token] -= remainder; + emit DomainFundsClaimed(msgSender(), _token, _domainId, feeToPay, remainder); + } + } 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; + } - emit DomainFundsClaimed(msgSender(), _token, _domainId, feeToPay, remainder); + function editAllowedDomainTokenReceipt( + uint256 _domainId, + address _token, + uint256 _amount, + bool _add + ) public stoppable auth { + require(domainExists(_domainId), "colony-funding-domain-does-not-exist"); + require(tokenEarnsReputationOnPayout(_token), "colony-funding-token-does-not-earn-reputation"); + if (_add) { + domainReputationTokenApprovals[_domainId][_token] += _amount; + } else { + domainReputationTokenApprovals[_domainId][_token] -= _amount; + } + } + + function getAllowedDomainTokenReceipt( + uint256 _domainId, + address _token + ) public view returns (uint256) { + require(domainExists(_domainId), "colony-funding-domain-does-not-exist"); + require(tokenEarnsReputationOnPayout(_token), "colony-funding-token-does-not-earn-reputation"); + return domainReputationTokenApprovals[_domainId][_token]; } function getNonRewardPotsTotal(address _token) public view returns (uint256) { diff --git a/contracts/colony/ColonyStorage.sol b/contracts/colony/ColonyStorage.sol index 850d8c653e..e0971c4c2b 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 domain id => token address => approval to receive if reputation-earning + mapping(uint256 => mapping(address => uint256)) domainReputationTokenApprovals; // 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 175c3e3493..e1e37314f4 100644 --- a/contracts/colony/IColony.sol +++ b/contracts/colony/IColony.sol @@ -843,6 +843,27 @@ interface IColony is IDSAuth, ColonyDataTypes, IRecovery, IBasicMetaTransaction, /// @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 _token Address of the token + /// @param _amount Amount to add or remove + /// @param _add Whether to add or remove the amount. True is add, false is remove + function editAllowedDomainTokenReceipt( + uint256 _domainId, + address _token, + 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 + /// @param _token Address of the token + /// @return uint256 amount Amount of the token that the domain can receive + function getAllowedDomainTokenReceipt( + uint256 _domainId, + address _token + ) 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/docs/interfaces/icolony.md b/docs/interfaces/icolony.md index 916d26f5ca..6a79e39e32 100644 --- a/docs/interfaces/icolony.md +++ b/docs/interfaces/icolony.md @@ -273,6 +273,21 @@ Deprecate a local skill for the colony. Secured function to authorised members. |deprecated|bool|Deprecation status to set for the skill +### ▸ `editAllowedDomainTokenReceipt(uint256 _domainId, address _token, 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 +|_token|address|Address of the token +|_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 @@ -451,6 +466,24 @@ A function to be called after an upgrade has been done from v2 to v3. +### ▸ `getAllowedDomainTokenReceipt(uint256 _domainId, address _token):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 +|_token|address|Address of the token + +**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/test/contracts-network/colony-funding.js b/test/contracts-network/colony-funding.js index ff62ba7b4e..82f74a1594 100755 --- a/test/contracts-network/colony-funding.js +++ b/test/contracts-network/colony-funding.js @@ -16,7 +16,13 @@ const { SLOT2, } = require("../../helpers/constants"); -const { fundColonyWithTokens, setupRandomColony, makeExpenditure, setupFundedExpenditure } = require("../../helpers/test-data-generator"); +const { + fundColonyWithTokens, + setupRandomColony, + makeExpenditure, + setupFundedExpenditure, + setupClaimedExpenditure, +} = require("../../helpers/test-data-generator"); const { getTokenArgs, checkErrorRevert, web3GetBalance, removeSubdomainLimit, expectEvent } = require("../../helpers/test-helper"); const { setupDomainTokenReceiverResolver } = require("../../helpers/upgradable-contracts"); @@ -421,7 +427,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 }); @@ -438,6 +444,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 }); }); @@ -602,6 +610,152 @@ contract("Colony Funding", (accounts) => { expect(nonRewardPotsTotalAfter.sub(nonRewardPotsTotalBefore)).to.eq.BN(99); }); + 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); + + // Pay the tokens to the domain + await setupClaimedExpenditure({ + colonyNetwork, + colony, + domainId: 1, + manager: MANAGER, + managerPayout: 1000, + evaluatorPayout: 0, + workerPayout: 0, + }); + + // Send 100 to the domain + await token.transfer(receiverAddress, 100); + + // 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); + + // Pay the tokens to the domain + await setupClaimedExpenditure({ + colonyNetwork, + colony, + domainId: 1, + manager: MANAGER, + tokenAddress: token.address, + managerPayout: 1000, + evaluatorPayout: 0, + workerPayout: 0, + }); + + // Send 100 to the domain + await token.transfer(receiverAddress, 100); + + // Approve 70 for the domain + await colony.editAllowedDomainTokenReceipt(2, token.address, 70, true); + let allowedReceipt = await colony.getAllowedDomainTokenReceipt(2, token.address); + 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.getAllowedDomainTokenReceipt(2, token.address); + expect(allowedReceipt).to.eq.BN(0); + }); + + 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); + + // Pay the tokens to the domain + await setupClaimedExpenditure({ + colonyNetwork, + colony, + domainId: 1, + manager: MANAGER, + tokenAddress: token.address, + managerPayout: 1000, + evaluatorPayout: 0, + workerPayout: 0, + }); + + // Send 100 to the domain + await token.transfer(receiverAddress, 100); + + // Approve 250 for the domain + await colony.editAllowedDomainTokenReceipt(2, token.address, 250, true); + let allowedReceipt = await colony.getAllowedDomainTokenReceipt(2, token.address); + 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.getAllowedDomainTokenReceipt(2, token.address); + 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"); }); From c8b0bc08fa787e8488031c35fc25cba3814ae015 Mon Sep 17 00:00:00 2001 From: Alex Rea Date: Mon, 14 Oct 2024 12:29:57 +0100 Subject: [PATCH 06/16] Update storage layouts for root permitting reputation token receipt in domains --- .../bridging/ProxyColony.sol:ProxyColony.json | 79 ++++++++++++++ ...yColonyNetwork.sol:ProxyColonyNetwork.json | 102 ++++++++++++++++++ .../contracts/colony/Colony.sol:Colony.json | 31 ++++++ ...action.sol:ColonyArbitraryTransaction.json | 31 ++++++ .../ColonyDomains.sol:ColonyDomains.json | 31 ++++++ ...lonyExpenditure.sol:ColonyExpenditure.json | 31 ++++++ .../ColonyFunding.sol:ColonyFunding.json | 31 ++++++ .../ColonyRewards.sol:ColonyRewards.json | 31 ++++++ .../colony/ColonyRoles.sol:ColonyRoles.json | 31 ++++++ .../ColonyStorage.sol:ColonyStorage.json | 31 ++++++ ...nagement.sol:DomainReceiverManagement.json | 3 + .../common/IsContract.sol:IsContract.json | 3 + ...ony.sol:FunctionsNotAvailableOnColony.json | 31 ++++++ ...FacetProxyMock.sol:LiFiFacetProxyMock.json | 3 + ...LimitSubdomains.sol:NoLimitSubdomains.json | 31 ++++++ 15 files changed, 500 insertions(+) create mode 100644 .storage-layouts-normalized/contracts/bridging/ProxyColony.sol:ProxyColony.json create mode 100644 .storage-layouts-normalized/contracts/bridging/ProxyColonyNetwork.sol:ProxyColonyNetwork.json create mode 100644 .storage-layouts-normalized/contracts/common/DomainReceiverManagement.sol:DomainReceiverManagement.json create mode 100644 .storage-layouts-normalized/contracts/common/IsContract.sol:IsContract.json create mode 100644 .storage-layouts-normalized/contracts/testHelpers/LiFiFacetProxyMock.sol:LiFiFacetProxyMock.json diff --git a/.storage-layouts-normalized/contracts/bridging/ProxyColony.sol:ProxyColony.json b/.storage-layouts-normalized/contracts/bridging/ProxyColony.sol:ProxyColony.json new file mode 100644 index 0000000000..f020621b32 --- /dev/null +++ b/.storage-layouts-normalized/contracts/bridging/ProxyColony.sol:ProxyColony.json @@ -0,0 +1,79 @@ +{ + "storage": [ + { + "contract": "contracts/bridging/ProxyColony.sol:ProxyColony", + "label": "authority", + "offset": 0, + "slot": "0", + "type": { + "encoding": "inplace", + "label": "contract DSAuthority", + "numberOfBytes": "20" + } + }, + { + "contract": "contracts/bridging/ProxyColony.sol:ProxyColony", + "label": "owner", + "offset": 0, + "slot": "1", + "type": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + } + }, + { + "contract": "contracts/bridging/ProxyColony.sol:ProxyColony", + "label": "resolver", + "offset": 0, + "slot": "2", + "type": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + } + }, + { + "contract": "contracts/bridging/ProxyColony.sol:ProxyColony", + "label": "metatransactionNonces", + "offset": 0, + "slot": "3", + "type": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + }, + "label": "mapping(address => uint256)", + "numberOfBytes": "32", + "value": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + } + } + }, + { + "contract": "contracts/bridging/ProxyColony.sol:ProxyColony", + "label": "tokenBalances", + "offset": 0, + "slot": "4", + "type": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + }, + "label": "mapping(address => uint256)", + "numberOfBytes": "32", + "value": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + } + } + } + ] +} \ No newline at end of file diff --git a/.storage-layouts-normalized/contracts/bridging/ProxyColonyNetwork.sol:ProxyColonyNetwork.json b/.storage-layouts-normalized/contracts/bridging/ProxyColonyNetwork.sol:ProxyColonyNetwork.json new file mode 100644 index 0000000000..9002f3042e --- /dev/null +++ b/.storage-layouts-normalized/contracts/bridging/ProxyColonyNetwork.sol:ProxyColonyNetwork.json @@ -0,0 +1,102 @@ +{ + "storage": [ + { + "contract": "contracts/bridging/ProxyColonyNetwork.sol:ProxyColonyNetwork", + "label": "authority", + "offset": 0, + "slot": "0", + "type": { + "encoding": "inplace", + "label": "contract DSAuthority", + "numberOfBytes": "20" + } + }, + { + "contract": "contracts/bridging/ProxyColonyNetwork.sol:ProxyColonyNetwork", + "label": "owner", + "offset": 0, + "slot": "1", + "type": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + } + }, + { + "contract": "contracts/bridging/ProxyColonyNetwork.sol:ProxyColonyNetwork", + "label": "resolver", + "offset": 0, + "slot": "2", + "type": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + } + }, + { + "contract": "contracts/bridging/ProxyColonyNetwork.sol:ProxyColonyNetwork", + "label": "colonyBridgeAddress", + "offset": 0, + "slot": "3", + "type": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + } + }, + { + "contract": "contracts/bridging/ProxyColonyNetwork.sol:ProxyColonyNetwork", + "label": "homeChainId", + "offset": 0, + "slot": "4", + "type": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + } + }, + { + "contract": "contracts/bridging/ProxyColonyNetwork.sol:ProxyColonyNetwork", + "label": "proxyColonyResolverAddress", + "offset": 0, + "slot": "5", + "type": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + } + }, + { + "contract": "contracts/bridging/ProxyColonyNetwork.sol:ProxyColonyNetwork", + "label": "shellColonies", + "offset": 0, + "slot": "6", + "type": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + }, + "label": "mapping(address => bool)", + "numberOfBytes": "32", + "value": { + "encoding": "inplace", + "label": "bool", + "numberOfBytes": "1" + } + } + }, + { + "contract": "contracts/bridging/ProxyColonyNetwork.sol:ProxyColonyNetwork", + "label": "domainTokenReceiverResolver", + "offset": 0, + "slot": "7", + "type": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + } + } + ] +} \ No newline at end of file diff --git a/.storage-layouts-normalized/contracts/colony/Colony.sol:Colony.json b/.storage-layouts-normalized/contracts/colony/Colony.sol:Colony.json index b22d40a278..50b99d68dd 100644 --- a/.storage-layouts-normalized/contracts/colony/Colony.sol:Colony.json +++ b/.storage-layouts-normalized/contracts/colony/Colony.sol:Colony.json @@ -1409,6 +1409,37 @@ "numberOfBytes": "32" } } + }, + { + "contract": "contracts/colony/Colony.sol:Colony", + "label": "domainReputationTokenApprovals", + "offset": 0, + "slot": "39", + "type": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "label": "mapping(uint256 => mapping(address => uint256))", + "numberOfBytes": "32", + "value": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + }, + "label": "mapping(address => 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..6dca97394a 100644 --- a/.storage-layouts-normalized/contracts/colony/ColonyArbitraryTransaction.sol:ColonyArbitraryTransaction.json +++ b/.storage-layouts-normalized/contracts/colony/ColonyArbitraryTransaction.sol:ColonyArbitraryTransaction.json @@ -1409,6 +1409,37 @@ "numberOfBytes": "32" } } + }, + { + "contract": "contracts/colony/ColonyArbitraryTransaction.sol:ColonyArbitraryTransaction", + "label": "domainReputationTokenApprovals", + "offset": 0, + "slot": "39", + "type": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "label": "mapping(uint256 => mapping(address => uint256))", + "numberOfBytes": "32", + "value": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + }, + "label": "mapping(address => 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..e2bb76fe03 100644 --- a/.storage-layouts-normalized/contracts/colony/ColonyDomains.sol:ColonyDomains.json +++ b/.storage-layouts-normalized/contracts/colony/ColonyDomains.sol:ColonyDomains.json @@ -1409,6 +1409,37 @@ "numberOfBytes": "32" } } + }, + { + "contract": "contracts/colony/ColonyDomains.sol:ColonyDomains", + "label": "domainReputationTokenApprovals", + "offset": 0, + "slot": "39", + "type": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "label": "mapping(uint256 => mapping(address => uint256))", + "numberOfBytes": "32", + "value": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + }, + "label": "mapping(address => 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..208a7f19e3 100644 --- a/.storage-layouts-normalized/contracts/colony/ColonyExpenditure.sol:ColonyExpenditure.json +++ b/.storage-layouts-normalized/contracts/colony/ColonyExpenditure.sol:ColonyExpenditure.json @@ -1409,6 +1409,37 @@ "numberOfBytes": "32" } } + }, + { + "contract": "contracts/colony/ColonyExpenditure.sol:ColonyExpenditure", + "label": "domainReputationTokenApprovals", + "offset": 0, + "slot": "39", + "type": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "label": "mapping(uint256 => mapping(address => uint256))", + "numberOfBytes": "32", + "value": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + }, + "label": "mapping(address => 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..dcf5ab5f8c 100644 --- a/.storage-layouts-normalized/contracts/colony/ColonyFunding.sol:ColonyFunding.json +++ b/.storage-layouts-normalized/contracts/colony/ColonyFunding.sol:ColonyFunding.json @@ -1409,6 +1409,37 @@ "numberOfBytes": "32" } } + }, + { + "contract": "contracts/colony/ColonyFunding.sol:ColonyFunding", + "label": "domainReputationTokenApprovals", + "offset": 0, + "slot": "39", + "type": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "label": "mapping(uint256 => mapping(address => uint256))", + "numberOfBytes": "32", + "value": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + }, + "label": "mapping(address => 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..256ec97e87 100644 --- a/.storage-layouts-normalized/contracts/colony/ColonyRewards.sol:ColonyRewards.json +++ b/.storage-layouts-normalized/contracts/colony/ColonyRewards.sol:ColonyRewards.json @@ -1409,6 +1409,37 @@ "numberOfBytes": "32" } } + }, + { + "contract": "contracts/colony/ColonyRewards.sol:ColonyRewards", + "label": "domainReputationTokenApprovals", + "offset": 0, + "slot": "39", + "type": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "label": "mapping(uint256 => mapping(address => uint256))", + "numberOfBytes": "32", + "value": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + }, + "label": "mapping(address => 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..e06831f16a 100644 --- a/.storage-layouts-normalized/contracts/colony/ColonyRoles.sol:ColonyRoles.json +++ b/.storage-layouts-normalized/contracts/colony/ColonyRoles.sol:ColonyRoles.json @@ -1409,6 +1409,37 @@ "numberOfBytes": "32" } } + }, + { + "contract": "contracts/colony/ColonyRoles.sol:ColonyRoles", + "label": "domainReputationTokenApprovals", + "offset": 0, + "slot": "39", + "type": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "label": "mapping(uint256 => mapping(address => uint256))", + "numberOfBytes": "32", + "value": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + }, + "label": "mapping(address => 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..152acc917f 100644 --- a/.storage-layouts-normalized/contracts/colony/ColonyStorage.sol:ColonyStorage.json +++ b/.storage-layouts-normalized/contracts/colony/ColonyStorage.sol:ColonyStorage.json @@ -1409,6 +1409,37 @@ "numberOfBytes": "32" } } + }, + { + "contract": "contracts/colony/ColonyStorage.sol:ColonyStorage", + "label": "domainReputationTokenApprovals", + "offset": 0, + "slot": "39", + "type": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "label": "mapping(uint256 => mapping(address => uint256))", + "numberOfBytes": "32", + "value": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + }, + "label": "mapping(address => uint256)", + "numberOfBytes": "32", + "value": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + } + } + } } ] } \ No newline at end of file diff --git a/.storage-layouts-normalized/contracts/common/DomainReceiverManagement.sol:DomainReceiverManagement.json b/.storage-layouts-normalized/contracts/common/DomainReceiverManagement.sol:DomainReceiverManagement.json new file mode 100644 index 0000000000..82b695cebb --- /dev/null +++ b/.storage-layouts-normalized/contracts/common/DomainReceiverManagement.sol:DomainReceiverManagement.json @@ -0,0 +1,3 @@ +{ + "storage": [] +} \ No newline at end of file diff --git a/.storage-layouts-normalized/contracts/common/IsContract.sol:IsContract.json b/.storage-layouts-normalized/contracts/common/IsContract.sol:IsContract.json new file mode 100644 index 0000000000..82b695cebb --- /dev/null +++ b/.storage-layouts-normalized/contracts/common/IsContract.sol:IsContract.json @@ -0,0 +1,3 @@ +{ + "storage": [] +} \ 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..be476521be 100644 --- a/.storage-layouts-normalized/contracts/testHelpers/FunctionsNotAvailableOnColony.sol:FunctionsNotAvailableOnColony.json +++ b/.storage-layouts-normalized/contracts/testHelpers/FunctionsNotAvailableOnColony.sol:FunctionsNotAvailableOnColony.json @@ -1409,6 +1409,37 @@ "numberOfBytes": "32" } } + }, + { + "contract": "contracts/testHelpers/FunctionsNotAvailableOnColony.sol:FunctionsNotAvailableOnColony", + "label": "domainReputationTokenApprovals", + "offset": 0, + "slot": "39", + "type": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "label": "mapping(uint256 => mapping(address => uint256))", + "numberOfBytes": "32", + "value": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + }, + "label": "mapping(address => uint256)", + "numberOfBytes": "32", + "value": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + } + } + } } ] } \ No newline at end of file diff --git a/.storage-layouts-normalized/contracts/testHelpers/LiFiFacetProxyMock.sol:LiFiFacetProxyMock.json b/.storage-layouts-normalized/contracts/testHelpers/LiFiFacetProxyMock.sol:LiFiFacetProxyMock.json new file mode 100644 index 0000000000..82b695cebb --- /dev/null +++ b/.storage-layouts-normalized/contracts/testHelpers/LiFiFacetProxyMock.sol:LiFiFacetProxyMock.json @@ -0,0 +1,3 @@ +{ + "storage": [] +} \ 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..5c378b98f6 100644 --- a/.storage-layouts-normalized/contracts/testHelpers/NoLimitSubdomains.sol:NoLimitSubdomains.json +++ b/.storage-layouts-normalized/contracts/testHelpers/NoLimitSubdomains.sol:NoLimitSubdomains.json @@ -1409,6 +1409,37 @@ "numberOfBytes": "32" } } + }, + { + "contract": "contracts/testHelpers/NoLimitSubdomains.sol:NoLimitSubdomains", + "label": "domainReputationTokenApprovals", + "offset": 0, + "slot": "39", + "type": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "label": "mapping(uint256 => mapping(address => uint256))", + "numberOfBytes": "32", + "value": { + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + }, + "label": "mapping(address => uint256)", + "numberOfBytes": "32", + "value": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + } + } + } } ] } \ No newline at end of file From a1c376556d6c5b557be1ededfac0f02afe0bd1f5 Mon Sep 17 00:00:00 2001 From: Alex Rea Date: Mon, 14 Oct 2024 14:58:58 +0100 Subject: [PATCH 07/16] Improve test coverage for ColonyFunding.sol --- contracts/colony/ColonyFunding.sol | 3 +- test/contracts-network/colony-expenditure.js | 5 ++- test/contracts-network/colony-funding.js | 36 +++++++++++++++++++- test/contracts-network/colony-recovery.js | 1 + 4 files changed, 41 insertions(+), 4 deletions(-) diff --git a/contracts/colony/ColonyFunding.sol b/contracts/colony/ColonyFunding.sol index 328b0eaeb0..cf4c6f247d 100755 --- a/contracts/colony/ColonyFunding.sol +++ b/contracts/colony/ColonyFunding.sol @@ -169,6 +169,7 @@ contract ColonyFunding is ) public stoppable auth { require(domainExists(_domainId), "colony-funding-domain-does-not-exist"); require(tokenEarnsReputationOnPayout(_token), "colony-funding-token-does-not-earn-reputation"); + require(_domainId > 1, "colony-funding-root-domain"); if (_add) { domainReputationTokenApprovals[_domainId][_token] += _amount; } else { @@ -180,8 +181,6 @@ contract ColonyFunding is uint256 _domainId, address _token ) public view returns (uint256) { - require(domainExists(_domainId), "colony-funding-domain-does-not-exist"); - require(tokenEarnsReputationOnPayout(_token), "colony-funding-token-does-not-earn-reputation"); return domainReputationTokenApprovals[_domainId][_token]; } 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 82f74a1594..e9c0bb2651 100755 --- a/test/contracts-network/colony-funding.js +++ b/test/contracts-network/colony-funding.js @@ -14,6 +14,8 @@ const { SLOT0, SLOT1, SLOT2, + ROOT_ROLE, + ADDRESS_ZERO, } = require("../../helpers/constants"); const { @@ -23,7 +25,7 @@ const { setupFundedExpenditure, setupClaimedExpenditure, } = require("../../helpers/test-data-generator"); -const { getTokenArgs, checkErrorRevert, web3GetBalance, removeSubdomainLimit, expectEvent } = require("../../helpers/test-helper"); +const { getTokenArgs, checkErrorRevert, web3GetBalance, removeSubdomainLimit, expectEvent, rolesToBytes32 } = require("../../helpers/test-helper"); const { setupDomainTokenReceiverResolver } = require("../../helpers/upgradable-contracts"); const { expect } = chai; @@ -704,6 +706,38 @@ contract("Colony Funding", (accounts) => { expect(allowedReceipt).to.eq.BN(0); }); + it(`root permission is required to call editAllowedDomainTokenReceipt`, async () => { + await colony.addDomain(1, UINT256_MAX, 1); + await checkErrorRevert(colony.editAllowedDomainTokenReceipt(2, token.address, 70, true, { from: WORKER }), "ds-auth-unauthorized"); + const rootRole = rolesToBytes32([ROOT_ROLE]); + + await colony.setUserRoles(1, UINT256_MAX, WORKER, 1, rootRole); + await colony.editAllowedDomainTokenReceipt(2, token.address, 70, true, { from: WORKER }); + }); + + it(`cannot editAllowedDomainTokenReceipt for a domain that does not exist`, async () => { + await checkErrorRevert(colony.editAllowedDomainTokenReceipt(2, token.address, 70, true), "colony-funding-domain-does-not-exist"); + }); + + it(`cannot editAllowedDomainTokenReceipt for a token that does not earn reputation`, async () => { + await checkErrorRevert(colony.editAllowedDomainTokenReceipt(1, ADDRESS_ZERO, 70, true), "colony-funding-token-does-not-earn-reputation"); + }); + + it(`can add and remove allowed domain token receipts as expected`, async () => { + await colony.addDomain(1, UINT256_MAX, 1); + await colony.editAllowedDomainTokenReceipt(2, token.address, 70, true); + let allowedReceipt = await colony.getAllowedDomainTokenReceipt(2, token.address); + expect(allowedReceipt).to.eq.BN(70); + + await colony.editAllowedDomainTokenReceipt(2, token.address, 20, false); + allowedReceipt = await colony.getAllowedDomainTokenReceipt(2, token.address); + expect(allowedReceipt).to.eq.BN(50); + }); + + it(`cannot editAllowedDomainTokenReceipt for the root domain`, async () => { + await checkErrorRevert(colony.editAllowedDomainTokenReceipt(1, token.address, 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 diff --git a/test/contracts-network/colony-recovery.js b/test/contracts-network/colony-recovery.js index 38f3ac192a..f42b9d54bb 100644 --- a/test/contracts-network/colony-recovery.js +++ b/test/contracts-network/colony-recovery.js @@ -214,6 +214,7 @@ contract("Colony Recovery", (accounts) => { 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.editAllowedDomainTokenReceipt(1, ADDRESS_ZERO, 1, true), "colony-in-recovery-mode"); }); it("recovery functions should be permissioned", async () => { From 50c3fa84786c26d8dc35fb58aab5cfc86bb0084e Mon Sep 17 00:00:00 2001 From: Alex Rea Date: Mon, 14 Oct 2024 15:19:11 +0100 Subject: [PATCH 08/16] Improve logic around funding domains with tokens --- contracts/colony/ColonyFunding.sol | 33 +++++++++++++++--------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/contracts/colony/ColonyFunding.sol b/contracts/colony/ColonyFunding.sol index cf4c6f247d..369ac0d324 100755 --- a/contracts/colony/ColonyFunding.sol +++ b/contracts/colony/ColonyFunding.sol @@ -131,25 +131,26 @@ contract ColonyFunding is fundingPots[0].balance[_token] += feeToPay; - if (tokenEarnsReputationOnPayout(_token)) { - // If token earns reputation, we only allow up to the approved amount to be received - uint256 approvedAmount = domainReputationTokenApprovals[_domainId][_token]; - if (approvedAmount < remainder) { - fundingPots[fundingPotId].balance[_token] += approvedAmount; - Domain storage rootDomain = domains[1]; - // And the rest goes to the root pot - fundingPots[rootDomain.fundingPotId].balance[_token] += remainder - approvedAmount; - domainReputationTokenApprovals[_domainId][_token] = 0; - emit DomainFundsClaimed(msgSender(), _token, _domainId, feeToPay, approvedAmount); - emit ColonyFundsClaimed(msgSender(), _token, 0, remainder - approvedAmount); - } else { - fundingPots[fundingPotId].balance[_token] += remainder; + uint256 approvedAmount = domainReputationTokenApprovals[_domainId][_token]; + if (!tokenEarnsReputationOnPayout(_token) || approvedAmount >= remainder) { + // Either the token doesn't earn reputation or there is enough approval + // Either way, the domain gets all the funds + fundingPots[fundingPotId].balance[_token] += remainder; + if (tokenEarnsReputationOnPayout(_token)) { + // If it does earn reputation, deduct the approved amount domainReputationTokenApprovals[_domainId][_token] -= remainder; - emit DomainFundsClaimed(msgSender(), _token, _domainId, feeToPay, remainder); } - } else { - fundingPots[fundingPotId].balance[_token] += remainder; emit DomainFundsClaimed(msgSender(), _token, _domainId, feeToPay, remainder); + } else { + // The token earns reputation and there is not enough approvalable + // The domain gets what was approved + fundingPots[fundingPotId].balance[_token] += approvedAmount; + // And the rest goes to the root pot + Domain storage rootDomain = domains[1]; + fundingPots[rootDomain.fundingPotId].balance[_token] += remainder - approvedAmount; + domainReputationTokenApprovals[_domainId][_token] = 0; + emit DomainFundsClaimed(msgSender(), _token, _domainId, feeToPay, approvedAmount); + emit ColonyFundsClaimed(msgSender(), _token, 0, remainder - approvedAmount); } // Claim funds From 50f8c81cec9f63a0e398e4fd26f9019ab176920a Mon Sep 17 00:00:00 2001 From: Alex Rea Date: Mon, 21 Oct 2024 11:58:52 +0100 Subject: [PATCH 09/16] Set up storage for direct domain funding in anticipation of proxy colonies --- .../bridging/ProxyColony.sol:ProxyColony.json | 79 -------------- ...yColonyNetwork.sol:ProxyColonyNetwork.json | 102 ------------------ .../contracts/colony/Colony.sol:Colony.json | 24 +++-- ...action.sol:ColonyArbitraryTransaction.json | 24 +++-- .../ColonyDomains.sol:ColonyDomains.json | 24 +++-- ...lonyExpenditure.sol:ColonyExpenditure.json | 24 +++-- .../ColonyFunding.sol:ColonyFunding.json | 24 +++-- .../ColonyRewards.sol:ColonyRewards.json | 24 +++-- .../colony/ColonyRoles.sol:ColonyRoles.json | 24 +++-- .../ColonyStorage.sol:ColonyStorage.json | 24 +++-- ...nagement.sol:DomainReceiverManagement.json | 3 - .../common/IsContract.sol:IsContract.json | 3 - ...ony.sol:FunctionsNotAvailableOnColony.json | 24 +++-- ...FacetProxyMock.sol:LiFiFacetProxyMock.json | 3 - ...LimitSubdomains.sol:NoLimitSubdomains.json | 24 +++-- contracts/colony/ColonyFunding.sol | 12 +-- contracts/colony/ColonyStorage.sol | 4 +- 17 files changed, 178 insertions(+), 268 deletions(-) delete mode 100644 .storage-layouts-normalized/contracts/bridging/ProxyColony.sol:ProxyColony.json delete mode 100644 .storage-layouts-normalized/contracts/bridging/ProxyColonyNetwork.sol:ProxyColonyNetwork.json delete mode 100644 .storage-layouts-normalized/contracts/common/DomainReceiverManagement.sol:DomainReceiverManagement.json delete mode 100644 .storage-layouts-normalized/contracts/common/IsContract.sol:IsContract.json delete mode 100644 .storage-layouts-normalized/contracts/testHelpers/LiFiFacetProxyMock.sol:LiFiFacetProxyMock.json diff --git a/.storage-layouts-normalized/contracts/bridging/ProxyColony.sol:ProxyColony.json b/.storage-layouts-normalized/contracts/bridging/ProxyColony.sol:ProxyColony.json deleted file mode 100644 index f020621b32..0000000000 --- a/.storage-layouts-normalized/contracts/bridging/ProxyColony.sol:ProxyColony.json +++ /dev/null @@ -1,79 +0,0 @@ -{ - "storage": [ - { - "contract": "contracts/bridging/ProxyColony.sol:ProxyColony", - "label": "authority", - "offset": 0, - "slot": "0", - "type": { - "encoding": "inplace", - "label": "contract DSAuthority", - "numberOfBytes": "20" - } - }, - { - "contract": "contracts/bridging/ProxyColony.sol:ProxyColony", - "label": "owner", - "offset": 0, - "slot": "1", - "type": { - "encoding": "inplace", - "label": "address", - "numberOfBytes": "20" - } - }, - { - "contract": "contracts/bridging/ProxyColony.sol:ProxyColony", - "label": "resolver", - "offset": 0, - "slot": "2", - "type": { - "encoding": "inplace", - "label": "address", - "numberOfBytes": "20" - } - }, - { - "contract": "contracts/bridging/ProxyColony.sol:ProxyColony", - "label": "metatransactionNonces", - "offset": 0, - "slot": "3", - "type": { - "encoding": "mapping", - "key": { - "encoding": "inplace", - "label": "address", - "numberOfBytes": "20" - }, - "label": "mapping(address => uint256)", - "numberOfBytes": "32", - "value": { - "encoding": "inplace", - "label": "uint256", - "numberOfBytes": "32" - } - } - }, - { - "contract": "contracts/bridging/ProxyColony.sol:ProxyColony", - "label": "tokenBalances", - "offset": 0, - "slot": "4", - "type": { - "encoding": "mapping", - "key": { - "encoding": "inplace", - "label": "address", - "numberOfBytes": "20" - }, - "label": "mapping(address => uint256)", - "numberOfBytes": "32", - "value": { - "encoding": "inplace", - "label": "uint256", - "numberOfBytes": "32" - } - } - } - ] -} \ No newline at end of file diff --git a/.storage-layouts-normalized/contracts/bridging/ProxyColonyNetwork.sol:ProxyColonyNetwork.json b/.storage-layouts-normalized/contracts/bridging/ProxyColonyNetwork.sol:ProxyColonyNetwork.json deleted file mode 100644 index 9002f3042e..0000000000 --- a/.storage-layouts-normalized/contracts/bridging/ProxyColonyNetwork.sol:ProxyColonyNetwork.json +++ /dev/null @@ -1,102 +0,0 @@ -{ - "storage": [ - { - "contract": "contracts/bridging/ProxyColonyNetwork.sol:ProxyColonyNetwork", - "label": "authority", - "offset": 0, - "slot": "0", - "type": { - "encoding": "inplace", - "label": "contract DSAuthority", - "numberOfBytes": "20" - } - }, - { - "contract": "contracts/bridging/ProxyColonyNetwork.sol:ProxyColonyNetwork", - "label": "owner", - "offset": 0, - "slot": "1", - "type": { - "encoding": "inplace", - "label": "address", - "numberOfBytes": "20" - } - }, - { - "contract": "contracts/bridging/ProxyColonyNetwork.sol:ProxyColonyNetwork", - "label": "resolver", - "offset": 0, - "slot": "2", - "type": { - "encoding": "inplace", - "label": "address", - "numberOfBytes": "20" - } - }, - { - "contract": "contracts/bridging/ProxyColonyNetwork.sol:ProxyColonyNetwork", - "label": "colonyBridgeAddress", - "offset": 0, - "slot": "3", - "type": { - "encoding": "inplace", - "label": "address", - "numberOfBytes": "20" - } - }, - { - "contract": "contracts/bridging/ProxyColonyNetwork.sol:ProxyColonyNetwork", - "label": "homeChainId", - "offset": 0, - "slot": "4", - "type": { - "encoding": "inplace", - "label": "uint256", - "numberOfBytes": "32" - } - }, - { - "contract": "contracts/bridging/ProxyColonyNetwork.sol:ProxyColonyNetwork", - "label": "proxyColonyResolverAddress", - "offset": 0, - "slot": "5", - "type": { - "encoding": "inplace", - "label": "address", - "numberOfBytes": "20" - } - }, - { - "contract": "contracts/bridging/ProxyColonyNetwork.sol:ProxyColonyNetwork", - "label": "shellColonies", - "offset": 0, - "slot": "6", - "type": { - "encoding": "mapping", - "key": { - "encoding": "inplace", - "label": "address", - "numberOfBytes": "20" - }, - "label": "mapping(address => bool)", - "numberOfBytes": "32", - "value": { - "encoding": "inplace", - "label": "bool", - "numberOfBytes": "1" - } - } - }, - { - "contract": "contracts/bridging/ProxyColonyNetwork.sol:ProxyColonyNetwork", - "label": "domainTokenReceiverResolver", - "offset": 0, - "slot": "7", - "type": { - "encoding": "inplace", - "label": "address", - "numberOfBytes": "20" - } - } - ] -} \ No newline at end of file diff --git a/.storage-layouts-normalized/contracts/colony/Colony.sol:Colony.json b/.storage-layouts-normalized/contracts/colony/Colony.sol:Colony.json index 50b99d68dd..eca53379ed 100644 --- a/.storage-layouts-normalized/contracts/colony/Colony.sol:Colony.json +++ b/.storage-layouts-normalized/contracts/colony/Colony.sol:Colony.json @@ -1422,21 +1422,31 @@ "label": "uint256", "numberOfBytes": "32" }, - "label": "mapping(uint256 => mapping(address => uint256))", + "label": "mapping(uint256 => mapping(uint256 => mapping(address => uint256)))", "numberOfBytes": "32", "value": { "encoding": "mapping", "key": { "encoding": "inplace", - "label": "address", - "numberOfBytes": "20" + "label": "uint256", + "numberOfBytes": "32" }, - "label": "mapping(address => uint256)", + "label": "mapping(uint256 => mapping(address => uint256))", "numberOfBytes": "32", "value": { - "encoding": "inplace", - "label": "uint256", - "numberOfBytes": "32" + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + }, + "label": "mapping(address => uint256)", + "numberOfBytes": "32", + "value": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + } } } } diff --git a/.storage-layouts-normalized/contracts/colony/ColonyArbitraryTransaction.sol:ColonyArbitraryTransaction.json b/.storage-layouts-normalized/contracts/colony/ColonyArbitraryTransaction.sol:ColonyArbitraryTransaction.json index 6dca97394a..9cc05a7bf8 100644 --- a/.storage-layouts-normalized/contracts/colony/ColonyArbitraryTransaction.sol:ColonyArbitraryTransaction.json +++ b/.storage-layouts-normalized/contracts/colony/ColonyArbitraryTransaction.sol:ColonyArbitraryTransaction.json @@ -1422,21 +1422,31 @@ "label": "uint256", "numberOfBytes": "32" }, - "label": "mapping(uint256 => mapping(address => uint256))", + "label": "mapping(uint256 => mapping(uint256 => mapping(address => uint256)))", "numberOfBytes": "32", "value": { "encoding": "mapping", "key": { "encoding": "inplace", - "label": "address", - "numberOfBytes": "20" + "label": "uint256", + "numberOfBytes": "32" }, - "label": "mapping(address => uint256)", + "label": "mapping(uint256 => mapping(address => uint256))", "numberOfBytes": "32", "value": { - "encoding": "inplace", - "label": "uint256", - "numberOfBytes": "32" + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + }, + "label": "mapping(address => uint256)", + "numberOfBytes": "32", + "value": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + } } } } diff --git a/.storage-layouts-normalized/contracts/colony/ColonyDomains.sol:ColonyDomains.json b/.storage-layouts-normalized/contracts/colony/ColonyDomains.sol:ColonyDomains.json index e2bb76fe03..81b92baa39 100644 --- a/.storage-layouts-normalized/contracts/colony/ColonyDomains.sol:ColonyDomains.json +++ b/.storage-layouts-normalized/contracts/colony/ColonyDomains.sol:ColonyDomains.json @@ -1422,21 +1422,31 @@ "label": "uint256", "numberOfBytes": "32" }, - "label": "mapping(uint256 => mapping(address => uint256))", + "label": "mapping(uint256 => mapping(uint256 => mapping(address => uint256)))", "numberOfBytes": "32", "value": { "encoding": "mapping", "key": { "encoding": "inplace", - "label": "address", - "numberOfBytes": "20" + "label": "uint256", + "numberOfBytes": "32" }, - "label": "mapping(address => uint256)", + "label": "mapping(uint256 => mapping(address => uint256))", "numberOfBytes": "32", "value": { - "encoding": "inplace", - "label": "uint256", - "numberOfBytes": "32" + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + }, + "label": "mapping(address => uint256)", + "numberOfBytes": "32", + "value": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + } } } } diff --git a/.storage-layouts-normalized/contracts/colony/ColonyExpenditure.sol:ColonyExpenditure.json b/.storage-layouts-normalized/contracts/colony/ColonyExpenditure.sol:ColonyExpenditure.json index 208a7f19e3..471700cb87 100644 --- a/.storage-layouts-normalized/contracts/colony/ColonyExpenditure.sol:ColonyExpenditure.json +++ b/.storage-layouts-normalized/contracts/colony/ColonyExpenditure.sol:ColonyExpenditure.json @@ -1422,21 +1422,31 @@ "label": "uint256", "numberOfBytes": "32" }, - "label": "mapping(uint256 => mapping(address => uint256))", + "label": "mapping(uint256 => mapping(uint256 => mapping(address => uint256)))", "numberOfBytes": "32", "value": { "encoding": "mapping", "key": { "encoding": "inplace", - "label": "address", - "numberOfBytes": "20" + "label": "uint256", + "numberOfBytes": "32" }, - "label": "mapping(address => uint256)", + "label": "mapping(uint256 => mapping(address => uint256))", "numberOfBytes": "32", "value": { - "encoding": "inplace", - "label": "uint256", - "numberOfBytes": "32" + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + }, + "label": "mapping(address => uint256)", + "numberOfBytes": "32", + "value": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + } } } } diff --git a/.storage-layouts-normalized/contracts/colony/ColonyFunding.sol:ColonyFunding.json b/.storage-layouts-normalized/contracts/colony/ColonyFunding.sol:ColonyFunding.json index dcf5ab5f8c..5fb3dad1ac 100644 --- a/.storage-layouts-normalized/contracts/colony/ColonyFunding.sol:ColonyFunding.json +++ b/.storage-layouts-normalized/contracts/colony/ColonyFunding.sol:ColonyFunding.json @@ -1422,21 +1422,31 @@ "label": "uint256", "numberOfBytes": "32" }, - "label": "mapping(uint256 => mapping(address => uint256))", + "label": "mapping(uint256 => mapping(uint256 => mapping(address => uint256)))", "numberOfBytes": "32", "value": { "encoding": "mapping", "key": { "encoding": "inplace", - "label": "address", - "numberOfBytes": "20" + "label": "uint256", + "numberOfBytes": "32" }, - "label": "mapping(address => uint256)", + "label": "mapping(uint256 => mapping(address => uint256))", "numberOfBytes": "32", "value": { - "encoding": "inplace", - "label": "uint256", - "numberOfBytes": "32" + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + }, + "label": "mapping(address => uint256)", + "numberOfBytes": "32", + "value": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + } } } } diff --git a/.storage-layouts-normalized/contracts/colony/ColonyRewards.sol:ColonyRewards.json b/.storage-layouts-normalized/contracts/colony/ColonyRewards.sol:ColonyRewards.json index 256ec97e87..d7ad274434 100644 --- a/.storage-layouts-normalized/contracts/colony/ColonyRewards.sol:ColonyRewards.json +++ b/.storage-layouts-normalized/contracts/colony/ColonyRewards.sol:ColonyRewards.json @@ -1422,21 +1422,31 @@ "label": "uint256", "numberOfBytes": "32" }, - "label": "mapping(uint256 => mapping(address => uint256))", + "label": "mapping(uint256 => mapping(uint256 => mapping(address => uint256)))", "numberOfBytes": "32", "value": { "encoding": "mapping", "key": { "encoding": "inplace", - "label": "address", - "numberOfBytes": "20" + "label": "uint256", + "numberOfBytes": "32" }, - "label": "mapping(address => uint256)", + "label": "mapping(uint256 => mapping(address => uint256))", "numberOfBytes": "32", "value": { - "encoding": "inplace", - "label": "uint256", - "numberOfBytes": "32" + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + }, + "label": "mapping(address => uint256)", + "numberOfBytes": "32", + "value": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + } } } } diff --git a/.storage-layouts-normalized/contracts/colony/ColonyRoles.sol:ColonyRoles.json b/.storage-layouts-normalized/contracts/colony/ColonyRoles.sol:ColonyRoles.json index e06831f16a..2b9e084351 100644 --- a/.storage-layouts-normalized/contracts/colony/ColonyRoles.sol:ColonyRoles.json +++ b/.storage-layouts-normalized/contracts/colony/ColonyRoles.sol:ColonyRoles.json @@ -1422,21 +1422,31 @@ "label": "uint256", "numberOfBytes": "32" }, - "label": "mapping(uint256 => mapping(address => uint256))", + "label": "mapping(uint256 => mapping(uint256 => mapping(address => uint256)))", "numberOfBytes": "32", "value": { "encoding": "mapping", "key": { "encoding": "inplace", - "label": "address", - "numberOfBytes": "20" + "label": "uint256", + "numberOfBytes": "32" }, - "label": "mapping(address => uint256)", + "label": "mapping(uint256 => mapping(address => uint256))", "numberOfBytes": "32", "value": { - "encoding": "inplace", - "label": "uint256", - "numberOfBytes": "32" + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + }, + "label": "mapping(address => uint256)", + "numberOfBytes": "32", + "value": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + } } } } diff --git a/.storage-layouts-normalized/contracts/colony/ColonyStorage.sol:ColonyStorage.json b/.storage-layouts-normalized/contracts/colony/ColonyStorage.sol:ColonyStorage.json index 152acc917f..cd14be9e98 100644 --- a/.storage-layouts-normalized/contracts/colony/ColonyStorage.sol:ColonyStorage.json +++ b/.storage-layouts-normalized/contracts/colony/ColonyStorage.sol:ColonyStorage.json @@ -1422,21 +1422,31 @@ "label": "uint256", "numberOfBytes": "32" }, - "label": "mapping(uint256 => mapping(address => uint256))", + "label": "mapping(uint256 => mapping(uint256 => mapping(address => uint256)))", "numberOfBytes": "32", "value": { "encoding": "mapping", "key": { "encoding": "inplace", - "label": "address", - "numberOfBytes": "20" + "label": "uint256", + "numberOfBytes": "32" }, - "label": "mapping(address => uint256)", + "label": "mapping(uint256 => mapping(address => uint256))", "numberOfBytes": "32", "value": { - "encoding": "inplace", - "label": "uint256", - "numberOfBytes": "32" + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + }, + "label": "mapping(address => uint256)", + "numberOfBytes": "32", + "value": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + } } } } diff --git a/.storage-layouts-normalized/contracts/common/DomainReceiverManagement.sol:DomainReceiverManagement.json b/.storage-layouts-normalized/contracts/common/DomainReceiverManagement.sol:DomainReceiverManagement.json deleted file mode 100644 index 82b695cebb..0000000000 --- a/.storage-layouts-normalized/contracts/common/DomainReceiverManagement.sol:DomainReceiverManagement.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "storage": [] -} \ No newline at end of file diff --git a/.storage-layouts-normalized/contracts/common/IsContract.sol:IsContract.json b/.storage-layouts-normalized/contracts/common/IsContract.sol:IsContract.json deleted file mode 100644 index 82b695cebb..0000000000 --- a/.storage-layouts-normalized/contracts/common/IsContract.sol:IsContract.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "storage": [] -} \ 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 be476521be..8fe2849b99 100644 --- a/.storage-layouts-normalized/contracts/testHelpers/FunctionsNotAvailableOnColony.sol:FunctionsNotAvailableOnColony.json +++ b/.storage-layouts-normalized/contracts/testHelpers/FunctionsNotAvailableOnColony.sol:FunctionsNotAvailableOnColony.json @@ -1422,21 +1422,31 @@ "label": "uint256", "numberOfBytes": "32" }, - "label": "mapping(uint256 => mapping(address => uint256))", + "label": "mapping(uint256 => mapping(uint256 => mapping(address => uint256)))", "numberOfBytes": "32", "value": { "encoding": "mapping", "key": { "encoding": "inplace", - "label": "address", - "numberOfBytes": "20" + "label": "uint256", + "numberOfBytes": "32" }, - "label": "mapping(address => uint256)", + "label": "mapping(uint256 => mapping(address => uint256))", "numberOfBytes": "32", "value": { - "encoding": "inplace", - "label": "uint256", - "numberOfBytes": "32" + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + }, + "label": "mapping(address => uint256)", + "numberOfBytes": "32", + "value": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + } } } } diff --git a/.storage-layouts-normalized/contracts/testHelpers/LiFiFacetProxyMock.sol:LiFiFacetProxyMock.json b/.storage-layouts-normalized/contracts/testHelpers/LiFiFacetProxyMock.sol:LiFiFacetProxyMock.json deleted file mode 100644 index 82b695cebb..0000000000 --- a/.storage-layouts-normalized/contracts/testHelpers/LiFiFacetProxyMock.sol:LiFiFacetProxyMock.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "storage": [] -} \ 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 5c378b98f6..6227bf3d8c 100644 --- a/.storage-layouts-normalized/contracts/testHelpers/NoLimitSubdomains.sol:NoLimitSubdomains.json +++ b/.storage-layouts-normalized/contracts/testHelpers/NoLimitSubdomains.sol:NoLimitSubdomains.json @@ -1422,21 +1422,31 @@ "label": "uint256", "numberOfBytes": "32" }, - "label": "mapping(uint256 => mapping(address => uint256))", + "label": "mapping(uint256 => mapping(uint256 => mapping(address => uint256)))", "numberOfBytes": "32", "value": { "encoding": "mapping", "key": { "encoding": "inplace", - "label": "address", - "numberOfBytes": "20" + "label": "uint256", + "numberOfBytes": "32" }, - "label": "mapping(address => uint256)", + "label": "mapping(uint256 => mapping(address => uint256))", "numberOfBytes": "32", "value": { - "encoding": "inplace", - "label": "uint256", - "numberOfBytes": "32" + "encoding": "mapping", + "key": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + }, + "label": "mapping(address => uint256)", + "numberOfBytes": "32", + "value": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + } } } } diff --git a/contracts/colony/ColonyFunding.sol b/contracts/colony/ColonyFunding.sol index 369ac0d324..065e184394 100755 --- a/contracts/colony/ColonyFunding.sol +++ b/contracts/colony/ColonyFunding.sol @@ -131,14 +131,14 @@ contract ColonyFunding is fundingPots[0].balance[_token] += feeToPay; - uint256 approvedAmount = domainReputationTokenApprovals[_domainId][_token]; + uint256 approvedAmount = domainReputationTokenApprovals[block.chainid][_domainId][_token]; if (!tokenEarnsReputationOnPayout(_token) || approvedAmount >= remainder) { // Either the token doesn't earn reputation or there is enough approval // Either way, the domain gets all the funds fundingPots[fundingPotId].balance[_token] += remainder; if (tokenEarnsReputationOnPayout(_token)) { // If it does earn reputation, deduct the approved amount - domainReputationTokenApprovals[_domainId][_token] -= remainder; + domainReputationTokenApprovals[block.chainid][_domainId][_token] -= remainder; } emit DomainFundsClaimed(msgSender(), _token, _domainId, feeToPay, remainder); } else { @@ -148,7 +148,7 @@ contract ColonyFunding is // And the rest goes to the root pot Domain storage rootDomain = domains[1]; fundingPots[rootDomain.fundingPotId].balance[_token] += remainder - approvedAmount; - domainReputationTokenApprovals[_domainId][_token] = 0; + domainReputationTokenApprovals[block.chainid][_domainId][_token] = 0; emit DomainFundsClaimed(msgSender(), _token, _domainId, feeToPay, approvedAmount); emit ColonyFundsClaimed(msgSender(), _token, 0, remainder - approvedAmount); } @@ -172,9 +172,9 @@ contract ColonyFunding is require(tokenEarnsReputationOnPayout(_token), "colony-funding-token-does-not-earn-reputation"); require(_domainId > 1, "colony-funding-root-domain"); if (_add) { - domainReputationTokenApprovals[_domainId][_token] += _amount; + domainReputationTokenApprovals[block.chainid][_domainId][_token] += _amount; } else { - domainReputationTokenApprovals[_domainId][_token] -= _amount; + domainReputationTokenApprovals[block.chainid][_domainId][_token] -= _amount; } } @@ -182,7 +182,7 @@ contract ColonyFunding is uint256 _domainId, address _token ) public view returns (uint256) { - return domainReputationTokenApprovals[_domainId][_token]; + return domainReputationTokenApprovals[block.chainid][_domainId][_token]; } function getNonRewardPotsTotal(address _token) public view returns (uint256) { diff --git a/contracts/colony/ColonyStorage.sol b/contracts/colony/ColonyStorage.sol index e0971c4c2b..5b075e7ebe 100755 --- a/contracts/colony/ColonyStorage.sol +++ b/contracts/colony/ColonyStorage.sol @@ -114,8 +114,8 @@ contract ColonyStorage is ColonyDataTypes, ColonyNetworkDataTypes, DSMath, Commo mapping(uint256 => bool) DEPRECATED_localSkills; // Storage slot 37 mapping(uint256 => LocalSkill) localSkills; // Storage slot 38 - // Mapping of domain id => token address => approval to receive if reputation-earning - mapping(uint256 => mapping(address => uint256)) domainReputationTokenApprovals; // Storage slot 39 + // Mapping of domain id => chainid => token address => approval to receive if reputation-earning + mapping(uint256 => mapping(uint256 => mapping(address => uint256))) domainReputationTokenApprovals; // Storage slot 39 // Constants From b1be3c929566ad3c87fdf29774dbd4b8315140fc Mon Sep 17 00:00:00 2001 From: Alex Rea Date: Wed, 23 Oct 2024 10:17:44 +0100 Subject: [PATCH 10/16] Directly fund domains review changes --- contracts/colony/ColonyFunding.sol | 30 ++++++++----------- .../colonyNetwork/ColonyNetworkDeployer.sol | 16 +++++----- contracts/common/DomainTokenReceiver.sol | 6 ++-- 3 files changed, 23 insertions(+), 29 deletions(-) diff --git a/contracts/colony/ColonyFunding.sol b/contracts/colony/ColonyFunding.sol index 065e184394..1616d2b3fe 100755 --- a/contracts/colony/ColonyFunding.sol +++ b/contracts/colony/ColonyFunding.sol @@ -132,25 +132,21 @@ contract ColonyFunding is fundingPots[0].balance[_token] += feeToPay; uint256 approvedAmount = domainReputationTokenApprovals[block.chainid][_domainId][_token]; - if (!tokenEarnsReputationOnPayout(_token) || approvedAmount >= remainder) { - // Either the token doesn't earn reputation or there is enough approval - // Either way, the domain gets all the funds - fundingPots[fundingPotId].balance[_token] += remainder; - if (tokenEarnsReputationOnPayout(_token)) { - // If it does earn reputation, deduct the approved amount - domainReputationTokenApprovals[block.chainid][_domainId][_token] -= remainder; + + if (tokenEarnsReputationOnPayout(_token)) { + uint256 transferrableAmount = min(approvedAmount, remainder); + uint256 untransferrableAmount = remainder - transferrableAmount; + + fundingPots[fundingPotId].balance[_token] += transferrableAmount; + domainReputationTokenApprovals[block.chainid][_domainId][_token] -= transferrableAmount; + emit DomainFundsClaimed(msgSender(), _token, _domainId, feeToPay, transferrableAmount); + if (untransferrableAmount > 0) { + fundingPots[domains[1].fundingPotId].balance[_token] += untransferrableAmount; + emit ColonyFundsClaimed(msgSender(), _token, 0, untransferrableAmount); } - emit DomainFundsClaimed(msgSender(), _token, _domainId, feeToPay, remainder); } else { - // The token earns reputation and there is not enough approvalable - // The domain gets what was approved - fundingPots[fundingPotId].balance[_token] += approvedAmount; - // And the rest goes to the root pot - Domain storage rootDomain = domains[1]; - fundingPots[rootDomain.fundingPotId].balance[_token] += remainder - approvedAmount; - domainReputationTokenApprovals[block.chainid][_domainId][_token] = 0; - emit DomainFundsClaimed(msgSender(), _token, _domainId, feeToPay, approvedAmount); - emit ColonyFundsClaimed(msgSender(), _token, 0, remainder - approvedAmount); + fundingPots[fundingPotId].balance[_token] += remainder; + emit DomainFundsClaimed(msgSender(), _token, _domainId, feeToPay, remainder); } // Claim funds diff --git a/contracts/colonyNetwork/ColonyNetworkDeployer.sol b/contracts/colonyNetwork/ColonyNetworkDeployer.sol index 61fcf5941a..7406e1ee06 100644 --- a/contracts/colonyNetwork/ColonyNetworkDeployer.sol +++ b/contracts/colonyNetwork/ColonyNetworkDeployer.sol @@ -220,14 +220,6 @@ contract ColonyNetworkDeployer is ColonyNetworkStorage { return domainTokenReceiverAddress; } - function isContract(address addr) internal view returns (bool) { - uint256 size; - assembly { - size := extcodesize(addr) - } - return size > 0; - } - function getDomainTokenReceiverAddress( address _colony, uint256 _domainId @@ -332,4 +324,12 @@ contract ColonyNetworkDeployer is ColonyNetworkStorage { ); return address(etherRouter); } + + function isContract(address addr) internal view returns (bool) { + uint256 size; + assembly { + size := extcodesize(addr) + } + return size > 0; + } } diff --git a/contracts/common/DomainTokenReceiver.sol b/contracts/common/DomainTokenReceiver.sol index b85561b95a..ccfa8ccdfc 100644 --- a/contracts/common/DomainTokenReceiver.sol +++ b/contracts/common/DomainTokenReceiver.sol @@ -40,11 +40,9 @@ contract DomainTokenReceiver is DSAuth { payable(colony).transfer(address(this).balance); return; } else { + uint256 balanceToTransfer = ERC20Extended(tokenAddress).balanceOf(address(this)); require( - ERC20Extended(tokenAddress).transfer( - colony, - ERC20Extended(tokenAddress).balanceOf(address(this)) - ), + ERC20Extended(tokenAddress).transfer(colony, balanceToTransfer), "domain-token-receiver-transfer-failed" ); } From 73f71ea91654c39496261348d064b90762de9d40 Mon Sep 17 00:00:00 2001 From: Alex Rea Date: Thu, 24 Oct 2024 09:50:38 +0100 Subject: [PATCH 11/16] Set colonyAddress for receiver on deployment once only --- .../colonyNetwork/ColonyNetworkDeployer.sol | 26 +++++++++---------- contracts/common/DomainTokenReceiver.sol | 1 + test/contracts-network/colony-funding.js | 9 +++++++ 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/contracts/colonyNetwork/ColonyNetworkDeployer.sol b/contracts/colonyNetwork/ColonyNetworkDeployer.sol index 7406e1ee06..9afec8d524 100644 --- a/contracts/colonyNetwork/ColonyNetworkDeployer.sol +++ b/contracts/colonyNetwork/ColonyNetworkDeployer.sol @@ -200,21 +200,21 @@ contract ColonyNetworkDeployer is ColonyNetworkStorage { deployedAddress == domainTokenReceiverAddress, "colony-network-domain-receiver-deploy-wrong-address" ); - } - - // 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"); - } - // Check it's set up correctly - - if (DomainTokenReceiver(domainTokenReceiverAddress).getColonyAddress() != msgSender()) { + // 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; diff --git a/contracts/common/DomainTokenReceiver.sol b/contracts/common/DomainTokenReceiver.sol index ccfa8ccdfc..6ef24d3576 100644 --- a/contracts/common/DomainTokenReceiver.sol +++ b/contracts/common/DomainTokenReceiver.sol @@ -30,6 +30,7 @@ contract DomainTokenReceiver is DSAuth { } function setColonyAddress(address _colony) public auth { + require(colony == address(0), "domain-token-receiver-colony-already-set"); colony = _colony; } diff --git a/test/contracts-network/colony-funding.js b/test/contracts-network/colony-funding.js index e9c0bb2651..811cb3525e 100755 --- a/test/contracts-network/colony-funding.js +++ b/test/contracts-network/colony-funding.js @@ -612,6 +612,15 @@ contract("Colony Funding", (accounts) => { 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); From 9e215ca1d12895b1f2966be8fd9c2a45c41f5b12 Mon Sep 17 00:00:00 2001 From: Alex Rea Date: Thu, 24 Oct 2024 10:49:57 +0100 Subject: [PATCH 12/16] Receiving reputation-earning tokens is denominated in reputation, not tokens --- .../contracts/colony/Colony.sol:Colony.json | 30 +++------------ ...action.sol:ColonyArbitraryTransaction.json | 30 +++------------ .../ColonyDomains.sol:ColonyDomains.json | 30 +++------------ ...lonyExpenditure.sol:ColonyExpenditure.json | 30 +++------------ .../ColonyFunding.sol:ColonyFunding.json | 30 +++------------ .../ColonyRewards.sol:ColonyRewards.json | 30 +++------------ .../colony/ColonyRoles.sol:ColonyRoles.json | 30 +++------------ .../ColonyStorage.sol:ColonyStorage.json | 30 +++------------ ...ony.sol:FunctionsNotAvailableOnColony.json | 30 +++------------ ...LimitSubdomains.sol:NoLimitSubdomains.json | 30 +++------------ contracts/colony/Colony.sol | 2 +- contracts/colony/ColonyAuthority.sol | 2 +- contracts/colony/ColonyFunding.sol | 19 ++++------ contracts/colony/ColonyStorage.sol | 4 +- contracts/colony/IColony.sol | 10 +---- docs/interfaces/icolony.md | 6 +-- test/contracts-network/colony-funding.js | 38 +++++++++---------- test/contracts-network/colony-recovery.js | 2 +- 18 files changed, 83 insertions(+), 300 deletions(-) diff --git a/.storage-layouts-normalized/contracts/colony/Colony.sol:Colony.json b/.storage-layouts-normalized/contracts/colony/Colony.sol:Colony.json index eca53379ed..bdaaa40296 100644 --- a/.storage-layouts-normalized/contracts/colony/Colony.sol:Colony.json +++ b/.storage-layouts-normalized/contracts/colony/Colony.sol:Colony.json @@ -1412,7 +1412,7 @@ }, { "contract": "contracts/colony/Colony.sol:Colony", - "label": "domainReputationTokenApprovals", + "label": "domainReputationApproval", "offset": 0, "slot": "39", "type": { @@ -1422,32 +1422,12 @@ "label": "uint256", "numberOfBytes": "32" }, - "label": "mapping(uint256 => mapping(uint256 => mapping(address => uint256)))", + "label": "mapping(uint256 => uint256)", "numberOfBytes": "32", "value": { - "encoding": "mapping", - "key": { - "encoding": "inplace", - "label": "uint256", - "numberOfBytes": "32" - }, - "label": "mapping(uint256 => mapping(address => uint256))", - "numberOfBytes": "32", - "value": { - "encoding": "mapping", - "key": { - "encoding": "inplace", - "label": "address", - "numberOfBytes": "20" - }, - "label": "mapping(address => uint256)", - "numberOfBytes": "32", - "value": { - "encoding": "inplace", - "label": "uint256", - "numberOfBytes": "32" - } - } + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" } } } diff --git a/.storage-layouts-normalized/contracts/colony/ColonyArbitraryTransaction.sol:ColonyArbitraryTransaction.json b/.storage-layouts-normalized/contracts/colony/ColonyArbitraryTransaction.sol:ColonyArbitraryTransaction.json index 9cc05a7bf8..44da3bf9d0 100644 --- a/.storage-layouts-normalized/contracts/colony/ColonyArbitraryTransaction.sol:ColonyArbitraryTransaction.json +++ b/.storage-layouts-normalized/contracts/colony/ColonyArbitraryTransaction.sol:ColonyArbitraryTransaction.json @@ -1412,7 +1412,7 @@ }, { "contract": "contracts/colony/ColonyArbitraryTransaction.sol:ColonyArbitraryTransaction", - "label": "domainReputationTokenApprovals", + "label": "domainReputationApproval", "offset": 0, "slot": "39", "type": { @@ -1422,32 +1422,12 @@ "label": "uint256", "numberOfBytes": "32" }, - "label": "mapping(uint256 => mapping(uint256 => mapping(address => uint256)))", + "label": "mapping(uint256 => uint256)", "numberOfBytes": "32", "value": { - "encoding": "mapping", - "key": { - "encoding": "inplace", - "label": "uint256", - "numberOfBytes": "32" - }, - "label": "mapping(uint256 => mapping(address => uint256))", - "numberOfBytes": "32", - "value": { - "encoding": "mapping", - "key": { - "encoding": "inplace", - "label": "address", - "numberOfBytes": "20" - }, - "label": "mapping(address => uint256)", - "numberOfBytes": "32", - "value": { - "encoding": "inplace", - "label": "uint256", - "numberOfBytes": "32" - } - } + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" } } } diff --git a/.storage-layouts-normalized/contracts/colony/ColonyDomains.sol:ColonyDomains.json b/.storage-layouts-normalized/contracts/colony/ColonyDomains.sol:ColonyDomains.json index 81b92baa39..b35aaabbd4 100644 --- a/.storage-layouts-normalized/contracts/colony/ColonyDomains.sol:ColonyDomains.json +++ b/.storage-layouts-normalized/contracts/colony/ColonyDomains.sol:ColonyDomains.json @@ -1412,7 +1412,7 @@ }, { "contract": "contracts/colony/ColonyDomains.sol:ColonyDomains", - "label": "domainReputationTokenApprovals", + "label": "domainReputationApproval", "offset": 0, "slot": "39", "type": { @@ -1422,32 +1422,12 @@ "label": "uint256", "numberOfBytes": "32" }, - "label": "mapping(uint256 => mapping(uint256 => mapping(address => uint256)))", + "label": "mapping(uint256 => uint256)", "numberOfBytes": "32", "value": { - "encoding": "mapping", - "key": { - "encoding": "inplace", - "label": "uint256", - "numberOfBytes": "32" - }, - "label": "mapping(uint256 => mapping(address => uint256))", - "numberOfBytes": "32", - "value": { - "encoding": "mapping", - "key": { - "encoding": "inplace", - "label": "address", - "numberOfBytes": "20" - }, - "label": "mapping(address => uint256)", - "numberOfBytes": "32", - "value": { - "encoding": "inplace", - "label": "uint256", - "numberOfBytes": "32" - } - } + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" } } } diff --git a/.storage-layouts-normalized/contracts/colony/ColonyExpenditure.sol:ColonyExpenditure.json b/.storage-layouts-normalized/contracts/colony/ColonyExpenditure.sol:ColonyExpenditure.json index 471700cb87..d24e1daa9f 100644 --- a/.storage-layouts-normalized/contracts/colony/ColonyExpenditure.sol:ColonyExpenditure.json +++ b/.storage-layouts-normalized/contracts/colony/ColonyExpenditure.sol:ColonyExpenditure.json @@ -1412,7 +1412,7 @@ }, { "contract": "contracts/colony/ColonyExpenditure.sol:ColonyExpenditure", - "label": "domainReputationTokenApprovals", + "label": "domainReputationApproval", "offset": 0, "slot": "39", "type": { @@ -1422,32 +1422,12 @@ "label": "uint256", "numberOfBytes": "32" }, - "label": "mapping(uint256 => mapping(uint256 => mapping(address => uint256)))", + "label": "mapping(uint256 => uint256)", "numberOfBytes": "32", "value": { - "encoding": "mapping", - "key": { - "encoding": "inplace", - "label": "uint256", - "numberOfBytes": "32" - }, - "label": "mapping(uint256 => mapping(address => uint256))", - "numberOfBytes": "32", - "value": { - "encoding": "mapping", - "key": { - "encoding": "inplace", - "label": "address", - "numberOfBytes": "20" - }, - "label": "mapping(address => uint256)", - "numberOfBytes": "32", - "value": { - "encoding": "inplace", - "label": "uint256", - "numberOfBytes": "32" - } - } + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" } } } diff --git a/.storage-layouts-normalized/contracts/colony/ColonyFunding.sol:ColonyFunding.json b/.storage-layouts-normalized/contracts/colony/ColonyFunding.sol:ColonyFunding.json index 5fb3dad1ac..3fa29005de 100644 --- a/.storage-layouts-normalized/contracts/colony/ColonyFunding.sol:ColonyFunding.json +++ b/.storage-layouts-normalized/contracts/colony/ColonyFunding.sol:ColonyFunding.json @@ -1412,7 +1412,7 @@ }, { "contract": "contracts/colony/ColonyFunding.sol:ColonyFunding", - "label": "domainReputationTokenApprovals", + "label": "domainReputationApproval", "offset": 0, "slot": "39", "type": { @@ -1422,32 +1422,12 @@ "label": "uint256", "numberOfBytes": "32" }, - "label": "mapping(uint256 => mapping(uint256 => mapping(address => uint256)))", + "label": "mapping(uint256 => uint256)", "numberOfBytes": "32", "value": { - "encoding": "mapping", - "key": { - "encoding": "inplace", - "label": "uint256", - "numberOfBytes": "32" - }, - "label": "mapping(uint256 => mapping(address => uint256))", - "numberOfBytes": "32", - "value": { - "encoding": "mapping", - "key": { - "encoding": "inplace", - "label": "address", - "numberOfBytes": "20" - }, - "label": "mapping(address => uint256)", - "numberOfBytes": "32", - "value": { - "encoding": "inplace", - "label": "uint256", - "numberOfBytes": "32" - } - } + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" } } } diff --git a/.storage-layouts-normalized/contracts/colony/ColonyRewards.sol:ColonyRewards.json b/.storage-layouts-normalized/contracts/colony/ColonyRewards.sol:ColonyRewards.json index d7ad274434..82d779f988 100644 --- a/.storage-layouts-normalized/contracts/colony/ColonyRewards.sol:ColonyRewards.json +++ b/.storage-layouts-normalized/contracts/colony/ColonyRewards.sol:ColonyRewards.json @@ -1412,7 +1412,7 @@ }, { "contract": "contracts/colony/ColonyRewards.sol:ColonyRewards", - "label": "domainReputationTokenApprovals", + "label": "domainReputationApproval", "offset": 0, "slot": "39", "type": { @@ -1422,32 +1422,12 @@ "label": "uint256", "numberOfBytes": "32" }, - "label": "mapping(uint256 => mapping(uint256 => mapping(address => uint256)))", + "label": "mapping(uint256 => uint256)", "numberOfBytes": "32", "value": { - "encoding": "mapping", - "key": { - "encoding": "inplace", - "label": "uint256", - "numberOfBytes": "32" - }, - "label": "mapping(uint256 => mapping(address => uint256))", - "numberOfBytes": "32", - "value": { - "encoding": "mapping", - "key": { - "encoding": "inplace", - "label": "address", - "numberOfBytes": "20" - }, - "label": "mapping(address => uint256)", - "numberOfBytes": "32", - "value": { - "encoding": "inplace", - "label": "uint256", - "numberOfBytes": "32" - } - } + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" } } } diff --git a/.storage-layouts-normalized/contracts/colony/ColonyRoles.sol:ColonyRoles.json b/.storage-layouts-normalized/contracts/colony/ColonyRoles.sol:ColonyRoles.json index 2b9e084351..cffe687525 100644 --- a/.storage-layouts-normalized/contracts/colony/ColonyRoles.sol:ColonyRoles.json +++ b/.storage-layouts-normalized/contracts/colony/ColonyRoles.sol:ColonyRoles.json @@ -1412,7 +1412,7 @@ }, { "contract": "contracts/colony/ColonyRoles.sol:ColonyRoles", - "label": "domainReputationTokenApprovals", + "label": "domainReputationApproval", "offset": 0, "slot": "39", "type": { @@ -1422,32 +1422,12 @@ "label": "uint256", "numberOfBytes": "32" }, - "label": "mapping(uint256 => mapping(uint256 => mapping(address => uint256)))", + "label": "mapping(uint256 => uint256)", "numberOfBytes": "32", "value": { - "encoding": "mapping", - "key": { - "encoding": "inplace", - "label": "uint256", - "numberOfBytes": "32" - }, - "label": "mapping(uint256 => mapping(address => uint256))", - "numberOfBytes": "32", - "value": { - "encoding": "mapping", - "key": { - "encoding": "inplace", - "label": "address", - "numberOfBytes": "20" - }, - "label": "mapping(address => uint256)", - "numberOfBytes": "32", - "value": { - "encoding": "inplace", - "label": "uint256", - "numberOfBytes": "32" - } - } + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" } } } diff --git a/.storage-layouts-normalized/contracts/colony/ColonyStorage.sol:ColonyStorage.json b/.storage-layouts-normalized/contracts/colony/ColonyStorage.sol:ColonyStorage.json index cd14be9e98..3c77c7ae35 100644 --- a/.storage-layouts-normalized/contracts/colony/ColonyStorage.sol:ColonyStorage.json +++ b/.storage-layouts-normalized/contracts/colony/ColonyStorage.sol:ColonyStorage.json @@ -1412,7 +1412,7 @@ }, { "contract": "contracts/colony/ColonyStorage.sol:ColonyStorage", - "label": "domainReputationTokenApprovals", + "label": "domainReputationApproval", "offset": 0, "slot": "39", "type": { @@ -1422,32 +1422,12 @@ "label": "uint256", "numberOfBytes": "32" }, - "label": "mapping(uint256 => mapping(uint256 => mapping(address => uint256)))", + "label": "mapping(uint256 => uint256)", "numberOfBytes": "32", "value": { - "encoding": "mapping", - "key": { - "encoding": "inplace", - "label": "uint256", - "numberOfBytes": "32" - }, - "label": "mapping(uint256 => mapping(address => uint256))", - "numberOfBytes": "32", - "value": { - "encoding": "mapping", - "key": { - "encoding": "inplace", - "label": "address", - "numberOfBytes": "20" - }, - "label": "mapping(address => uint256)", - "numberOfBytes": "32", - "value": { - "encoding": "inplace", - "label": "uint256", - "numberOfBytes": "32" - } - } + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" } } } diff --git a/.storage-layouts-normalized/contracts/testHelpers/FunctionsNotAvailableOnColony.sol:FunctionsNotAvailableOnColony.json b/.storage-layouts-normalized/contracts/testHelpers/FunctionsNotAvailableOnColony.sol:FunctionsNotAvailableOnColony.json index 8fe2849b99..61de74236f 100644 --- a/.storage-layouts-normalized/contracts/testHelpers/FunctionsNotAvailableOnColony.sol:FunctionsNotAvailableOnColony.json +++ b/.storage-layouts-normalized/contracts/testHelpers/FunctionsNotAvailableOnColony.sol:FunctionsNotAvailableOnColony.json @@ -1412,7 +1412,7 @@ }, { "contract": "contracts/testHelpers/FunctionsNotAvailableOnColony.sol:FunctionsNotAvailableOnColony", - "label": "domainReputationTokenApprovals", + "label": "domainReputationApproval", "offset": 0, "slot": "39", "type": { @@ -1422,32 +1422,12 @@ "label": "uint256", "numberOfBytes": "32" }, - "label": "mapping(uint256 => mapping(uint256 => mapping(address => uint256)))", + "label": "mapping(uint256 => uint256)", "numberOfBytes": "32", "value": { - "encoding": "mapping", - "key": { - "encoding": "inplace", - "label": "uint256", - "numberOfBytes": "32" - }, - "label": "mapping(uint256 => mapping(address => uint256))", - "numberOfBytes": "32", - "value": { - "encoding": "mapping", - "key": { - "encoding": "inplace", - "label": "address", - "numberOfBytes": "20" - }, - "label": "mapping(address => uint256)", - "numberOfBytes": "32", - "value": { - "encoding": "inplace", - "label": "uint256", - "numberOfBytes": "32" - } - } + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" } } } diff --git a/.storage-layouts-normalized/contracts/testHelpers/NoLimitSubdomains.sol:NoLimitSubdomains.json b/.storage-layouts-normalized/contracts/testHelpers/NoLimitSubdomains.sol:NoLimitSubdomains.json index 6227bf3d8c..5d5e24feb5 100644 --- a/.storage-layouts-normalized/contracts/testHelpers/NoLimitSubdomains.sol:NoLimitSubdomains.json +++ b/.storage-layouts-normalized/contracts/testHelpers/NoLimitSubdomains.sol:NoLimitSubdomains.json @@ -1412,7 +1412,7 @@ }, { "contract": "contracts/testHelpers/NoLimitSubdomains.sol:NoLimitSubdomains", - "label": "domainReputationTokenApprovals", + "label": "domainReputationApproval", "offset": 0, "slot": "39", "type": { @@ -1422,32 +1422,12 @@ "label": "uint256", "numberOfBytes": "32" }, - "label": "mapping(uint256 => mapping(uint256 => mapping(address => uint256)))", + "label": "mapping(uint256 => uint256)", "numberOfBytes": "32", "value": { - "encoding": "mapping", - "key": { - "encoding": "inplace", - "label": "uint256", - "numberOfBytes": "32" - }, - "label": "mapping(uint256 => mapping(address => uint256))", - "numberOfBytes": "32", - "value": { - "encoding": "mapping", - "key": { - "encoding": "inplace", - "label": "address", - "numberOfBytes": "20" - }, - "label": "mapping(address => uint256)", - "numberOfBytes": "32", - "value": { - "encoding": "inplace", - "label": "uint256", - "numberOfBytes": "32" - } - } + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" } } } diff --git a/contracts/colony/Colony.sol b/contracts/colony/Colony.sol index 7bde99f014..5481d76c4c 100755 --- a/contracts/colony/Colony.sol +++ b/contracts/colony/Colony.sol @@ -320,7 +320,7 @@ contract Colony is BasicMetaTransaction, Multicall, ColonyStorage, PatriciaTreeP ColonyAuthority colonyAuthority = ColonyAuthority(address(authority)); bytes4 sig; - sig = bytes4(keccak256("editAllowedDomainTokenReceipt(uint256,address,uint256,bool)")); + sig = bytes4(keccak256("editAllowedDomainReputationReceipt(uint256,uint256,bool)")); colonyAuthority.setRoleCapability(uint8(ColonyRole.Root), address(this), sig, true); } diff --git a/contracts/colony/ColonyAuthority.sol b/contracts/colony/ColonyAuthority.sol index 95e5c6ed23..8ec4fefc4a 100644 --- a/contracts/colony/ColonyAuthority.sol +++ b/contracts/colony/ColonyAuthority.sol @@ -136,7 +136,7 @@ contract ColonyAuthority is CommonAuthority { addRoleCapability(ROOT_ROLE, "initialiseReputationMining(uint256,bytes32,uint256)"); // Added in colony v?? - addRoleCapability(ROOT_ROLE, "editAllowedDomainTokenReceipt(uint256,address,uint256,bool)"); + addRoleCapability(ROOT_ROLE, "editAllowedDomainReputationReceipt(uint256,uint256,bool)"); } function addRoleCapability(uint8 role, bytes memory sig) private { diff --git a/contracts/colony/ColonyFunding.sol b/contracts/colony/ColonyFunding.sol index 1616d2b3fe..6cb0d71ea8 100755 --- a/contracts/colony/ColonyFunding.sol +++ b/contracts/colony/ColonyFunding.sol @@ -131,14 +131,14 @@ contract ColonyFunding is fundingPots[0].balance[_token] += feeToPay; - uint256 approvedAmount = domainReputationTokenApprovals[block.chainid][_domainId][_token]; + uint256 approvedAmount = domainReputationApproval[_domainId]; if (tokenEarnsReputationOnPayout(_token)) { uint256 transferrableAmount = min(approvedAmount, remainder); uint256 untransferrableAmount = remainder - transferrableAmount; fundingPots[fundingPotId].balance[_token] += transferrableAmount; - domainReputationTokenApprovals[block.chainid][_domainId][_token] -= transferrableAmount; + domainReputationApproval[_domainId] -= transferrableAmount; emit DomainFundsClaimed(msgSender(), _token, _domainId, feeToPay, transferrableAmount); if (untransferrableAmount > 0) { fundingPots[domains[1].fundingPotId].balance[_token] += untransferrableAmount; @@ -158,27 +158,22 @@ contract ColonyFunding is return _token == token; } - function editAllowedDomainTokenReceipt( + function editAllowedDomainReputationReceipt( uint256 _domainId, - address _token, uint256 _amount, bool _add ) public stoppable auth { require(domainExists(_domainId), "colony-funding-domain-does-not-exist"); - require(tokenEarnsReputationOnPayout(_token), "colony-funding-token-does-not-earn-reputation"); require(_domainId > 1, "colony-funding-root-domain"); if (_add) { - domainReputationTokenApprovals[block.chainid][_domainId][_token] += _amount; + domainReputationApproval[_domainId] += _amount; } else { - domainReputationTokenApprovals[block.chainid][_domainId][_token] -= _amount; + domainReputationApproval[_domainId] -= _amount; } } - function getAllowedDomainTokenReceipt( - uint256 _domainId, - address _token - ) public view returns (uint256) { - return domainReputationTokenApprovals[block.chainid][_domainId][_token]; + function getAllowedDomainReputationReceipt(uint256 _domainId) public view returns (uint256) { + return domainReputationApproval[_domainId]; } function getNonRewardPotsTotal(address _token) public view returns (uint256) { diff --git a/contracts/colony/ColonyStorage.sol b/contracts/colony/ColonyStorage.sol index 5b075e7ebe..6975adb348 100755 --- a/contracts/colony/ColonyStorage.sol +++ b/contracts/colony/ColonyStorage.sol @@ -114,8 +114,8 @@ contract ColonyStorage is ColonyDataTypes, ColonyNetworkDataTypes, DSMath, Commo mapping(uint256 => bool) DEPRECATED_localSkills; // Storage slot 37 mapping(uint256 => LocalSkill) localSkills; // Storage slot 38 - // Mapping of domain id => chainid => token address => approval to receive if reputation-earning - mapping(uint256 => mapping(uint256 => mapping(address => uint256))) domainReputationTokenApprovals; // Storage slot 39 + // Mapping of domainId to allowed amount of reputation received tokens could generate if paid out + mapping(uint256 => uint256) domainReputationApproval; // Storage slot 39 // Constants diff --git a/contracts/colony/IColony.sol b/contracts/colony/IColony.sol index e1e37314f4..d4b25f062c 100644 --- a/contracts/colony/IColony.sol +++ b/contracts/colony/IColony.sol @@ -845,24 +845,18 @@ interface IColony is IDSAuth, ColonyDataTypes, IRecovery, IBasicMetaTransaction, /// @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 _token Address of the token /// @param _amount Amount to add or remove /// @param _add Whether to add or remove the amount. True is add, false is remove - function editAllowedDomainTokenReceipt( + function editAllowedDomainReputationReceipt( uint256 _domainId, - address _token, 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 - /// @param _token Address of the token /// @return uint256 amount Amount of the token that the domain can receive - function getAllowedDomainTokenReceipt( - uint256 _domainId, - address _token - ) external view returns (uint256); + 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 diff --git a/docs/interfaces/icolony.md b/docs/interfaces/icolony.md index 6a79e39e32..033da0286e 100644 --- a/docs/interfaces/icolony.md +++ b/docs/interfaces/icolony.md @@ -273,7 +273,7 @@ Deprecate a local skill for the colony. Secured function to authorised members. |deprecated|bool|Deprecation status to set for the skill -### ▸ `editAllowedDomainTokenReceipt(uint256 _domainId, address _token, uint256 _amount, bool _add)` +### ▸ `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 @@ -283,7 +283,6 @@ Add or remove an amount from the amount of a reputation earning token that a dom |Name|Type|Description| |---|---|---| |_domainId|uint256|Id of the domain -|_token|address|Address of the token |_amount|uint256|Amount to add or remove |_add|bool|Whether to add or remove the amount. True is add, false is remove @@ -466,7 +465,7 @@ A function to be called after an upgrade has been done from v2 to v3. -### ▸ `getAllowedDomainTokenReceipt(uint256 _domainId, address _token):uint256 uint256` +### ▸ `getAllowedDomainReputationReceipt(uint256 _domainId):uint256 uint256` Get the amount of a reputation earning token that a domain can receive @@ -476,7 +475,6 @@ Get the amount of a reputation earning token that a domain can receive |Name|Type|Description| |---|---|---| |_domainId|uint256|Id of the domain -|_token|address|Address of the token **Return Parameters** diff --git a/test/contracts-network/colony-funding.js b/test/contracts-network/colony-funding.js index 811cb3525e..4befe34fef 100755 --- a/test/contracts-network/colony-funding.js +++ b/test/contracts-network/colony-funding.js @@ -688,8 +688,8 @@ contract("Colony Funding", (accounts) => { await token.transfer(receiverAddress, 100); // Approve 70 for the domain - await colony.editAllowedDomainTokenReceipt(2, token.address, 70, true); - let allowedReceipt = await colony.getAllowedDomainTokenReceipt(2, token.address); + 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 @@ -711,40 +711,36 @@ contract("Colony Funding", (accounts) => { expect(nonRewardPotsTotalAfter.sub(nonRewardPotsTotalBefore)).to.eq.BN(99); expect(rootDomainPotBalanceAfter.sub(rootDomainPotBalanceBefore)).to.eq.BN(29); - allowedReceipt = await colony.getAllowedDomainTokenReceipt(2, token.address); + allowedReceipt = await colony.getAllowedDomainReputationReceipt(2); expect(allowedReceipt).to.eq.BN(0); }); - it(`root permission is required to call editAllowedDomainTokenReceipt`, async () => { + it(`root permission is required to call editAllowedDomainReputationReceipt`, async () => { await colony.addDomain(1, UINT256_MAX, 1); - await checkErrorRevert(colony.editAllowedDomainTokenReceipt(2, token.address, 70, true, { from: WORKER }), "ds-auth-unauthorized"); + 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.editAllowedDomainTokenReceipt(2, token.address, 70, true, { from: WORKER }); + await colony.editAllowedDomainReputationReceipt(2, 70, true, { from: WORKER }); }); - it(`cannot editAllowedDomainTokenReceipt for a domain that does not exist`, async () => { - await checkErrorRevert(colony.editAllowedDomainTokenReceipt(2, token.address, 70, true), "colony-funding-domain-does-not-exist"); - }); - - it(`cannot editAllowedDomainTokenReceipt for a token that does not earn reputation`, async () => { - await checkErrorRevert(colony.editAllowedDomainTokenReceipt(1, ADDRESS_ZERO, 70, true), "colony-funding-token-does-not-earn-reputation"); + 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.editAllowedDomainTokenReceipt(2, token.address, 70, true); - let allowedReceipt = await colony.getAllowedDomainTokenReceipt(2, token.address); + await colony.editAllowedDomainReputationReceipt(2, 70, true); + let allowedReceipt = await colony.getAllowedDomainReputationReceipt(2); expect(allowedReceipt).to.eq.BN(70); - await colony.editAllowedDomainTokenReceipt(2, token.address, 20, false); - allowedReceipt = await colony.getAllowedDomainTokenReceipt(2, token.address); + await colony.editAllowedDomainReputationReceipt(2, 20, false); + allowedReceipt = await colony.getAllowedDomainReputationReceipt(2); expect(allowedReceipt).to.eq.BN(50); }); - it(`cannot editAllowedDomainTokenReceipt for the root domain`, async () => { - await checkErrorRevert(colony.editAllowedDomainTokenReceipt(1, token.address, 70, true), "colony-funding-root-domain"); + 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, @@ -772,8 +768,8 @@ contract("Colony Funding", (accounts) => { await token.transfer(receiverAddress, 100); // Approve 250 for the domain - await colony.editAllowedDomainTokenReceipt(2, token.address, 250, true); - let allowedReceipt = await colony.getAllowedDomainTokenReceipt(2, token.address); + 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 @@ -795,7 +791,7 @@ contract("Colony Funding", (accounts) => { expect(nonRewardPotsTotalAfter.sub(nonRewardPotsTotalBefore)).to.eq.BN(99); expect(rootDomainPotBalanceAfter.sub(rootDomainPotBalanceBefore)).to.eq.BN(0); - allowedReceipt = await colony.getAllowedDomainTokenReceipt(2, token.address); + allowedReceipt = await colony.getAllowedDomainReputationReceipt(2); expect(allowedReceipt).to.eq.BN(151); }); diff --git a/test/contracts-network/colony-recovery.js b/test/contracts-network/colony-recovery.js index f42b9d54bb..b785f7d102 100644 --- a/test/contracts-network/colony-recovery.js +++ b/test/contracts-network/colony-recovery.js @@ -214,7 +214,7 @@ contract("Colony Recovery", (accounts) => { 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.editAllowedDomainTokenReceipt(1, ADDRESS_ZERO, 1, true), "colony-in-recovery-mode"); + await checkErrorRevert(metaColony.editAllowedDomainReputationReceipt(1, 1, true), "colony-in-recovery-mode"); }); it("recovery functions should be permissioned", async () => { From cc85c91f131ea4c5765e37084ed6a0eda700526c Mon Sep 17 00:00:00 2001 From: Alex Rea Date: Thu, 24 Oct 2024 17:01:28 +0100 Subject: [PATCH 13/16] More efficiently award address native tokens in tests --- docs/interfaces/imetacolony.md | 31 +++++++++++++ test/contracts-network/colony-funding.js | 58 ++++-------------------- 2 files changed, 41 insertions(+), 48 deletions(-) diff --git a/docs/interfaces/imetacolony.md b/docs/interfaces/imetacolony.md index 7679ece549..cab1e6ea16 100644 --- a/docs/interfaces/imetacolony.md +++ b/docs/interfaces/imetacolony.md @@ -296,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 @@ -474,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/test/contracts-network/colony-funding.js b/test/contracts-network/colony-funding.js index 4befe34fef..b03ab547c3 100755 --- a/test/contracts-network/colony-funding.js +++ b/test/contracts-network/colony-funding.js @@ -18,13 +18,7 @@ const { ADDRESS_ZERO, } = require("../../helpers/constants"); -const { - fundColonyWithTokens, - setupRandomColony, - makeExpenditure, - setupFundedExpenditure, - setupClaimedExpenditure, -} = require("../../helpers/test-data-generator"); +const { fundColonyWithTokens, setupRandomColony, makeExpenditure, setupFundedExpenditure } = require("../../helpers/test-data-generator"); const { getTokenArgs, checkErrorRevert, web3GetBalance, removeSubdomainLimit, expectEvent, rolesToBytes32 } = require("../../helpers/test-helper"); const { setupDomainTokenReceiverResolver } = require("../../helpers/upgradable-contracts"); @@ -629,19 +623,9 @@ contract("Colony Funding", (accounts) => { await colony.claimColonyFunds(token.address); const domain1 = await colony.getDomain(1); - // Pay the tokens to the domain - await setupClaimedExpenditure({ - colonyNetwork, - colony, - domainId: 1, - manager: MANAGER, - managerPayout: 1000, - evaluatorPayout: 0, - workerPayout: 0, - }); - - // Send 100 to the domain - await token.transfer(receiverAddress, 100); + // 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 @@ -672,20 +656,9 @@ contract("Colony Funding", (accounts) => { await colony.claimColonyFunds(token.address); const domain1 = await colony.getDomain(1); - // Pay the tokens to the domain - await setupClaimedExpenditure({ - colonyNetwork, - colony, - domainId: 1, - manager: MANAGER, - tokenAddress: token.address, - managerPayout: 1000, - evaluatorPayout: 0, - workerPayout: 0, - }); - - // Send 100 to the domain - await token.transfer(receiverAddress, 100); + // 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); @@ -752,20 +725,9 @@ contract("Colony Funding", (accounts) => { await colony.claimColonyFunds(token.address); const domain1 = await colony.getDomain(1); - // Pay the tokens to the domain - await setupClaimedExpenditure({ - colonyNetwork, - colony, - domainId: 1, - manager: MANAGER, - tokenAddress: token.address, - managerPayout: 1000, - evaluatorPayout: 0, - workerPayout: 0, - }); - - // Send 100 to the domain - await token.transfer(receiverAddress, 100); + // 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); From 925347435178521cfbba4695258d8d1599c8e291 Mon Sep 17 00:00:00 2001 From: Alex Rea Date: Thu, 5 Dec 2024 12:08:41 +0000 Subject: [PATCH 14/16] Prevent direct calls of transferToColony --- contracts/common/DomainTokenReceiver.sol | 7 ++++++- test/contracts-network/colony-funding.js | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/contracts/common/DomainTokenReceiver.sol b/contracts/common/DomainTokenReceiver.sol index 6ef24d3576..40bda1d8bc 100644 --- a/contracts/common/DomainTokenReceiver.sol +++ b/contracts/common/DomainTokenReceiver.sol @@ -25,6 +25,11 @@ contract DomainTokenReceiver is DSAuth { address colony; + modifier onlyColony() { + require(msg.sender == colony, "domain-token-receiver-unauthorized"); + _; + } + function getColonyAddress() public view returns (address) { return colony; } @@ -34,7 +39,7 @@ contract DomainTokenReceiver is DSAuth { colony = _colony; } - function transferToColony(address tokenAddress) public { + function transferToColony(address tokenAddress) public onlyColony { // Transfer the token to the colony. if (tokenAddress == address(0)) { // slither-disable-next-line arbitrary-send-eth diff --git a/test/contracts-network/colony-funding.js b/test/contracts-network/colony-funding.js index b03ab547c3..9585de41bf 100755 --- a/test/contracts-network/colony-funding.js +++ b/test/contracts-network/colony-funding.js @@ -606,6 +606,22 @@ contract("Colony Funding", (accounts) => { expect(nonRewardPotsTotalAfter.sub(nonRewardPotsTotalBefore)).to.eq.BN(99); }); + it("should not allow someone to call transferToColonyDirectly, which would mess up bookkeeping", async () => { + await colony.addDomain(1, UINT256_MAX, 1); + const receiverAddress = await colonyNetwork.getDomainTokenReceiverAddress(colony.address, 2); + + // Send 100 wei + await otherToken.mint(receiverAddress, 100); + + // Claim the funds + await colony.claimDomainFunds(otherToken.address, 2); + + await otherToken.mint(receiverAddress, 100); + + const receiver = await DomainTokenReceiver.at(receiverAddress); + await checkErrorRevert(receiver.transferToColony(otherToken.address), "domain-token-receiver-unauthorized"); + }); + 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); From c9c91373397047afb40a96ad54fce7fdb12b7e42 Mon Sep 17 00:00:00 2001 From: Alex Rea Date: Thu, 5 Dec 2024 13:32:40 +0000 Subject: [PATCH 15/16] Allow directly sending to domains to work with locked tokens --- contracts/colony/ColonyFunding.sol | 12 +++++-- contracts/common/DomainTokenReceiver.sol | 23 ++++++------- contracts/common/TokenAuthority.sol | 1 + test/contracts-network/colony-funding.js | 43 ++++++++++++++++++++++-- 4 files changed, 61 insertions(+), 18 deletions(-) diff --git a/contracts/colony/ColonyFunding.sol b/contracts/colony/ColonyFunding.sol index 6cb0d71ea8..84840261c3 100755 --- a/contracts/colony/ColonyFunding.sol +++ b/contracts/colony/ColonyFunding.sol @@ -150,8 +150,16 @@ contract ColonyFunding is } // Claim funds - - DomainTokenReceiver(domainTokenReceiverAddress).transferToColony(_token); + if (_token == address(0x0)) { + DomainTokenReceiver(domainTokenReceiverAddress).transferNativeToColony(); + } else { + DomainTokenReceiver(domainTokenReceiverAddress).approveTokenToColony(_token); + // slither-disable-next-line arbitrary-send-erc20 + require( + ERC20Extended(_token).transferFrom(domainTokenReceiverAddress, address(this), claimAmount), + "colony-funding-transfer-failed" + ); + } } function tokenEarnsReputationOnPayout(address _token) internal view returns (bool) { diff --git a/contracts/common/DomainTokenReceiver.sol b/contracts/common/DomainTokenReceiver.sol index 40bda1d8bc..ef0b721a60 100644 --- a/contracts/common/DomainTokenReceiver.sol +++ b/contracts/common/DomainTokenReceiver.sol @@ -39,18 +39,15 @@ contract DomainTokenReceiver is DSAuth { colony = _colony; } - function transferToColony(address tokenAddress) public onlyColony { - // 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" - ); - } + function transferNativeToColony() public onlyColony { + payable(colony).transfer(address(this).balance); + } + + function approveTokenToColony(address tokenAddress) public onlyColony { + uint256 balanceToTransfer = ERC20Extended(tokenAddress).balanceOf(address(this)); + require( + ERC20Extended(tokenAddress).approve(colony, balanceToTransfer), + "domain-token-receiver-approve-failed" + ); } } diff --git a/contracts/common/TokenAuthority.sol b/contracts/common/TokenAuthority.sol index 8552314613..bce4b28aad 100644 --- a/contracts/common/TokenAuthority.sol +++ b/contracts/common/TokenAuthority.sol @@ -35,6 +35,7 @@ contract TokenAuthority is DSAuthority { bytes4 mintSigOverload = bytes4(keccak256("mint(address,uint256)")); authorizations[_colony][transferSig] = true; + authorizations[_colony][transferFromSig] = true; authorizations[_colony][mintSig] = true; authorizations[_colony][mintSigOverload] = true; diff --git a/test/contracts-network/colony-funding.js b/test/contracts-network/colony-funding.js index 9585de41bf..77926a8fcf 100755 --- a/test/contracts-network/colony-funding.js +++ b/test/contracts-network/colony-funding.js @@ -18,7 +18,13 @@ const { ADDRESS_ZERO, } = require("../../helpers/constants"); -const { fundColonyWithTokens, setupRandomColony, makeExpenditure, setupFundedExpenditure } = require("../../helpers/test-data-generator"); +const { + fundColonyWithTokens, + setupRandomColony, + makeExpenditure, + setupFundedExpenditure, + setupRandomToken, +} = require("../../helpers/test-data-generator"); const { getTokenArgs, checkErrorRevert, web3GetBalance, removeSubdomainLimit, expectEvent, rolesToBytes32 } = require("../../helpers/test-helper"); const { setupDomainTokenReceiverResolver } = require("../../helpers/upgradable-contracts"); @@ -31,6 +37,7 @@ const IMetaColony = artifacts.require("IMetaColony"); const Token = artifacts.require("Token"); const Resolver = artifacts.require("Resolver"); const DomainTokenReceiver = artifacts.require("DomainTokenReceiver"); +const TokenAuthority = artifacts.require("contracts/common/TokenAuthority.sol:TokenAuthority"); contract("Colony Funding", (accounts) => { const MANAGER = accounts[0]; @@ -606,7 +613,36 @@ contract("Colony Funding", (accounts) => { expect(nonRewardPotsTotalAfter.sub(nonRewardPotsTotalBefore)).to.eq.BN(99); }); - it("should not allow someone to call transferToColonyDirectly, which would mess up bookkeeping", async () => { + it("should allow a locked 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 + const otherToken2 = await setupRandomToken(true); + const tokenLockingAddress = await colonyNetwork.getTokenLocking(); + const tokenAuthority = await TokenAuthority.new(otherToken2.address, colony.address, [tokenLockingAddress]); + await otherToken2.setAuthority(tokenAuthority.address); + + await otherToken2.mint(receiverAddress, 100); + + const domain = await colony.getDomain(2); + const domainPotBalanceBefore = await colony.getFundingPotBalance(domain.fundingPotId, otherToken2.address); + const nonRewardPotsTotalBefore = await colony.getNonRewardPotsTotal(otherToken2.address); + + // Claim the funds + const tx = await colony.claimDomainFunds(otherToken2.address, 2); + await expectEvent(tx, "DomainFundsClaimed", [MANAGER, otherToken2.address, 2, 1, 99]); + + const domainPotBalanceAfter = await colony.getFundingPotBalance(domain.fundingPotId, otherToken2.address); + const nonRewardPotsTotalAfter = await colony.getNonRewardPotsTotal(otherToken2.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 someone to call functions on domain receiver directly, which would mess up bookkeeping", async () => { await colony.addDomain(1, UINT256_MAX, 1); const receiverAddress = await colonyNetwork.getDomainTokenReceiverAddress(colony.address, 2); @@ -619,7 +655,8 @@ contract("Colony Funding", (accounts) => { await otherToken.mint(receiverAddress, 100); const receiver = await DomainTokenReceiver.at(receiverAddress); - await checkErrorRevert(receiver.transferToColony(otherToken.address), "domain-token-receiver-unauthorized"); + await checkErrorRevert(receiver.approveTokenToColony(otherToken.address), "domain-token-receiver-unauthorized"); + await checkErrorRevert(receiver.transferNativeToColony(), "domain-token-receiver-unauthorized"); }); it("should not allow even the colonyNetwork to call setColonyAddress once it's set on domainTokenReceiver", async () => { From b816071927abe37afda70ff7939dedc46f4c48f7 Mon Sep 17 00:00:00 2001 From: Alex Rea Date: Wed, 18 Dec 2024 11:57:49 +0000 Subject: [PATCH 16/16] Improvements following review --- contracts/colony/ColonyFunding.sol | 11 +++- contracts/common/DomainTokenReceiver.sol | 2 +- test/contracts-network/colony-funding.js | 84 ++++++++++++------------ 3 files changed, 51 insertions(+), 46 deletions(-) diff --git a/contracts/colony/ColonyFunding.sol b/contracts/colony/ColonyFunding.sol index 84840261c3..8346e71af0 100755 --- a/contracts/colony/ColonyFunding.sol +++ b/contracts/colony/ColonyFunding.sol @@ -111,8 +111,6 @@ contract ColonyFunding is 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; @@ -131,6 +129,7 @@ contract ColonyFunding is fundingPots[0].balance[_token] += feeToPay; + uint256 fundingPotId = domains[_domainId].fundingPotId; uint256 approvedAmount = domainReputationApproval[_domainId]; if (tokenEarnsReputationOnPayout(_token)) { @@ -139,19 +138,22 @@ contract ColonyFunding is 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 if (_token == address(0x0)) { - DomainTokenReceiver(domainTokenReceiverAddress).transferNativeToColony(); + DomainTokenReceiver(domainTokenReceiverAddress).transferChainNativeToColony(); } else { DomainTokenReceiver(domainTokenReceiverAddress).approveTokenToColony(_token); // slither-disable-next-line arbitrary-send-erc20 @@ -163,6 +165,7 @@ contract ColonyFunding is } function tokenEarnsReputationOnPayout(address _token) internal view returns (bool) { + // This function is in anticipation of multiple tokens being able to earn reputation on payout. return _token == token; } @@ -174,6 +177,8 @@ contract ColonyFunding is require(domainExists(_domainId), "colony-funding-domain-does-not-exist"); require(_domainId > 1, "colony-funding-root-domain"); if (_add) { + // Rather than setting the approval to the amount, we add the amount to the approval, so that if there + // are multiple calls to this function via motions etc, the approval will be the sum of all the amounts. domainReputationApproval[_domainId] += _amount; } else { domainReputationApproval[_domainId] -= _amount; diff --git a/contracts/common/DomainTokenReceiver.sol b/contracts/common/DomainTokenReceiver.sol index ef0b721a60..b039c25608 100644 --- a/contracts/common/DomainTokenReceiver.sol +++ b/contracts/common/DomainTokenReceiver.sol @@ -39,7 +39,7 @@ contract DomainTokenReceiver is DSAuth { colony = _colony; } - function transferNativeToColony() public onlyColony { + function transferChainNativeToColony() public onlyColony { payable(colony).transfer(address(this).balance); } diff --git a/test/contracts-network/colony-funding.js b/test/contracts-network/colony-funding.js index 77926a8fcf..686216bca6 100755 --- a/test/contracts-network/colony-funding.js +++ b/test/contracts-network/colony-funding.js @@ -656,7 +656,7 @@ contract("Colony Funding", (accounts) => { const receiver = await DomainTokenReceiver.at(receiverAddress); await checkErrorRevert(receiver.approveTokenToColony(otherToken.address), "domain-token-receiver-unauthorized"); - await checkErrorRevert(receiver.transferNativeToColony(), "domain-token-receiver-unauthorized"); + await checkErrorRevert(receiver.transferChainNativeToColony(), "domain-token-receiver-unauthorized"); }); it("should not allow even the colonyNetwork to call setColonyAddress once it's set on domainTokenReceiver", async () => { @@ -700,6 +700,47 @@ contract("Colony Funding", (accounts) => { expect(rootDomainPotBalanceAfter.sub(rootDomainPotBalanceBefore)).to.eq.BN(99); }); + 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(`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 @@ -769,47 +810,6 @@ contract("Colony Funding", (accounts) => { 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"); });