From a3e9ac3ec22a8239916dd6b9b97ddea2fb7e4434 Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Fri, 6 Oct 2023 14:26:46 +0400 Subject: [PATCH] feat: modify top up factories to work with different stablecoins --- contracts/AllowedRecipientsBuilder.sol | 236 +++++++++-------- contracts/AllowedRecipientsFactory.sol | 33 +++ contracts/AllowedTokensRegistry.sol | 127 +++++++++ .../TopUpAllowedRecipients.sol | 28 +- contracts/test/MockERC20.sol | 13 + .../contracts/token/ERC20/IERC20.sol | 77 ++++++ .../token/ERC20/extensions/IERC20Metadata.sol | 27 ++ tests/conftest.py | 52 +++- .../test_top_up_allowed_recipients.py | 183 ++++++++++--- tests/integration/conftest.py | 54 +++- .../test_allowed_recipients_happy_path.py | 94 ++++--- .../test_allowed_recipients_motions.py | 151 ++++++++--- tests/test_allowed_recipients_builder.py | 93 ++++--- tests/test_allowed_tokens_registry.py | 244 ++++++++++++++++++ utils/lido.py | 14 + utils/test_helpers.py | 6 + 16 files changed, 1139 insertions(+), 293 deletions(-) create mode 100644 contracts/AllowedTokensRegistry.sol create mode 100644 contracts/test/MockERC20.sol create mode 100644 dependencies/OpenZeppelin/openzeppelin-contracts@4.3.2/contracts/token/ERC20/IERC20.sol create mode 100644 dependencies/OpenZeppelin/openzeppelin-contracts@4.3.2/contracts/token/ERC20/extensions/IERC20Metadata.sol create mode 100644 tests/test_allowed_tokens_registry.py diff --git a/contracts/AllowedRecipientsBuilder.sol b/contracts/AllowedRecipientsBuilder.sol index 3133a57a..d525dc80 100644 --- a/contracts/AllowedRecipientsBuilder.sol +++ b/contracts/AllowedRecipientsBuilder.sol @@ -29,6 +29,18 @@ interface IAllowedRecipientsRegistry { function bokkyPooBahsDateTimeContract() external view returns (address); } +interface IAllowedTokensRegistry { + function addToken(address _token) external; + + function renounceRole(bytes32 role, address account) external; + + function isTokenAllowed(address _token) external view returns (bool); + + function hasRole(bytes32 role, address account) external view returns (bool); + + function getAllowedTokens() external view returns (address[] memory); +} + interface ITopUpAllowedRecipients { function token() external view returns (address); @@ -63,22 +75,28 @@ interface IAllowedRecipientsFactory { address bokkyPooBahsDateTimeContract ) external returns (IAllowedRecipientsRegistry); + function deployAllowedTokensRegistry( + address _defaultAdmin, + address[] memory _addTokensToAllowedListRoleHolders, + address[] memory _removeTokensFromAllowedListRoleHolders + ) external returns (IAllowedTokensRegistry registry); + function deployTopUpAllowedRecipients( address _trustedCaller, address _allowedRecipientsRegistry, + address _allowedTokensRegistry, address _token, address _finance, - IEasyTrack _easyTrack - ) external returns (ITopUpAllowedRecipients); + address _easyTrack + ) external returns (ITopUpAllowedRecipients topUpAllowedRecipients); function deployAddAllowedRecipient(address _trustedCaller, address _allowedRecipientsRegistry) external returns (IAddAllowedRecipient); - function deployRemoveAllowedRecipient( - address _trustedCaller, - address _allowedRecipientsRegistry - ) external returns (IRemoveAllowedRecipient); + function deployRemoveAllowedRecipient(address _trustedCaller, address _allowedRecipientsRegistry) + external + returns (IRemoveAllowedRecipient); } contract AllowedRecipientsBuilder { @@ -89,10 +107,11 @@ contract AllowedRecipientsBuilder { address public immutable bokkyPooBahsDateTimeContract; IAllowedRecipientsFactory public immutable factory; - bytes32 public constant ADD_RECIPIENT_TO_ALLOWED_LIST_ROLE = - keccak256("ADD_RECIPIENT_TO_ALLOWED_LIST_ROLE"); + bytes32 public constant ADD_RECIPIENT_TO_ALLOWED_LIST_ROLE = keccak256("ADD_RECIPIENT_TO_ALLOWED_LIST_ROLE"); bytes32 public constant REMOVE_RECIPIENT_FROM_ALLOWED_LIST_ROLE = keccak256("REMOVE_RECIPIENT_FROM_ALLOWED_LIST_ROLE"); + bytes32 public constant ADD_TOKEN_TO_ALLOWED_LIST_ROLE = keccak256("ADD_TOKEN_TO_ALLOWED_LIST_ROLE"); + bytes32 public constant REMOVE_TOKEN_FROM_ALLOWED_LIST_ROLE = keccak256("REMOVE_TOKEN_FROM_ALLOWED_LIST_ROLE"); bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00; bytes32 public constant SET_PARAMETERS_ROLE = keccak256("SET_PARAMETERS_ROLE"); bytes32 public constant UPDATE_SPENT_AMOUNT_ROLE = keccak256("UPDATE_SPENT_AMOUNT_ROLE"); @@ -112,14 +131,15 @@ contract AllowedRecipientsBuilder { bokkyPooBahsDateTimeContract = _bokkyPooBahsDateTimeContract; } - function deployAllowedRecipientsRegistry( + function deployRegistries( uint256 _limit, uint256 _periodDurationMonths, + address[] memory _tokens, address[] memory _recipients, string[] memory _titles, uint256 _spentAmount, bool _grantRightsToEVMScriptExecutor - ) public returns (IAllowedRecipientsRegistry registry) { + ) public returns (IAllowedRecipientsRegistry recipientRegistry, IAllowedTokensRegistry tokenRegistry) { require(_recipients.length == _titles.length, "Recipients data length mismatch"); require(_spentAmount <= _limit, "_spentAmount must be lower or equal to limit"); @@ -146,7 +166,7 @@ contract AllowedRecipientsBuilder { updateSpentAmountRoleHolders[1] = evmScriptExecutor; updateSpentAmountRoleHolders[2] = address(this); - registry = factory.deployAllowedRecipientsRegistry( + recipientRegistry = factory.deployAllowedRecipientsRegistry( admin, addRecipientToAllowedListRoleHolders, removeRecipientFromAllowedListRoleHolders, @@ -155,185 +175,159 @@ contract AllowedRecipientsBuilder { bokkyPooBahsDateTimeContract ); - assert(registry.bokkyPooBahsDateTimeContract() == bokkyPooBahsDateTimeContract); + assert(recipientRegistry.bokkyPooBahsDateTimeContract() == bokkyPooBahsDateTimeContract); for (uint256 i = 0; i < _recipients.length; i++) { - registry.addRecipient(_recipients[i], _titles[i]); + recipientRegistry.addRecipient(_recipients[i], _titles[i]); } - registry.renounceRole(ADD_RECIPIENT_TO_ALLOWED_LIST_ROLE, address(this)); + recipientRegistry.renounceRole(ADD_RECIPIENT_TO_ALLOWED_LIST_ROLE, address(this)); - assert(registry.getAllowedRecipients().length == _recipients.length); + assert(recipientRegistry.getAllowedRecipients().length == _recipients.length); for (uint256 i = 0; i < _recipients.length; i++) { - assert(registry.isRecipientAllowed(_recipients[i])); + assert(recipientRegistry.isRecipientAllowed(_recipients[i])); + } + + tokenRegistry = factory.deployAllowedTokensRegistry( + admin, addRecipientToAllowedListRoleHolders, removeRecipientFromAllowedListRoleHolders + ); + + for (uint256 i = 0; i < _tokens.length; i++) { + tokenRegistry.addToken(_tokens[i]); } + tokenRegistry.renounceRole(ADD_TOKEN_TO_ALLOWED_LIST_ROLE, address(this)); - registry.setLimitParameters(_limit, _periodDurationMonths); - registry.renounceRole(SET_PARAMETERS_ROLE, address(this)); + assert(tokenRegistry.getAllowedTokens().length == _tokens.length); - (uint256 registryLimit, uint256 registryPeriodDuration) = registry.getLimitParameters(); + for (uint256 i = 0; i < _tokens.length; i++) { + assert(tokenRegistry.isTokenAllowed(_tokens[i])); + } + + recipientRegistry.setLimitParameters(_limit, _periodDurationMonths); + recipientRegistry.renounceRole(SET_PARAMETERS_ROLE, address(this)); + + (uint256 registryLimit, uint256 registryPeriodDuration) = recipientRegistry.getLimitParameters(); assert(registryLimit == _limit); assert(registryPeriodDuration == _periodDurationMonths); - registry.updateSpentAmount(_spentAmount); - registry.renounceRole(UPDATE_SPENT_AMOUNT_ROLE, address(this)); + recipientRegistry.updateSpentAmount(_spentAmount); + recipientRegistry.renounceRole(UPDATE_SPENT_AMOUNT_ROLE, address(this)); - assert(registry.spendableBalance() == _limit - _spentAmount); + assert(recipientRegistry.spendableBalance() == _limit - _spentAmount); - assert(registry.hasRole(ADD_RECIPIENT_TO_ALLOWED_LIST_ROLE, admin)); - assert(registry.hasRole(REMOVE_RECIPIENT_FROM_ALLOWED_LIST_ROLE, admin)); - assert(registry.hasRole(SET_PARAMETERS_ROLE, admin)); - assert(registry.hasRole(UPDATE_SPENT_AMOUNT_ROLE, admin)); - assert(registry.hasRole(DEFAULT_ADMIN_ROLE, admin)); + assert(recipientRegistry.hasRole(ADD_RECIPIENT_TO_ALLOWED_LIST_ROLE, admin)); + assert(recipientRegistry.hasRole(REMOVE_RECIPIENT_FROM_ALLOWED_LIST_ROLE, admin)); + assert(recipientRegistry.hasRole(SET_PARAMETERS_ROLE, admin)); + assert(recipientRegistry.hasRole(UPDATE_SPENT_AMOUNT_ROLE, admin)); + assert(recipientRegistry.hasRole(DEFAULT_ADMIN_ROLE, admin)); + assert(tokenRegistry.hasRole(ADD_TOKEN_TO_ALLOWED_LIST_ROLE, admin)); + assert(tokenRegistry.hasRole(REMOVE_TOKEN_FROM_ALLOWED_LIST_ROLE, admin)); if (_grantRightsToEVMScriptExecutor) { - assert(registry.hasRole(ADD_RECIPIENT_TO_ALLOWED_LIST_ROLE, evmScriptExecutor)); - assert(registry.hasRole(REMOVE_RECIPIENT_FROM_ALLOWED_LIST_ROLE, evmScriptExecutor)); + assert(recipientRegistry.hasRole(ADD_RECIPIENT_TO_ALLOWED_LIST_ROLE, evmScriptExecutor)); + assert(recipientRegistry.hasRole(REMOVE_RECIPIENT_FROM_ALLOWED_LIST_ROLE, evmScriptExecutor)); + assert(tokenRegistry.hasRole(ADD_TOKEN_TO_ALLOWED_LIST_ROLE, evmScriptExecutor)); + assert(tokenRegistry.hasRole(REMOVE_TOKEN_FROM_ALLOWED_LIST_ROLE, evmScriptExecutor)); } else { - assert(!registry.hasRole(ADD_RECIPIENT_TO_ALLOWED_LIST_ROLE, evmScriptExecutor)); - assert(!registry.hasRole(REMOVE_RECIPIENT_FROM_ALLOWED_LIST_ROLE, evmScriptExecutor)); + assert(!recipientRegistry.hasRole(ADD_RECIPIENT_TO_ALLOWED_LIST_ROLE, evmScriptExecutor)); + assert(!recipientRegistry.hasRole(REMOVE_RECIPIENT_FROM_ALLOWED_LIST_ROLE, evmScriptExecutor)); + assert(!tokenRegistry.hasRole(ADD_TOKEN_TO_ALLOWED_LIST_ROLE, evmScriptExecutor)); + assert(!tokenRegistry.hasRole(REMOVE_TOKEN_FROM_ALLOWED_LIST_ROLE, evmScriptExecutor)); } - assert(registry.hasRole(UPDATE_SPENT_AMOUNT_ROLE, evmScriptExecutor)); - assert(!registry.hasRole(SET_PARAMETERS_ROLE, evmScriptExecutor)); - assert(!registry.hasRole(DEFAULT_ADMIN_ROLE, evmScriptExecutor)); - - assert(!registry.hasRole(ADD_RECIPIENT_TO_ALLOWED_LIST_ROLE, address(this))); - assert(!registry.hasRole(REMOVE_RECIPIENT_FROM_ALLOWED_LIST_ROLE, address(this))); - assert(!registry.hasRole(SET_PARAMETERS_ROLE, address(this))); - assert(!registry.hasRole(UPDATE_SPENT_AMOUNT_ROLE, address(this))); - assert(!registry.hasRole(DEFAULT_ADMIN_ROLE, address(this))); + assert(recipientRegistry.hasRole(UPDATE_SPENT_AMOUNT_ROLE, evmScriptExecutor)); + assert(!recipientRegistry.hasRole(SET_PARAMETERS_ROLE, evmScriptExecutor)); + assert(!recipientRegistry.hasRole(DEFAULT_ADMIN_ROLE, evmScriptExecutor)); + + assert(!recipientRegistry.hasRole(ADD_RECIPIENT_TO_ALLOWED_LIST_ROLE, address(this))); + assert(!recipientRegistry.hasRole(REMOVE_RECIPIENT_FROM_ALLOWED_LIST_ROLE, address(this))); + assert(!recipientRegistry.hasRole(SET_PARAMETERS_ROLE, address(this))); + assert(!recipientRegistry.hasRole(UPDATE_SPENT_AMOUNT_ROLE, address(this))); + assert(!recipientRegistry.hasRole(DEFAULT_ADMIN_ROLE, address(this))); + assert(!tokenRegistry.hasRole(ADD_TOKEN_TO_ALLOWED_LIST_ROLE, address(this))); + assert(!tokenRegistry.hasRole(REMOVE_TOKEN_FROM_ALLOWED_LIST_ROLE, address(this))); } function deployTopUpAllowedRecipients( address _trustedCaller, address _allowedRecipientsRegistry, + address _allowedTokensRegistry, address _token ) public returns (ITopUpAllowedRecipients topUpAllowedRecipients) { topUpAllowedRecipients = factory.deployTopUpAllowedRecipients( - _trustedCaller, - _allowedRecipientsRegistry, - _token, - finance, - easyTrack + _trustedCaller, _allowedRecipientsRegistry, _allowedTokensRegistry, _token, finance, address(easyTrack) ); assert(topUpAllowedRecipients.token() == _token); assert(topUpAllowedRecipients.finance() == finance); assert(topUpAllowedRecipients.easyTrack() == easyTrack); assert(topUpAllowedRecipients.trustedCaller() == _trustedCaller); - assert( - address(topUpAllowedRecipients.allowedRecipientsRegistry()) == - _allowedRecipientsRegistry - ); + assert(address(topUpAllowedRecipients.allowedRecipientsRegistry()) == _allowedRecipientsRegistry); } function deployAddAllowedRecipient(address _trustedCaller, address _allowedRecipientsRegistry) public returns (IAddAllowedRecipient addAllowedRecipient) { - addAllowedRecipient = factory.deployAddAllowedRecipient( - _trustedCaller, - _allowedRecipientsRegistry - ); + addAllowedRecipient = factory.deployAddAllowedRecipient(_trustedCaller, _allowedRecipientsRegistry); assert(addAllowedRecipient.trustedCaller() == _trustedCaller); - assert( - address(addAllowedRecipient.allowedRecipientsRegistry()) == _allowedRecipientsRegistry - ); + assert(address(addAllowedRecipient.allowedRecipientsRegistry()) == _allowedRecipientsRegistry); } - function deployRemoveAllowedRecipient( - address _trustedCaller, - address _allowedRecipientsRegistry - ) public returns (IRemoveAllowedRecipient removeAllowedRecipient) { - removeAllowedRecipient = factory.deployRemoveAllowedRecipient( - _trustedCaller, - _allowedRecipientsRegistry - ); + function deployRemoveAllowedRecipient(address _trustedCaller, address _allowedRecipientsRegistry) + public + returns (IRemoveAllowedRecipient removeAllowedRecipient) + { + removeAllowedRecipient = factory.deployRemoveAllowedRecipient(_trustedCaller, _allowedRecipientsRegistry); assert(removeAllowedRecipient.trustedCaller() == _trustedCaller); - assert( - address(removeAllowedRecipient.allowedRecipientsRegistry()) == - _allowedRecipientsRegistry - ); + assert(address(removeAllowedRecipient.allowedRecipientsRegistry()) == _allowedRecipientsRegistry); } function deployFullSetup( address _trustedCaller, - address _token, uint256 _limit, uint256 _periodDurationMonths, + address[] memory _tokens, address[] memory _recipients, string[] memory _titles, uint256 _spentAmount - ) - public - returns ( - IAllowedRecipientsRegistry allowedRecipientsRegistry, - ITopUpAllowedRecipients topUpAllowedRecipients, - IAddAllowedRecipient addAllowedRecipient, - IRemoveAllowedRecipient removeAllowedRecipient - ) - { - allowedRecipientsRegistry = deployAllowedRecipientsRegistry( - _limit, - _periodDurationMonths, - _recipients, - _titles, - _spentAmount, - true - ); - - topUpAllowedRecipients = deployTopUpAllowedRecipients( - _trustedCaller, - address(allowedRecipientsRegistry), - _token - ); + ) public { + (IAllowedRecipientsRegistry allowedRecipientsRegistry, IAllowedTokensRegistry allowedTokensRegistry) = + deployRegistries(_limit, _periodDurationMonths, _tokens, _recipients, _titles, _spentAmount, true); + + for (uint256 i = 0; i < _tokens.length; i++) { + deployTopUpAllowedRecipients( + _trustedCaller, address(allowedRecipientsRegistry), address(allowedTokensRegistry), _tokens[i] + ); + } - addAllowedRecipient = deployAddAllowedRecipient( - _trustedCaller, - address(allowedRecipientsRegistry) - ); + deployAddAllowedRecipient(_trustedCaller, address(allowedRecipientsRegistry)); - removeAllowedRecipient = deployRemoveAllowedRecipient( - _trustedCaller, - address(allowedRecipientsRegistry) - ); + deployRemoveAllowedRecipient(_trustedCaller, address(allowedRecipientsRegistry)); } function deploySingleRecipientTopUpOnlySetup( address _recipient, string memory _title, - address _token, + address[] memory _tokens, uint256 _limit, uint256 _periodDurationMonths, uint256 _spentAmount - ) - public - returns ( - IAllowedRecipientsRegistry allowedRecipientsRegistry, - ITopUpAllowedRecipients topUpAllowedRecipients - ) - { + ) public { address[] memory recipients = new address[](1); recipients[0] = _recipient; string[] memory titles = new string[](1); titles[0] = _title; - allowedRecipientsRegistry = deployAllowedRecipientsRegistry( - _limit, - _periodDurationMonths, - recipients, - titles, - _spentAmount, - false - ); + (IAllowedRecipientsRegistry allowedRecipientsRegistry, IAllowedTokensRegistry allowedTokensRegistry) = + deployRegistries(_limit, _periodDurationMonths, _tokens, recipients, titles, _spentAmount, false); - topUpAllowedRecipients = deployTopUpAllowedRecipients( - _recipient, - address(allowedRecipientsRegistry), - _token - ); + for (uint256 i = 0; i < _tokens.length; i++) { + deployTopUpAllowedRecipients( + _recipient, address(allowedRecipientsRegistry), address(allowedTokensRegistry), _tokens[i] + ); + } } } diff --git a/contracts/AllowedRecipientsFactory.sol b/contracts/AllowedRecipientsFactory.sol index 123798a5..73c05a59 100644 --- a/contracts/AllowedRecipientsFactory.sol +++ b/contracts/AllowedRecipientsFactory.sol @@ -7,6 +7,7 @@ import "./EVMScriptFactories/AddAllowedRecipient.sol"; import "./EVMScriptFactories/RemoveAllowedRecipient.sol"; import "./EVMScriptFactories/TopUpAllowedRecipients.sol"; import "./AllowedRecipientsRegistry.sol"; +import "./AllowedTokensRegistry.sol"; /// @author bulbozaur /// @notice Factory for Allowed Recipient Easy Track contracts @@ -22,11 +23,20 @@ contract AllowedRecipientsFactory { IBokkyPooBahsDateTimeContract bokkyPooBahsDateTimeContract ); + event AllowedTokensRegistryDeployed( + address indexed creator, + address indexed allowedTokensRegistry, + address _defaultAdmin, + address[] addTokenToAllowedListRoleHolders, + address[] removeTokenFromAllowedListRoleHolders + ); + event TopUpAllowedRecipientsDeployed( address indexed creator, address indexed topUpAllowedRecipients, address trustedCaller, address allowedRecipientsRegistry, + address allowedTokenssRegistry, address finance, address token, address easyTrack @@ -75,9 +85,30 @@ contract AllowedRecipientsFactory { ); } + function deployAllowedTokensRegistry( + address _defaultAdmin, + address[] memory _addTokensToAllowedListRoleHolders, + address[] memory _removeTokensFromAllowedListRoleHolders + ) public returns (AllowedTokensRegistry registry) { + registry = new AllowedTokensRegistry( + _defaultAdmin, + _addTokensToAllowedListRoleHolders, + _removeTokensFromAllowedListRoleHolders + ); + + emit AllowedTokensRegistryDeployed( + msg.sender, + address(registry), + _defaultAdmin, + _addTokensToAllowedListRoleHolders, + _removeTokensFromAllowedListRoleHolders + ); + } + function deployTopUpAllowedRecipients( address _trustedCaller, address _allowedRecipientsRegistry, + address _allowedTokensRegistry, address _token, address _finance, address _easyTrack @@ -85,6 +116,7 @@ contract AllowedRecipientsFactory { topUpAllowedRecipients = new TopUpAllowedRecipients( _trustedCaller, _allowedRecipientsRegistry, + _allowedTokensRegistry, _finance, _token, _easyTrack @@ -95,6 +127,7 @@ contract AllowedRecipientsFactory { address(topUpAllowedRecipients), _trustedCaller, _allowedRecipientsRegistry, + _allowedTokensRegistry, _finance, _token, _easyTrack diff --git a/contracts/AllowedTokensRegistry.sol b/contracts/AllowedTokensRegistry.sol new file mode 100644 index 00000000..42d9f9f9 --- /dev/null +++ b/contracts/AllowedTokensRegistry.sol @@ -0,0 +1,127 @@ +// SPDX-FileCopyrightText: 2022 Lido +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.4; + +import "OpenZeppelin/openzeppelin-contracts@4.3.2/contracts/access/AccessControl.sol"; +import "OpenZeppelin/openzeppelin-contracts@4.3.2/contracts/token/ERC20/extensions/IERC20Metadata.sol"; + +contract AllowedTokensRegistry is AccessControl { + // ------------- + // EVENTS + // ------------- + event TokenAdded(address indexed _token); + event TokenRemoved(address indexed _token); + + // ------------- + // ROLES + // ------------- + + bytes32 public constant ADD_TOKEN_TO_ALLOWED_LIST_ROLE = keccak256("ADD_TOKEN_TO_ALLOWED_LIST_ROLE"); + bytes32 public constant REMOVE_TOKEN_FROM_ALLOWED_LIST_ROLE = keccak256("REMOVE_TOKEN_FROM_ALLOWED_LIST_ROLE"); + + // ------------- + // ERRORS + // ------------- + string private constant ERROR_TOKEN_ALREADY_ADDED_TO_ALLOWED_LIST = "TOKEN_ALREADY_ADDED_TO_ALLOWED_LIST"; + string private constant ERROR_TOKEN_NOT_FOUND_IN_ALLOWED_LIST = "TOKEN_NOT_FOUND_IN_ALLOWED_LIST"; + string private constant ERROR_TOKEN_ADDRESS_IS_ZERO = "TOKEN_ADDRESS_IS_ZERO"; + + // ------------- + // VARIABLES + // ------------- + /// @dev List of allowed tokens for payouts + address[] public allowedTokens; + + // Position of the address in the `allowedTokens` array, + // plus 1 because index 0 means a value is not in the set. + mapping(address => uint256) private allowedTokenIndices; + + /// @notice Precise number of tokens in the system + uint8 public constant PRECISION = 18; + + constructor( + address _admin, + address[] memory _addTokenToAllowedListRoleHolders, + address[] memory _removeTokenFromAllowedListRoleHolders + ) { + _setupRole(DEFAULT_ADMIN_ROLE, _admin); + for (uint256 i = 0; i < _addTokenToAllowedListRoleHolders.length; i++) { + _setupRole(ADD_TOKEN_TO_ALLOWED_LIST_ROLE, _addTokenToAllowedListRoleHolders[i]); + } + for (uint256 i = 0; i < _removeTokenFromAllowedListRoleHolders.length; i++) { + _setupRole(REMOVE_TOKEN_FROM_ALLOWED_LIST_ROLE, _removeTokenFromAllowedListRoleHolders[i]); + } + } + + // ------------- + // EXTERNAL METHODS + // ------------- + + /// @notice Adds address to list of allowed tokens for payouts + function addToken(address _token) external onlyRole(ADD_TOKEN_TO_ALLOWED_LIST_ROLE) { + require(_token != address(0), ERROR_TOKEN_ADDRESS_IS_ZERO); + require(allowedTokenIndices[_token] == 0, ERROR_TOKEN_ALREADY_ADDED_TO_ALLOWED_LIST); + + allowedTokens.push(_token); + allowedTokenIndices[_token] = allowedTokens.length; + emit TokenAdded(_token); + } + + /// @notice Removes address from list of allowed tokens for payouts + /// @dev To delete an allowed token from the allowedTokens array in O(1), + /// we swap the element to delete with the last one in the array, + /// and then remove the last element (sometimes called as 'swap and pop'). + function removeToken(address _token) external onlyRole(REMOVE_TOKEN_FROM_ALLOWED_LIST_ROLE) { + uint256 index = _getAllowedTokenIndex(_token); + uint256 lastIndex = allowedTokens.length - 1; + + if (index != lastIndex) { + address lastAllowedToken = allowedTokens[lastIndex]; + allowedTokens[index] = lastAllowedToken; + allowedTokenIndices[lastAllowedToken] = index + 1; + } + + allowedTokens.pop(); + delete allowedTokenIndices[_token]; + emit TokenRemoved(_token); + } + + /// @notice Returns if passed address is listed as allowed token in the registry + function isTokenAllowed(address _token) external view returns (bool) { + return allowedTokenIndices[_token] > 0; + } + + /// @notice Returns current list of allowed tokens + function getAllowedTokens() external view returns (address[] memory) { + return allowedTokens; + } + + /// @notice Transforms amout from token format to precise format + function normalizeAmount(uint256 _tokenAmount, address _token) external view returns (uint256) { + require(_token != address(0), ERROR_TOKEN_ADDRESS_IS_ZERO); + + uint8 tokenDecimals = IERC20Metadata(_token).decimals(); + + if (tokenDecimals == PRECISION) return _tokenAmount; + if (tokenDecimals > PRECISION) { + uint256 difference = tokenDecimals - PRECISION; + uint256 remainder = _tokenAmount % (10 ** difference); + uint256 quotient = _tokenAmount / (10 ** difference); + if (remainder > 0) { + quotient += 1; + } + return quotient; + } + return _tokenAmount * 10 ** (PRECISION - tokenDecimals); + } + + // ------------------ + // PRIVATE METHODS + // ------------------ + + function _getAllowedTokenIndex(address _token) private view returns (uint256 _index) { + _index = allowedTokenIndices[_token]; + require(_index > 0, ERROR_TOKEN_NOT_FOUND_IN_ALLOWED_LIST); + _index -= 1; + } +} diff --git a/contracts/EVMScriptFactories/TopUpAllowedRecipients.sol b/contracts/EVMScriptFactories/TopUpAllowedRecipients.sol index 10008f12..d336ee2f 100644 --- a/contracts/EVMScriptFactories/TopUpAllowedRecipients.sol +++ b/contracts/EVMScriptFactories/TopUpAllowedRecipients.sol @@ -5,6 +5,7 @@ pragma solidity ^0.8.4; import "../TrustedCaller.sol"; import "../AllowedRecipientsRegistry.sol"; +import "../AllowedTokensRegistry.sol"; import "../interfaces/IFinance.sol"; import "../libraries/EVMScriptCreator.sol"; import "../interfaces/IEVMScriptFactory.sol"; @@ -18,6 +19,7 @@ contract TopUpAllowedRecipients is TrustedCaller, IEVMScriptFactory { string private constant ERROR_LENGTH_MISMATCH = "LENGTH_MISMATCH"; string private constant ERROR_EMPTY_DATA = "EMPTY_DATA"; string private constant ERROR_ZERO_AMOUNT = "ZERO_AMOUNT"; + string private constant ERROR_TOKEN_NOT_ALLOWED = "TOKEN_NOT_ALLOWED"; string private constant ERROR_RECIPIENT_NOT_ALLOWED = "RECIPIENT_NOT_ALLOWED"; string private constant ERROR_SUM_EXCEEDS_SPENDABLE_BALANCE = "SUM_EXCEEDS_SPENDABLE_BALANCE"; @@ -37,6 +39,9 @@ contract TopUpAllowedRecipients is TrustedCaller, IEVMScriptFactory { /// @notice Address of AllowedRecipientsRegistry contract AllowedRecipientsRegistry public allowedRecipientsRegistry; + /// @notice Address of AllowedTokenssRegistry contract + AllowedTokensRegistry public allowedTokensRegistry; + // ------------- // CONSTRUCTOR // ------------- @@ -50,6 +55,7 @@ contract TopUpAllowedRecipients is TrustedCaller, IEVMScriptFactory { constructor( address _trustedCaller, address _allowedRecipientsRegistry, + address _allowedTokensRegistry, address _finance, address _token, address _easyTrack @@ -57,6 +63,7 @@ contract TopUpAllowedRecipients is TrustedCaller, IEVMScriptFactory { finance = IFinance(_finance); token = _token; allowedRecipientsRegistry = AllowedRecipientsRegistry(_allowedRecipientsRegistry); + allowedTokensRegistry = AllowedTokensRegistry(_allowedTokensRegistry); easyTrack = EasyTrack(_easyTrack); } @@ -77,9 +84,7 @@ contract TopUpAllowedRecipients is TrustedCaller, IEVMScriptFactory { onlyTrustedCaller(_creator) returns (bytes memory) { - (address[] memory recipients, uint256[] memory amounts) = _decodeEVMScriptCallData( - _evmScriptCallData - ); + (address[] memory recipients, uint256[] memory amounts) = _decodeEVMScriptCallData(_evmScriptCallData); uint256 totalAmount = _validateEVMScriptCallData(recipients, amounts); address[] memory to = new address[](recipients.length + 1); @@ -88,17 +93,12 @@ contract TopUpAllowedRecipients is TrustedCaller, IEVMScriptFactory { to[0] = address(allowedRecipientsRegistry); methodIds[0] = allowedRecipientsRegistry.updateSpentAmount.selector; - evmScriptsCalldata[0] = abi.encode(totalAmount); + evmScriptsCalldata[0] = abi.encode(allowedTokensRegistry.normalizeAmount(totalAmount, token)); for (uint256 i = 0; i < recipients.length; ++i) { to[i + 1] = address(finance); methodIds[i + 1] = finance.newImmediatePayment.selector; - evmScriptsCalldata[i + 1] = abi.encode( - token, - recipients[i], - amounts[i], - "Easy Track: top up recipient" - ); + evmScriptsCalldata[i + 1] = abi.encode(token, recipients[i], amounts[i], "Easy Track: top up recipient"); } return EVMScriptCreator.createEVMScript(to, methodIds, evmScriptsCalldata); @@ -129,17 +129,15 @@ contract TopUpAllowedRecipients is TrustedCaller, IEVMScriptFactory { { require(_amounts.length == _recipients.length, ERROR_LENGTH_MISMATCH); require(_recipients.length > 0, ERROR_EMPTY_DATA); + require(allowedTokensRegistry.isTokenAllowed(token), ERROR_TOKEN_NOT_ALLOWED); for (uint256 i = 0; i < _recipients.length; ++i) { require(_amounts[i] > 0, ERROR_ZERO_AMOUNT); - require( - allowedRecipientsRegistry.isRecipientAllowed(_recipients[i]), - ERROR_RECIPIENT_NOT_ALLOWED - ); + require(allowedRecipientsRegistry.isRecipientAllowed(_recipients[i]), ERROR_RECIPIENT_NOT_ALLOWED); totalAmount += _amounts[i]; } - _validateSpendableBalance(totalAmount); + _validateSpendableBalance(allowedTokensRegistry.normalizeAmount(totalAmount, token)); } function _decodeEVMScriptCallData(bytes memory _evmScriptCallData) diff --git a/contracts/test/MockERC20.sol b/contracts/test/MockERC20.sol new file mode 100644 index 00000000..c223f5ef --- /dev/null +++ b/contracts/test/MockERC20.sol @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2021 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.4; + +contract MockERC20 { + + uint8 public decimals; + + constructor(uint8 _decimals) { + decimals = _decimals; + } +} \ No newline at end of file diff --git a/dependencies/OpenZeppelin/openzeppelin-contracts@4.3.2/contracts/token/ERC20/IERC20.sol b/dependencies/OpenZeppelin/openzeppelin-contracts@4.3.2/contracts/token/ERC20/IERC20.sol new file mode 100644 index 00000000..b7490382 --- /dev/null +++ b/dependencies/OpenZeppelin/openzeppelin-contracts@4.3.2/contracts/token/ERC20/IERC20.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +/** + * @dev Interface of the ERC20 standard as defined in the EIP. + */ +interface IERC20 { + /** + * @dev Returns the amount of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the amount of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Moves `amount` tokens from the caller's account to `recipient`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address recipient, uint256 amount) external returns (bool); + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowance(address owner, address spender) external view returns (uint256); + + /** + * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 amount) external returns (bool); + + /** + * @dev Moves `amount` tokens from `sender` to `recipient` using the + * allowance mechanism. `amount` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); + + /** + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approve}. `value` is the new allowance. + */ + event Approval(address indexed owner, address indexed spender, uint256 value); +} \ No newline at end of file diff --git a/dependencies/OpenZeppelin/openzeppelin-contracts@4.3.2/contracts/token/ERC20/extensions/IERC20Metadata.sol b/dependencies/OpenZeppelin/openzeppelin-contracts@4.3.2/contracts/token/ERC20/extensions/IERC20Metadata.sol new file mode 100644 index 00000000..93666341 --- /dev/null +++ b/dependencies/OpenZeppelin/openzeppelin-contracts@4.3.2/contracts/token/ERC20/extensions/IERC20Metadata.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../IERC20.sol"; + +/** + * @dev Interface for the optional metadata functions from the ERC20 standard. + * + * _Available since v4.1._ + */ +interface IERC20Metadata is IERC20 { + /** + * @dev Returns the name of the token. + */ + function name() external view returns (string memory); + + /** + * @dev Returns the symbol of the token. + */ + function symbol() external view returns (string memory); + + /** + * @dev Returns the decimals places of the token. + */ + function decimals() external view returns (uint8); +} \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 270a4f8c..ff0a02d9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,11 +3,11 @@ import pytest import brownie -from brownie import chain +from brownie import chain import constants from utils.evm_script import encode_call_script, encode_calldata -from utils.lido import contracts +from utils.lido import contracts, external_contracts from utils.config import get_network_name from utils import test_helpers, deployed_date_time @@ -126,7 +126,9 @@ def evm_script_executor(owner, easy_track, calls_script, EVMScriptExecutor): @pytest.fixture(scope="module") -def reward_programs_registry(owner, voting, evm_script_executor_stub, RewardProgramsRegistry): +def reward_programs_registry( + owner, voting, evm_script_executor_stub, RewardProgramsRegistry +): return owner.deploy( RewardProgramsRegistry, voting, @@ -158,8 +160,12 @@ def remove_reward_program(owner, reward_programs_registry, RemoveRewardProgram): @pytest.fixture(scope="module") -def top_up_reward_programs(owner, finance, ldo, reward_programs_registry, TopUpRewardPrograms): - return owner.deploy(TopUpRewardPrograms, owner, reward_programs_registry, finance, ldo) +def top_up_reward_programs( + owner, finance, ldo, reward_programs_registry, TopUpRewardPrograms +): + return owner.deploy( + TopUpRewardPrograms, owner, reward_programs_registry, finance, ldo + ) @pytest.fixture(scope="module") @@ -174,7 +180,9 @@ def add_allowed_recipients(owner, allowed_recipients_registry, AddAllowedRecipie @pytest.fixture(scope="module") -def remove_allowed_recipients(owner, allowed_recipients_registry, RemoveAllowedRecipient): +def remove_allowed_recipients( + owner, allowed_recipients_registry, RemoveAllowedRecipient +): (registry, _, _, _, _, _) = allowed_recipients_registry return owner.deploy(RemoveAllowedRecipient, owner, registry) @@ -271,21 +279,44 @@ def allowed_recipients_registry( ) +@pytest.fixture(scope="module") +def allowed_tokens_registry(AllowedTokensRegistry, owner, accounts): + add_token_role_holder = accounts[6] + remove_token_role_holder = accounts[7] + + registry = owner.deploy( + AllowedTokensRegistry, + owner, + [add_token_role_holder], + [remove_token_role_holder], + ) + + return (registry, owner, add_token_role_holder, remove_token_role_holder) + + @pytest.fixture(scope="module") def top_up_allowed_recipients( allowed_recipients_registry, + allowed_tokens_registry, accounts, finance, ldo, easy_track, TopUpAllowedRecipients, ): - (registry, owner, _, _, _, _) = allowed_recipients_registry + (recipients_registry, owner, _, _, _, _) = allowed_recipients_registry + (tokens_registry, _, _, _) = allowed_tokens_registry trusted_caller = accounts[4] top_up_factory = owner.deploy( - TopUpAllowedRecipients, trusted_caller, registry, finance, ldo, easy_track + TopUpAllowedRecipients, + trusted_caller, + recipients_registry, + tokens_registry, + finance, + ldo, + easy_track, ) return top_up_factory @@ -306,6 +337,11 @@ def steth(lido_contracts): return lido_contracts.steth +@pytest.fixture(scope="module") +def usdc(): + return external_contracts(network=brownie.network.show_active())["usdc"] + + @pytest.fixture(scope="module") def node_operators_registry(lido_contracts): return lido_contracts.node_operators_registry diff --git a/tests/evm_script_factories/test_top_up_allowed_recipients.py b/tests/evm_script_factories/test_top_up_allowed_recipients.py index ea82c285..2c9a39e0 100644 --- a/tests/evm_script_factories/test_top_up_allowed_recipients.py +++ b/tests/evm_script_factories/test_top_up_allowed_recipients.py @@ -5,28 +5,37 @@ from utils.evm_script import encode_calldata, encode_call_script + def make_call_data(recipients, amounts): return encode_calldata("(address[],uint256[])", [recipients, amounts]) def test_top_up_factory_initial_state( allowed_recipients_registry, + allowed_tokens_registry, accounts, finance, ldo, easy_track, TopUpAllowedRecipients, ): - (registry, owner, _, _, _, _) = allowed_recipients_registry + (recipients_registry, owner, _, _, _, _) = allowed_recipients_registry + (tokens_registry, _, _, _) = allowed_tokens_registry trusted_caller = accounts[4] top_up_factory = owner.deploy( - TopUpAllowedRecipients, trusted_caller, registry, finance, ldo, easy_track + TopUpAllowedRecipients, + trusted_caller, + recipients_registry, + tokens_registry, + finance, + ldo, + easy_track, ) assert top_up_factory.token() == ldo - assert top_up_factory.allowedRecipientsRegistry() == registry + assert top_up_factory.allowedRecipientsRegistry() == recipients_registry assert top_up_factory.trustedCaller() == trusted_caller assert top_up_factory.easyTrack() == easy_track assert top_up_factory.finance() == finance @@ -34,18 +43,24 @@ def test_top_up_factory_initial_state( def test_fail_if_zero_trusted_caller( allowed_recipients_registry, + allowed_tokens_registry, finance, ldo, easy_track, TopUpAllowedRecipients, ): (registry, owner, _, _, _, _) = allowed_recipients_registry + (tokens_registry, _, _, _) = allowed_tokens_registry with reverts("TRUSTED_CALLER_IS_ZERO_ADDRESS"): - owner.deploy(TopUpAllowedRecipients, ZERO_ADDRESS, registry, finance, ldo, easy_track) + owner.deploy( + TopUpAllowedRecipients, ZERO_ADDRESS, registry, tokens_registry, finance, ldo, easy_track + ) -def test_top_up_factory_constructor_zero_argument_addresses_allowed(TopUpAllowedRecipients, owner): +def test_top_up_factory_constructor_zero_argument_addresses_allowed( + TopUpAllowedRecipients, owner +): """Check no revert""" trusted_caller = accounts[4] owner.deploy( @@ -55,33 +70,44 @@ def test_top_up_factory_constructor_zero_argument_addresses_allowed(TopUpAllowed ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS, + ZERO_ADDRESS, ) -def test_fail_create_evm_script_if_not_trusted_caller(top_up_allowed_recipients, stranger): +def test_fail_create_evm_script_if_not_trusted_caller( + top_up_allowed_recipients, stranger +): with reverts("CALLER_IS_FORBIDDEN"): top_up_allowed_recipients.createEVMScript(stranger, make_call_data([], [])) def test_create_evm_script_is_permissionless( - allowed_recipients_registry, stranger, top_up_allowed_recipients + allowed_recipients_registry, allowed_tokens_registry, stranger, top_up_allowed_recipients ): ( registry, - owner, + _, add_recipient_role_holder, _, set_limit_role_holder, _, ) = allowed_recipients_registry - registry.addRecipient(stranger.address, "Test Recipient", {"from": add_recipient_role_holder}) + (token_registry, _, add_token_role_holder, _) = allowed_tokens_registry + token_registry.addToken(top_up_allowed_recipients.token(), {"from": add_token_role_holder}) + registry.addRecipient( + stranger.address, "Test Recipient", {"from": add_recipient_role_holder} + ) registry.setLimitParameters(int(100e18), 12, {"from": set_limit_role_holder}) call_data = make_call_data([stranger.address], [123]) trusted_caller = top_up_allowed_recipients.trustedCaller() - top_up_allowed_recipients.createEVMScript(trusted_caller, call_data, {"from": stranger}) + top_up_allowed_recipients.createEVMScript( + trusted_caller, call_data, {"from": stranger} + ) -def test_decode_evm_script_calldata_is_permissionless(stranger, top_up_allowed_recipients): +def test_decode_evm_script_calldata_is_permissionless( + stranger, top_up_allowed_recipients +): call_data = make_call_data([stranger.address], [123]) top_up_allowed_recipients.decodeEVMScriptCallData(call_data, {"from": stranger}) @@ -108,6 +134,7 @@ def test_fail_create_evm_script_if_empty_data(top_up_allowed_recipients, account def test_fail_create_evm_script_if_zero_amount( allowed_recipients_registry, + allowed_tokens_registry, TopUpAllowedRecipients, owner, finance, @@ -125,14 +152,20 @@ def test_fail_create_evm_script_if_zero_amount( set_limit_role_holder, _, ) = allowed_recipients_registry + (tokens_registry, _, add_token_role_holder, _) = allowed_tokens_registry + - registry.addRecipient(recipient, "Test Recipient", {"from": add_recipient_role_holder}) + registry.addRecipient( + recipient, "Test Recipient", {"from": add_recipient_role_holder} + ) registry.setLimitParameters(int(100e18), 12, {"from": set_limit_role_holder}) top_up_factory = owner.deploy( - TopUpAllowedRecipients, trusted_caller, registry, finance, ldo, easy_track + TopUpAllowedRecipients, trusted_caller, registry, tokens_registry, finance, ldo, easy_track ) + tokens_registry.addToken(top_up_factory.token(), {"from": add_token_role_holder}) + with reverts("ZERO_AMOUNT"): top_up_factory.createEVMScript(trusted_caller, make_call_data([recipient], [0])) @@ -144,6 +177,7 @@ def test_fail_create_evm_script_if_zero_amount( def test_fail_create_evm_script_if_recipient_not_allowed( allowed_recipients_registry, + allowed_tokens_registry, TopUpAllowedRecipients, owner, finance, @@ -162,20 +196,65 @@ def test_fail_create_evm_script_if_recipient_not_allowed( set_limit_role_holder, _, ) = allowed_recipients_registry + (tokens_registry, _, add_token_role_holder, _) = allowed_tokens_registry - registry.addRecipient(recipient, "Test Recipient", {"from": add_recipient_role_holder}) + registry.addRecipient( + recipient, "Test Recipient", {"from": add_recipient_role_holder} + ) registry.setLimitParameters(int(100e18), 12, {"from": set_limit_role_holder}) top_up_factory = owner.deploy( - TopUpAllowedRecipients, trusted_caller, registry, finance, ldo, easy_track + TopUpAllowedRecipients, trusted_caller, registry, tokens_registry, finance, ldo, easy_track ) + tokens_registry.addToken(top_up_factory.token(), {"from": add_token_role_holder}) + with reverts("RECIPIENT_NOT_ALLOWED"): - top_up_factory.createEVMScript(trusted_caller, make_call_data([stranger.address], [123])) + top_up_factory.createEVMScript( + trusted_caller, make_call_data([stranger.address], [123]) + ) + + +def test_fail_create_evm_script_if_token_not_allowed( + allowed_recipients_registry, + allowed_tokens_registry, + TopUpAllowedRecipients, + owner, + finance, + ldo, + easy_track, +): + trusted_caller = owner + recipient = accounts[4].address + + ( + registry, + owner, + add_recipient_role_holder, + _, + set_limit_role_holder, + _, + ) = allowed_recipients_registry + (tokens_registry, _, _, _) = allowed_tokens_registry + + registry.addRecipient( + recipient, "Test Recipient", {"from": add_recipient_role_holder} + ) + registry.setLimitParameters(int(100e18), 12, {"from": set_limit_role_holder}) + + top_up_factory = owner.deploy( + TopUpAllowedRecipients, trusted_caller, registry, tokens_registry, finance, ldo, easy_track + ) + + with reverts("TOKEN_NOT_ALLOWED"): + top_up_factory.createEVMScript( + trusted_caller, make_call_data([recipient], [123]) + ) def test_top_up_factory_evm_script_creation_happy_path( allowed_recipients_registry, + allowed_tokens_registry, TopUpAllowedRecipients, owner, finance, @@ -193,14 +272,19 @@ def test_top_up_factory_evm_script_creation_happy_path( set_limit_role_holder, _, ) = allowed_recipients_registry + (tokens_registry, _, add_token_role_holder, _) = allowed_tokens_registry - registry.addRecipient(recipient, "Test Recipient", {"from": add_recipient_role_holder}) + registry.addRecipient( + recipient, "Test Recipient", {"from": add_recipient_role_holder} + ) registry.setLimitParameters(int(100e18), 12, {"from": set_limit_role_holder}) top_up_factory = owner.deploy( - TopUpAllowedRecipients, trusted_caller, registry, finance, ldo, easy_track + TopUpAllowedRecipients, trusted_caller, registry, tokens_registry, finance, ldo, easy_track ) + tokens_registry.addToken(top_up_factory.token(), {"from": add_token_role_holder}) + payout = int(1e18) call_data = make_call_data([recipient], [payout]) evm_script = top_up_factory.createEVMScript(trusted_caller, call_data) @@ -210,6 +294,7 @@ def test_top_up_factory_evm_script_creation_happy_path( def test_top_up_factory_evm_script_creation_multiple_recipients_happy_path( allowed_recipients_registry, + allowed_tokens_registry, TopUpAllowedRecipients, owner, finance, @@ -227,15 +312,22 @@ def test_top_up_factory_evm_script_creation_multiple_recipients_happy_path( set_limit_role_holder, _, ) = allowed_recipients_registry + (tokens_registry, _, add_token_role_holder, _) = allowed_tokens_registry - registry.addRecipient(recipients[0], "Test Recipient 1", {"from": add_recipient_role_holder}) - registry.addRecipient(recipients[1], "Test Recipient 2", {"from": add_recipient_role_holder}) + registry.addRecipient( + recipients[0], "Test Recipient 1", {"from": add_recipient_role_holder} + ) + registry.addRecipient( + recipients[1], "Test Recipient 2", {"from": add_recipient_role_holder} + ) registry.setLimitParameters(int(100e18), 12, {"from": set_limit_role_holder}) top_up_factory = owner.deploy( - TopUpAllowedRecipients, trusted_caller, registry, finance, ldo, easy_track + TopUpAllowedRecipients, trusted_caller, registry, tokens_registry, finance, ldo, easy_track ) + tokens_registry.addToken(top_up_factory.token(), {"from": add_token_role_holder}) + payouts = [int(1e18), int(2e18)] call_data = make_call_data(recipients, payouts) evm_script = top_up_factory.createEVMScript(trusted_caller, call_data) @@ -245,12 +337,13 @@ def test_top_up_factory_evm_script_creation_multiple_recipients_happy_path( def test_fail_create_evm_script_if_sum_exceeds_limit( allowed_recipients_registry, + allowed_tokens_registry, TopUpAllowedRecipients, owner, finance, ldo, - easy_track,): - + easy_track, +): recipients = [accounts[4].address, accounts[5].address] payouts = [int(10e18), int(20e18)] call_data = make_call_data(recipients, payouts) @@ -263,27 +356,35 @@ def test_fail_create_evm_script_if_sum_exceeds_limit( set_limit_role_holder, _, ) = allowed_recipients_registry + (tokens_registry, _, add_token_role_holder, _) = allowed_tokens_registry - registry.addRecipient(recipients[0], "Test Recipient 1", {"from": add_recipient_role_holder}) - registry.addRecipient(recipients[1], "Test Recipient 2", {"from": add_recipient_role_holder}) + registry.addRecipient( + recipients[0], "Test Recipient 1", {"from": add_recipient_role_holder} + ) + registry.addRecipient( + recipients[1], "Test Recipient 2", {"from": add_recipient_role_holder} + ) registry.setLimitParameters(int(20e18), 12, {"from": set_limit_role_holder}) top_up_factory = owner.deploy( - TopUpAllowedRecipients, owner, registry, finance, ldo, easy_track + TopUpAllowedRecipients, owner, registry, tokens_registry, finance, ldo, easy_track ) + tokens_registry.addToken(top_up_factory.token(), {"from": add_token_role_holder}) + with reverts("SUM_EXCEEDS_SPENDABLE_BALANCE"): top_up_factory.createEVMScript(owner, call_data) def test_create_evm_script_correctly( allowed_recipients_registry, + allowed_tokens_registry, TopUpAllowedRecipients, owner, finance, ldo, - easy_track,): - + easy_track, +): recipients = [accounts[4].address, accounts[5].address] payouts = [int(1e18), int(2e18)] totalAmount = int(3e18) @@ -296,24 +397,29 @@ def test_create_evm_script_correctly( set_limit_role_holder, _, ) = allowed_recipients_registry + (tokens_registry, _, add_token_role_holder, _) = allowed_tokens_registry - registry.addRecipient(recipients[0], "Test Recipient 1", {"from": add_recipient_role_holder}) - registry.addRecipient(recipients[1], "Test Recipient 2", {"from": add_recipient_role_holder}) + registry.addRecipient( + recipients[0], "Test Recipient 1", {"from": add_recipient_role_holder} + ) + registry.addRecipient( + recipients[1], "Test Recipient 2", {"from": add_recipient_role_holder} + ) registry.setLimitParameters(int(100e18), 12, {"from": set_limit_role_holder}) top_up_factory = owner.deploy( - TopUpAllowedRecipients, owner, registry, finance, ldo, easy_track + TopUpAllowedRecipients, owner, registry, tokens_registry, finance, ldo, easy_track ) - call_data = make_call_data(recipients,payouts) + tokens_registry.addToken(top_up_factory.token(), {"from": add_token_role_holder}) + + call_data = make_call_data(recipients, payouts) evm_script = top_up_factory.createEVMScript(owner, call_data) expected_evm_script = encode_call_script( [ ( registry.address, - registry.updateSpentAmount.encode_input( - totalAmount - ), + registry.updateSpentAmount.encode_input(totalAmount), ), ( finance.address, @@ -326,7 +432,7 @@ def test_create_evm_script_correctly( finance.newImmediatePayment.encode_input( ldo, recipients[1], payouts[1], "Easy Track: top up recipient" ), - ) + ), ] ) @@ -338,4 +444,7 @@ def test_decode_evm_script_call_data(top_up_allowed_recipients, accounts): payout = int(1e18) call_data = make_call_data([recipient], [payout]) - assert top_up_allowed_recipients.decodeEVMScriptCallData(call_data) == [[recipient], [payout]] + assert top_up_allowed_recipients.decodeEVMScriptCallData(call_data) == [ + [recipient], + [payout], + ] diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index b1721e24..929cef99 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -94,6 +94,9 @@ def _load_deployed_contract(contract_name): def lido_contracts(): return lido.contracts(network=brownie.network.show_active()) +@pytest.fixture(scope="module") +def external_contracts(): + return lido.external_contracts(network=brownie.network.show_active()) @pytest.fixture(scope="module") def easy_track( @@ -180,10 +183,11 @@ def add_allowed_recipient_evm_script_factory( trusted_caller, load_deployed_contract, allowed_recipients_builder, - allowed_recipients_registry, + registries, deployer, ): + (allowed_recipients_registry, _) = registries evm_script_factory = load_deployed_contract("AddAllowedRecipient") if evm_script_factory is None: @@ -214,11 +218,12 @@ def remove_allowed_recipient_evm_script_factory( lido_contracts, load_deployed_contract, allowed_recipients_builder, - allowed_recipients_registry, + registries, deployer, trusted_caller, ): evm_script_factory = load_deployed_contract("RemoveAllowedRecipient") + (allowed_recipients_registry, _) = registries if evm_script_factory is None: tx = allowed_recipients_builder.deployRemoveAllowedRecipient( @@ -312,17 +317,18 @@ def top_up_allowed_recipients_evm_script_factory( lido_contracts, load_deployed_contract, allowed_recipients_builder, - allowed_recipients_registry, + registries, trusted_caller, deployer, ): - + (allowed_recipients_registry, allowed_tokens_registry) = registries evm_script_factory = load_deployed_contract("TopUpAllowedRecipients") if evm_script_factory is None: tx = allowed_recipients_builder.deployTopUpAllowedRecipients( trusted_caller, allowed_recipients_registry, + allowed_tokens_registry, lido_contracts.ldo, {"from": deployer}, ) @@ -484,10 +490,11 @@ def _enact_top_up_allowed_recipient_motion_by_creation_tx(motion_creation_tx): @pytest.fixture(scope="module") def check_top_up_motion_enactment( - AllowedRecipientsRegistry, get_balances, lido_contracts + AllowedRecipientsRegistry, AllowedTokensRegistry, get_balances, lido_contracts ): """Note: this check works correctly only when was payment in the period""" + def _check_top_up_motion_enactment( top_up_allowed_recipients_evm_script_factory, top_up_motion_enactment_tx, @@ -498,12 +505,19 @@ def _check_top_up_motion_enactment( top_up_recipients, top_up_amounts, ): + top_up_token = top_up_allowed_recipients_evm_script_factory.token() allowed_recipients_registry = AllowedRecipientsRegistry.at( top_up_allowed_recipients_evm_script_factory.allowedRecipientsRegistry() ) + allowed_tokens_registry = AllowedTokensRegistry.at( + top_up_allowed_recipients_evm_script_factory.allowedTokensRegistry() + ) limit, duration = allowed_recipients_registry.getLimitParameters() - spending = sum(top_up_amounts) + spending_in_tokens = sum(top_up_amounts) + spending = allowed_tokens_registry.normalizeAmount( + spending_in_tokens, top_up_token + ) spendable = limit - spending assert allowed_recipients_registry.isUnderSpendableBalance(spendable, 0) @@ -519,7 +533,6 @@ def _check_top_up_motion_enactment( == spendable ) - top_up_token = top_up_allowed_recipients_evm_script_factory.token() (sender_balance,) = get_balances(top_up_token, [lido_contracts.aragon.agent]) recipients_balances = get_balances( top_up_token, @@ -527,7 +540,7 @@ def _check_top_up_motion_enactment( ) if top_up_token == lido_contracts.steth: - assert math.isclose(sender_balance, sender_balance_before - spending, abs_tol = STETH_ERROR_MARGIN_WEI) + assert math.isclose(sender_balance, sender_balance_before - spending_in_tokens, abs_tol = STETH_ERROR_MARGIN_WEI) sender_shares_balance_after = lido_contracts.steth.sharesOf(lido_contracts.aragon.agent) recipients_shares_balance_after = 0 @@ -536,7 +549,7 @@ def _check_top_up_motion_enactment( assert sender_shares_balance_before >= sender_shares_balance_after assert sender_shares_balance_before - sender_shares_balance_after == recipients_shares_balance_after - recipients_shares_balance_before else: - assert sender_balance == sender_balance_before - spending + assert sender_balance == sender_balance_before - spending_in_tokens for before, now, payment in zip( recipients_balances_before, recipients_balances, top_up_amounts @@ -620,8 +633,9 @@ class AllowedRecipientsDefaultParams: @pytest.fixture(scope="module") -def allowed_recipients_registry( +def registries( AllowedRecipientsRegistry, + AllowedTokensRegistry, allowed_recipients_default_params, allowed_recipients_builder, load_deployed_contract, @@ -632,11 +646,12 @@ def allowed_recipients_registry( allowed_recipients_registry = load_deployed_contract("AllowedRecipientsRegistry") if allowed_recipients_registry is None: - tx = allowed_recipients_builder.deployAllowedRecipientsRegistry( + tx = allowed_recipients_builder.deployRegistries( allowed_recipients_default_params.limit, allowed_recipients_default_params.period_duration_months, [], [], + [], allowed_recipients_default_params.spent_amount, True, {"from": deployer}, @@ -645,6 +660,9 @@ def allowed_recipients_registry( allowed_recipients_registry = AllowedRecipientsRegistry.at( tx.events["AllowedRecipientsRegistryDeployed"]["allowedRecipientsRegistry"] ) + allowed_tokens_registry = AllowedTokensRegistry.at( + tx.events["AllowedTokensRegistryDeployed"]["allowedTokensRegistry"] + ) if not allowed_recipients_registry.hasRole( allowed_recipients_registry.ADD_RECIPIENT_TO_ALLOWED_LIST_ROLE(), @@ -666,16 +684,26 @@ def allowed_recipients_registry( {"from": lido_contracts.aragon.agent}, ) - return allowed_recipients_registry + return (allowed_recipients_registry, allowed_tokens_registry) + +@pytest.fixture(scope="module") +def add_allowed_token(registries, lido_contracts): + (_, allowed_tokens_registry) = registries + def _add_allowed_token(token): + allowed_tokens_registry.addToken(token, {"from": lido_contracts.aragon.agent}) + assert allowed_tokens_registry.isTokenAllowed(token) + return _add_allowed_token @pytest.fixture(scope="module") -def allowed_recipients_limit_params(allowed_recipients_registry): +def allowed_recipients_limit_params(registries): @dataclass class AllowedRecipientsLimits: limit: int duration: int + (allowed_recipients_registry, _) = registries + limit, duration = allowed_recipients_registry.getLimitParameters() return AllowedRecipientsLimits(limit, duration) diff --git a/tests/integration/test_allowed_recipients_happy_path.py b/tests/integration/test_allowed_recipients_happy_path.py index 63602b56..ed31bdf4 100644 --- a/tests/integration/test_allowed_recipients_happy_path.py +++ b/tests/integration/test_allowed_recipients_happy_path.py @@ -16,7 +16,8 @@ @dataclass class SingleRecipientTopUpOnlySetup: allowed_recipients_registry: AllowedRecipientsRegistry - top_up_allowed_recipients_evm_script_factory: TopUpAllowedRecipients + top_up_allowed_recipients_ldo_evm_script_factory: TopUpAllowedRecipients + top_up_allowed_recipients_usdc_evm_script_factory: TopUpAllowedRecipients @dataclass @@ -41,16 +42,16 @@ def single_recipient_top_up_only_setup( TopUpAllowedRecipients, easy_track, lido_contracts, + external_contracts, allowed_recipient, allowed_recipients_builder, allowed_recipients_default_params, deployer, ): - deploy_tx = allowed_recipients_builder.deploySingleRecipientTopUpOnlySetup( allowed_recipient.address, allowed_recipient.title, - lido_contracts.ldo, + [lido_contracts.ldo, external_contracts["usdc"]], allowed_recipients_default_params.limit, allowed_recipients_default_params.period_duration_months, allowed_recipients_default_params.spent_amount, @@ -62,11 +63,25 @@ def single_recipient_top_up_only_setup( "allowedRecipientsRegistry" ] ) - top_up_allowed_recipients_evm_script_factory = TopUpAllowedRecipients.at( - deploy_tx.events["TopUpAllowedRecipientsDeployed"]["topUpAllowedRecipients"] + top_up_allowed_recipients_ldo_evm_script_factory = TopUpAllowedRecipients.at( + deploy_tx.events["TopUpAllowedRecipientsDeployed"][0]["topUpAllowedRecipients"] + ) + top_up_allowed_recipients_usdc_evm_script_factory = TopUpAllowedRecipients.at( + deploy_tx.events["TopUpAllowedRecipientsDeployed"][1]["topUpAllowedRecipients"] + ) + + easy_track.addEVMScriptFactory( + top_up_allowed_recipients_ldo_evm_script_factory, + deployment.create_permission( + lido_contracts.aragon.finance, "newImmediatePayment" + ) + + deployment.create_permission( + allowed_recipients_registry, "updateSpentAmount" + )[2:], + {"from": lido_contracts.aragon.voting}, ) easy_track.addEVMScriptFactory( - top_up_allowed_recipients_evm_script_factory, + top_up_allowed_recipients_usdc_evm_script_factory, deployment.create_permission( lido_contracts.aragon.finance, "newImmediatePayment" ) @@ -76,7 +91,9 @@ def single_recipient_top_up_only_setup( {"from": lido_contracts.aragon.voting}, ) return SingleRecipientTopUpOnlySetup( - allowed_recipients_registry, top_up_allowed_recipients_evm_script_factory + allowed_recipients_registry, + top_up_allowed_recipients_ldo_evm_script_factory, + top_up_allowed_recipients_usdc_evm_script_factory, ) @@ -92,12 +109,14 @@ def full_setup( lido_contracts, allowed_recipients_default_params, deployer, + ldo, + usdc ): deploy_tx = allowed_recipients_builder.deployFullSetup( trusted_caller, - lido_contracts.ldo, allowed_recipients_default_params.limit, allowed_recipients_default_params.period_duration_months, + [ldo, usdc], [], [], allowed_recipients_default_params.spent_amount, @@ -111,7 +130,7 @@ def full_setup( ) add_allowed_recipient_evm_script_factory = AddAllowedRecipient.at( - deploy_tx.events["AddAllowedRecipientDeployed"]["addAllowedRecipient"] + deploy_tx.events["AddAllowedRecipientDeployed"][0]["addAllowedRecipient"] ) easy_track.addEVMScriptFactory( @@ -129,11 +148,24 @@ def full_setup( {"from": lido_contracts.aragon.voting}, ) - top_up_allowed_recipients_evm_script_factory = TopUpAllowedRecipients.at( - deploy_tx.events["TopUpAllowedRecipientsDeployed"]["topUpAllowedRecipients"] + top_up_allowed_recipients_ldo_evm_script_factory = TopUpAllowedRecipients.at( + deploy_tx.events["TopUpAllowedRecipientsDeployed"][0]["topUpAllowedRecipients"] + ) + top_up_allowed_recipients_usdc_evm_script_factory = TopUpAllowedRecipients.at( + deploy_tx.events["TopUpAllowedRecipientsDeployed"][1]["topUpAllowedRecipients"] + ) + easy_track.addEVMScriptFactory( + top_up_allowed_recipients_ldo_evm_script_factory, + deployment.create_permission( + lido_contracts.aragon.finance, "newImmediatePayment" + ) + + deployment.create_permission( + allowed_recipients_registry, "updateSpentAmount" + )[2:], + {"from": lido_contracts.aragon.voting}, ) easy_track.addEVMScriptFactory( - top_up_allowed_recipients_evm_script_factory, + top_up_allowed_recipients_usdc_evm_script_factory, deployment.create_permission( lido_contracts.aragon.finance, "newImmediatePayment" ) @@ -145,7 +177,8 @@ def full_setup( return FullSetup( allowed_recipients_registry, - top_up_allowed_recipients_evm_script_factory, + top_up_allowed_recipients_ldo_evm_script_factory, + top_up_allowed_recipients_usdc_evm_script_factory, add_allowed_recipient_evm_script_factory, remove_allowed_recipient_evm_script_factory, ) @@ -160,8 +193,8 @@ def test_single_recipient_top_up_only_setup_happy_path( allowed_recipient, new_recipient, ): - first_top_up_amount = 50 * 10 ** 18 - second_top_up_amount = 100 * 10 ** 18 + first_top_up_amount = 100 * 10**6 + second_top_up_amount = 100 * 10**18 test_helpers.advance_chain_time_to_beginning_of_the_next_period( allowed_recipients_default_params.period_duration_months @@ -170,14 +203,17 @@ def test_single_recipient_top_up_only_setup_happy_path( allowed_recipients_registry = ( single_recipient_top_up_only_setup.allowed_recipients_registry ) - top_up_allowed_recipients_evm_script_factory = ( - single_recipient_top_up_only_setup.top_up_allowed_recipients_evm_script_factory + top_up_allowed_recipients_ldo_evm_script_factory = ( + single_recipient_top_up_only_setup.top_up_allowed_recipients_ldo_evm_script_factory + ) + top_up_allowed_recipients_usdc_evm_script_factory = ( + single_recipient_top_up_only_setup.top_up_allowed_recipients_usdc_evm_script_factory ) # Top up allowed recipient top_up_allowed_recipient_by_motion( - top_up_allowed_recipients_evm_script_factory, + top_up_allowed_recipients_usdc_evm_script_factory, [allowed_recipient.address], [first_top_up_amount], ) @@ -231,7 +267,7 @@ def test_single_recipient_top_up_only_setup_happy_path( # Top up newly added recipient top_up_allowed_recipient_by_motion( - top_up_allowed_recipients_evm_script_factory, + top_up_allowed_recipients_ldo_evm_script_factory, [new_recipient.address], [second_top_up_amount], ) @@ -239,7 +275,7 @@ def test_single_recipient_top_up_only_setup_happy_path( # Validate motion creation fails if the limit was exceeded with reverts("SUM_EXCEEDS_SPENDABLE_BALANCE"): top_up_allowed_recipient_by_motion( - top_up_allowed_recipients_evm_script_factory, + top_up_allowed_recipients_ldo_evm_script_factory, [new_recipient.address], [1], ) @@ -255,8 +291,8 @@ def test_full_setup_happy_path( allowed_recipient, new_recipient, ): - first_top_up_amount = 50 * 10 ** 18 - second_top_up_amount = 100 * 10 ** 18 + first_top_up_amount = 50 * 10**6 + second_top_up_amount = 100 * 10**18 test_helpers.advance_chain_time_to_beginning_of_the_next_period( allowed_recipients_default_params.period_duration_months @@ -273,11 +309,11 @@ def test_full_setup_happy_path( ) # Top up allowed recipient by motion - top_up_allowed_recipients_evm_script_factory = ( - full_setup.top_up_allowed_recipients_evm_script_factory + top_up_allowed_recipients_ldo_evm_script_factory = ( + full_setup.top_up_allowed_recipients_ldo_evm_script_factory ) top_up_allowed_recipient_by_motion( - top_up_allowed_recipients_evm_script_factory, + top_up_allowed_recipients_ldo_evm_script_factory, [allowed_recipient.address], [first_top_up_amount], ) @@ -307,11 +343,11 @@ def test_full_setup_happy_path( ) # Top up newly allowed recipient by motion - top_up_allowed_recipients_evm_script_factory = ( - full_setup.top_up_allowed_recipients_evm_script_factory + top_up_allowed_recipients_ldo_evm_script_factory = ( + full_setup.top_up_allowed_recipients_ldo_evm_script_factory ) top_up_allowed_recipient_by_motion( - top_up_allowed_recipients_evm_script_factory, + top_up_allowed_recipients_ldo_evm_script_factory, [new_recipient.address], [second_top_up_amount], ) @@ -319,7 +355,7 @@ def test_full_setup_happy_path( # Validate motion creation cause limit was spent with reverts("SUM_EXCEEDS_SPENDABLE_BALANCE"): top_up_allowed_recipient_by_motion( - top_up_allowed_recipients_evm_script_factory, + top_up_allowed_recipients_ldo_evm_script_factory, [new_recipient.address], [1], ) diff --git a/tests/integration/test_allowed_recipients_motions.py b/tests/integration/test_allowed_recipients_motions.py index ba2d3f92..1c55a929 100644 --- a/tests/integration/test_allowed_recipients_motions.py +++ b/tests/integration/test_allowed_recipients_motions.py @@ -11,12 +11,14 @@ def test_add_recipient_motion( recipients, - allowed_recipients_registry, + registries, add_allowed_recipient_by_motion, add_allowed_recipient_evm_script_factory, ): recipient = recipients[0] + (allowed_recipients_registry, _) = registries + allowed_recipients_count_before = len( allowed_recipients_registry.getAllowedRecipients() ) @@ -38,11 +40,12 @@ def test_add_recipient_motion( def test_add_multiple_recipients_by_concurrent_motions( recipients, easy_track, - allowed_recipients_registry, + registries, enact_motion_by_creation_tx, create_add_allowed_recipient_motion, add_allowed_recipient_evm_script_factory, ): + (allowed_recipients_registry, _) = registries first_recipient, second_recipient = recipients[:2] allowed_recipients_count_before = len( @@ -125,12 +128,13 @@ def test_fail_if_add_same_recipient_twice( def test_remove_recipient_motion( recipients, - allowed_recipients_registry, + registries, add_allowed_recipient_by_motion, add_allowed_recipient_evm_script_factory, remove_allowed_recipient_evm_script_factory, remove_allowed_recipient_by_motion, ): + (allowed_recipients_registry, _) = registries allowed_recipient = recipients[0] allowed_recipients_count_before = len( @@ -161,10 +165,11 @@ def test_remove_recipient_motion( def test_fail_remove_recipient_if_empty_allowed_recipients_list( recipients, - allowed_recipients_registry, + registries, remove_allowed_recipient_by_motion, remove_allowed_recipient_evm_script_factory, ): + (allowed_recipients_registry, _) = registries allowed_recipients = allowed_recipients_registry.getAllowedRecipients() for allowed_recipient in allowed_recipients: @@ -182,12 +187,13 @@ def test_fail_remove_recipient_if_empty_allowed_recipients_list( def test_fail_remove_recipient_if_it_is_not_allowed( recipients, - allowed_recipients_registry, + registries, add_allowed_recipient_by_motion, remove_allowed_recipient_by_motion, add_allowed_recipient_evm_script_factory, remove_allowed_recipient_evm_script_factory, ): + (allowed_recipients_registry, _) = registries allowed_recipient, not_allowed_recipient = recipients[0], recipients[1] add_allowed_recipient_by_motion( @@ -209,6 +215,7 @@ def test_fail_remove_recipient_if_it_is_not_allowed( def test_top_up_single_recipient( recipients, + add_allowed_token, allowed_recipients_limit_params, add_allowed_recipient_by_motion, top_up_allowed_recipient_by_motion, @@ -223,8 +230,10 @@ def test_top_up_single_recipient( allowed_recipient.title, ) + add_allowed_token(top_up_allowed_recipients_evm_script_factory.token()) + top_up_recipient_addresses = [allowed_recipient.address] - top_up_amounts = [2 * 10 ** 18] + top_up_amounts = [2 * 10**18] test_helpers.advance_chain_time_to_beginning_of_the_next_period( allowed_recipients_limit_params.duration @@ -239,6 +248,7 @@ def test_top_up_single_recipient( def test_top_up_multiple_recipients( recipients, + add_allowed_token, allowed_recipients_limit_params, add_allowed_recipient_by_motion, top_up_allowed_recipient_by_motion, @@ -258,11 +268,13 @@ def test_top_up_multiple_recipients( allowed_recipients[1].title, ) + add_allowed_token(top_up_allowed_recipients_evm_script_factory.token()) + test_helpers.advance_chain_time_to_beginning_of_the_next_period( allowed_recipients_limit_params.duration ) - top_up_amounts = [2 * 10 ** 18, 1 * 10 ** 18] + top_up_amounts = [2 * 10**18, 1 * 10**18] top_up_allowed_recipient_by_motion( top_up_allowed_recipients_evm_script_factory, @@ -273,6 +285,7 @@ def test_top_up_multiple_recipients( def test_top_up_motion_enacted_in_next_period( recipients, + add_allowed_token, allowed_recipients_limit_params, add_allowed_recipient_by_motion, create_top_up_allowed_recipients_motion, @@ -293,6 +306,8 @@ def test_top_up_motion_enacted_in_next_period( allowed_recipients[1].title, ) + add_allowed_token(top_up_allowed_recipients_evm_script_factory.token()) + top_up_amounts = [int(3e18), int(90e18)] test_helpers.advance_chain_time_to_beginning_of_the_next_period( @@ -315,13 +330,15 @@ def test_top_up_motion_ended_and_enacted_in_next_period( recipients, easy_track, allowed_recipients_limit_params, - allowed_recipients_registry, + registries, + add_allowed_token, add_allowed_recipient_by_motion, create_top_up_allowed_recipients_motion, add_allowed_recipient_evm_script_factory, top_up_allowed_recipients_evm_script_factory, enact_top_up_allowed_recipient_motion_by_creation_tx, ): + (allowed_recipients_registry, _) = registries allowed_recipients = recipients[:2] add_allowed_recipient_by_motion( @@ -337,6 +354,8 @@ def test_top_up_motion_ended_and_enacted_in_next_period( top_up_amounts = [int(3e18), int(90e18)] + add_allowed_token(top_up_allowed_recipients_evm_script_factory.token()) + test_helpers.advance_chain_time_to_beginning_of_the_next_period( allowed_recipients_limit_params.duration ) @@ -362,6 +381,7 @@ def test_top_up_motion_ended_and_enacted_in_next_period( def test_top_up_motion_enacted_in_second_next_period( recipients, + add_allowed_token, allowed_recipients_limit_params, add_allowed_recipient_by_motion, create_top_up_allowed_recipients_motion, @@ -382,6 +402,8 @@ def test_top_up_motion_enacted_in_second_next_period( allowed_recipients[1].title, ) + add_allowed_token(top_up_allowed_recipients_evm_script_factory.token()) + top_up_amounts = [int(3e18), int(90e18)] test_helpers.advance_chain_time_to_beginning_of_the_next_period( @@ -402,12 +424,14 @@ def test_top_up_motion_enacted_in_second_next_period( def test_spendable_balance_is_renewed_in_next_period( recipients, allowed_recipients_limit_params, - allowed_recipients_registry, + registries, + add_allowed_token, add_allowed_recipient_by_motion, top_up_allowed_recipient_by_motion, add_allowed_recipient_evm_script_factory, top_up_allowed_recipients_evm_script_factory, ): + (allowed_recipients_registry, _) = registries test_helpers.advance_chain_time_to_beginning_of_the_next_period( allowed_recipients_limit_params.duration ) @@ -430,9 +454,11 @@ def test_spendable_balance_is_renewed_in_next_period( allowed_recipients[1].title, ) + add_allowed_token(top_up_allowed_recipients_evm_script_factory.token()) + top_up_amounts = [ - int(allowed_recipients_limit_params.limit // 10 ** 18 * 0.1) * 10 ** 18, - int(allowed_recipients_limit_params.limit // 10 ** 18 * 0.9) * 10 ** 18, + int(allowed_recipients_limit_params.limit // 10**18 * 0.1) * 10**18, + int(allowed_recipients_limit_params.limit // 10**18 * 0.9) * 10**18, ] top_up_allowed_recipient_by_motion( @@ -473,8 +499,37 @@ def test_spendable_balance_is_renewed_in_next_period( assert allowed_recipients_registry.spendableBalance() == 0 +def test_fail_if_token_not_allowed( + recipients, + add_allowed_recipient_by_motion, + allowed_recipients_limit_params, + create_top_up_allowed_recipients_motion, + add_allowed_recipient_evm_script_factory, + top_up_allowed_recipients_evm_script_factory, +): + allowed_recipient = recipients[0] + + add_allowed_recipient_by_motion( + add_allowed_recipient_evm_script_factory, + allowed_recipient.address, + allowed_recipient.title, + ) + + test_helpers.advance_chain_time_to_beginning_of_the_next_period( + allowed_recipients_limit_params.duration + ) + + with reverts("TOKEN_NOT_ALLOWED"): + create_top_up_allowed_recipients_motion( + top_up_allowed_recipients_evm_script_factory, + [allowed_recipient.address], + [allowed_recipients_limit_params.limit], + ) + + def test_fail_enact_top_up_motion_if_recipient_removed_by_other_motion( recipients, + add_allowed_token, allowed_recipients_limit_params, add_allowed_recipient_by_motion, remove_allowed_recipient_by_motion, @@ -501,6 +556,8 @@ def test_fail_enact_top_up_motion_if_recipient_removed_by_other_motion( allowed_recipients[1].title, ) + add_allowed_token(top_up_allowed_recipients_evm_script_factory.token()) + recipient_to_remove = allowed_recipients[0] top_up_amounts = [int(40e18), int(30e18)] @@ -520,6 +577,7 @@ def test_fail_enact_top_up_motion_if_recipient_removed_by_other_motion( def test_fail_create_top_up_motion_if_exceeds_limit( recipients, + add_allowed_token, allowed_recipients_limit_params, add_allowed_recipient_by_motion, create_top_up_allowed_recipients_motion, @@ -534,6 +592,8 @@ def test_fail_create_top_up_motion_if_exceeds_limit( allowed_recipient.title, ) + add_allowed_token(top_up_allowed_recipients_evm_script_factory.token()) + test_helpers.advance_chain_time_to_beginning_of_the_next_period( allowed_recipients_limit_params.duration ) @@ -549,6 +609,7 @@ def test_fail_create_top_up_motion_if_exceeds_limit( def test_fail_to_create_top_up_motion_which_exceeds_spendable( recipients, + add_allowed_token, allowed_recipients_limit_params, add_allowed_recipient_by_motion, top_up_allowed_recipient_by_motion, @@ -568,13 +629,15 @@ def test_fail_to_create_top_up_motion_which_exceeds_spendable( allowed_recipients[1].title, ) + add_allowed_token(top_up_allowed_recipients_evm_script_factory.token()) + test_helpers.advance_chain_time_to_beginning_of_the_next_period( allowed_recipients_limit_params.duration ) first_top_up_amounts = [ - int(allowed_recipients_limit_params.limit // 10 ** 18 * 0.4) * 10 ** 18, - int(allowed_recipients_limit_params.limit // 10 ** 18 * 0.6) * 10 ** 18, + int(allowed_recipients_limit_params.limit // 10**18 * 0.4) * 10**18, + int(allowed_recipients_limit_params.limit // 10**18 * 0.6) * 10**18, ] assert sum(first_top_up_amounts) == allowed_recipients_limit_params.limit @@ -596,6 +659,7 @@ def test_fail_to_create_top_up_motion_which_exceeds_spendable( def test_fail_2nd_top_up_motion_enactment_due_limit_but_can_enact_in_next( recipients, + add_allowed_token, allowed_recipients_limit_params, add_allowed_recipient_by_motion, create_top_up_allowed_recipients_motion, @@ -616,17 +680,19 @@ def test_fail_2nd_top_up_motion_enactment_due_limit_but_can_enact_in_next( allowed_recipients[1].title, ) + add_allowed_token(top_up_allowed_recipients_evm_script_factory.token()) + test_helpers.advance_chain_time_to_beginning_of_the_next_period( allowed_recipients_limit_params.duration ) first_top_up_amount = [ - int(allowed_recipients_limit_params.limit // 10 ** 18 * 0.4) * 10 ** 18, - int(allowed_recipients_limit_params.limit // 10 ** 18 * 0.3) * 10 ** 18, + int(allowed_recipients_limit_params.limit // 10**18 * 0.4) * 10**18, + int(allowed_recipients_limit_params.limit // 10**18 * 0.3) * 10**18, ] second_top_up_amount = [ - int(allowed_recipients_limit_params.limit // 10 ** 18 * 0.3) * 10 ** 18, - int(allowed_recipients_limit_params.limit // 10 ** 18 * 0.2) * 10 ** 18, + int(allowed_recipients_limit_params.limit // 10**18 * 0.3) * 10**18, + int(allowed_recipients_limit_params.limit // 10**18 * 0.2) * 10**18, ] assert ( @@ -656,7 +722,8 @@ def test_fail_2nd_top_up_motion_enactment_due_limit_but_can_enact_in_next( def test_fail_2nd_top_up_motion_creation_in_period_if_it_exceeds_spendable( recipients, - allowed_recipients_registry, + registries, + add_allowed_token, add_allowed_recipient_by_motion, allowed_recipients_limit_params, top_up_allowed_recipient_by_motion, @@ -665,6 +732,7 @@ def test_fail_2nd_top_up_motion_creation_in_period_if_it_exceeds_spendable( ): """Revert 2nd payout which together with 1st payout exceed the current period limit""" + (allowed_recipients_registry, _) = registries allowed_recipients = recipients[:2] add_allowed_recipient_by_motion( @@ -678,17 +746,19 @@ def test_fail_2nd_top_up_motion_creation_in_period_if_it_exceeds_spendable( allowed_recipients[1].title, ) + add_allowed_token(top_up_allowed_recipients_evm_script_factory.token()) + test_helpers.advance_chain_time_to_beginning_of_the_next_period( allowed_recipients_limit_params.duration ) first_top_up_amounts = [ - int(allowed_recipients_limit_params.limit // 10 ** 18 * 0.03) * 10 ** 18, - int(allowed_recipients_limit_params.limit // 10 ** 18 * 0.9) * 10 ** 18, + int(allowed_recipients_limit_params.limit // 10**18 * 0.03) * 10**18, + int(allowed_recipients_limit_params.limit // 10**18 * 0.9) * 10**18, ] second_top_up_amounts = [ - int(allowed_recipients_limit_params.limit // 10 ** 18 * 0.05) * 10 ** 18, - int(allowed_recipients_limit_params.limit // 10 ** 18 * 0.04) * 10 ** 18, + int(allowed_recipients_limit_params.limit // 10**18 * 0.05) * 10**18, + int(allowed_recipients_limit_params.limit // 10**18 * 0.04) * 10**18, ] assert ( @@ -715,7 +785,8 @@ def test_fail_2nd_top_up_motion_creation_in_period_if_it_exceeds_spendable( def test_fail_top_up_if_limit_decreased_while_motion_is_in_flight( recipients, lido_contracts, - allowed_recipients_registry, + registries, + add_allowed_token, allowed_recipients_limit_params, add_allowed_recipient_by_motion, create_top_up_allowed_recipients_motion, @@ -723,6 +794,7 @@ def test_fail_top_up_if_limit_decreased_while_motion_is_in_flight( top_up_allowed_recipients_evm_script_factory, enact_top_up_allowed_recipient_motion_by_creation_tx, ): + (allowed_recipients_registry, _) = registries allowed_recipients = recipients[:1] add_allowed_recipient_by_motion( @@ -731,6 +803,8 @@ def test_fail_top_up_if_limit_decreased_while_motion_is_in_flight( allowed_recipients[0].title, ) + add_allowed_token(top_up_allowed_recipients_evm_script_factory.token()) + test_helpers.advance_chain_time_to_beginning_of_the_next_period( allowed_recipients_limit_params.duration ) @@ -755,7 +829,8 @@ def test_fail_top_up_if_limit_decreased_while_motion_is_in_flight( def test_top_up_if_limit_increased_while_motion_is_in_flight( recipients, lido_contracts, - allowed_recipients_registry, + registries, + add_allowed_token, add_allowed_recipient_by_motion, allowed_recipients_limit_params, create_top_up_allowed_recipients_motion, @@ -763,6 +838,7 @@ def test_top_up_if_limit_increased_while_motion_is_in_flight( top_up_allowed_recipients_evm_script_factory, enact_top_up_allowed_recipient_motion_by_creation_tx, ): + (allowed_recipients_registry, _) = registries allowed_recipients = recipients[:1] add_allowed_recipient_by_motion( @@ -771,6 +847,8 @@ def test_top_up_if_limit_increased_while_motion_is_in_flight( allowed_recipients[0].title, ) + add_allowed_token(top_up_allowed_recipients_evm_script_factory.token()) + test_helpers.advance_chain_time_to_beginning_of_the_next_period( allowed_recipients_limit_params.duration ) @@ -796,7 +874,8 @@ def test_two_motion_seconds_failed_to_enact_due_limit_but_succeeded_after_limit_ recipients, lido_contracts, enact_motion_by_creation_tx, - allowed_recipients_registry, + registries, + add_allowed_token, add_allowed_recipient_by_motion, allowed_recipients_limit_params, create_top_up_allowed_recipients_motion, @@ -804,6 +883,7 @@ def test_two_motion_seconds_failed_to_enact_due_limit_but_succeeded_after_limit_ top_up_allowed_recipients_evm_script_factory, enact_top_up_allowed_recipient_motion_by_creation_tx, ): + (allowed_recipients_registry, _) = registries allowed_recipients = recipients[:2] add_allowed_recipient_by_motion( @@ -817,13 +897,15 @@ def test_two_motion_seconds_failed_to_enact_due_limit_but_succeeded_after_limit_ allowed_recipients[1].title, ) + add_allowed_token(top_up_allowed_recipients_evm_script_factory.token()) + test_helpers.advance_chain_time_to_beginning_of_the_next_period( allowed_recipients_limit_params.duration ) first_top_up_amounts = [ - int(allowed_recipients_limit_params.limit // 10 ** 18 * 0.4) * 10 ** 18, - int(allowed_recipients_limit_params.limit // 10 ** 18 * 0.6) * 10 ** 18, + int(allowed_recipients_limit_params.limit // 10**18 * 0.4) * 10**18, + int(allowed_recipients_limit_params.limit // 10**18 * 0.6) * 10**18, ] assert sum(first_top_up_amounts) == allowed_recipients_limit_params.limit second_top_up_amounts = [1, 1] @@ -863,7 +945,8 @@ def test_two_motion_seconds_failed_to_enact_due_limit_but_succeeded_after_limit_ ) def test_top_up_spendable_renewal_if_period_duration_changed( recipients, - allowed_recipients_registry, + registries, + add_allowed_token, add_allowed_recipient_by_motion, lido_contracts, create_top_up_allowed_recipients_motion, @@ -873,7 +956,8 @@ def test_top_up_spendable_renewal_if_period_duration_changed( initial_period_duration: int, new_period_duration: int, ): - period_limit = 100 * 10 ** 18 + (allowed_recipients_registry, _) = registries + period_limit = 100 * 10**18 allowed_recipients = recipients[:1] add_allowed_recipient_by_motion( @@ -882,6 +966,8 @@ def test_top_up_spendable_renewal_if_period_duration_changed( allowed_recipients[0].title, ) + add_allowed_token(top_up_allowed_recipients_evm_script_factory.token()) + first_top_up_amount = [period_limit] second_top_up_amount = [1] # just 1 wei @@ -929,11 +1015,10 @@ def test_top_up_spendable_renewal_if_period_duration_changed( ) -def test_set_limit_parameters_by_aragon_agent_via_voting( - lido_contracts, allowed_recipients_registry -): +def test_set_limit_parameters_by_aragon_agent_via_voting(lido_contracts, registries): """Do Aragon Agent to set limit parameters to the allowed recipients registry""" - period_limit, period_duration = 100 * 10 ** 18, 6 + period_limit, period_duration = 100 * 10**18, 6 + (allowed_recipients_registry, _) = registries set_limit_parameters_voting_id, _ = lido_contracts.create_voting( evm_script=evm_script.encode_call_script( diff --git a/tests/test_allowed_recipients_builder.py b/tests/test_allowed_recipients_builder.py index a00a2759..174b1d78 100644 --- a/tests/test_allowed_recipients_builder.py +++ b/tests/test_allowed_recipients_builder.py @@ -1,5 +1,6 @@ import pytest from brownie import Contract, reverts +from utils.test_helpers import ADD_TOKEN_TO_ALLOWED_LIST_ROLE, REMOVE_TOKEN_FROM_ALLOWED_LIST_ROLE ADD_RECIPIENT_TO_ALLOWED_LIST_ROLE = ( "0xec20c52871c824e5437859e75ac830e83aaaaeb7b0ffd850de830ddd3e385276" @@ -73,10 +74,11 @@ def test_deploy_top_up_allowed_recipients( TopUpAllowedRecipients, ): trusted_caller = accounts[3] - registry = accounts[4] + recipients_registry = accounts[4] + tokens_registry = accounts[4] tx = allowed_recipients_builder.deployTopUpAllowedRecipients( - trusted_caller, registry, ldo, {"from": stranger} + trusted_caller, recipients_registry, tokens_registry, ldo, {"from": stranger} ) top_up_address = tx.events["TopUpAllowedRecipientsDeployed"][ @@ -92,7 +94,7 @@ def test_deploy_top_up_allowed_recipients( ) assert ( tx.events["TopUpAllowedRecipientsDeployed"]["allowedRecipientsRegistry"] - == registry + == recipients_registry ) assert tx.events["TopUpAllowedRecipientsDeployed"]["finance"] == finance assert tx.events["TopUpAllowedRecipientsDeployed"]["token"] == ldo @@ -102,7 +104,8 @@ def test_deploy_top_up_allowed_recipients( "TopUpAllowedRecipients", top_up_address, TopUpAllowedRecipients.abi ) assert topUpAllowedRecipients.token() == ldo - assert topUpAllowedRecipients.allowedRecipientsRegistry() == registry + assert topUpAllowedRecipients.allowedRecipientsRegistry() == recipients_registry + assert topUpAllowedRecipients.allowedTokensRegistry() == tokens_registry assert topUpAllowedRecipients.trustedCaller() == trusted_caller assert topUpAllowedRecipients.finance() == finance assert topUpAllowedRecipients.token() == ldo @@ -182,87 +185,103 @@ def test_deploy_allowed_recipients_registry( accounts, stranger, agent, + ldo, + usdc, evm_script_executor, AllowedRecipientsRegistry, ): limit = 1e18 period = 1 recipients = [accounts[3], accounts[4]] + tokens = [ldo, usdc] titles = ["account 3", "account 4"] spentAmount = 1e10 - tx = allowed_recipients_builder.deployAllowedRecipientsRegistry( - limit, period, recipients, titles, spentAmount, True, {"from": stranger} + tx = allowed_recipients_builder.deployRegistries( + limit, period, tokens, recipients, titles, spentAmount, True, {"from": stranger} ) - registry_address = tx.events["AllowedRecipientsRegistryDeployed"][ + recipient_registry_address = tx.events["AllowedRecipientsRegistryDeployed"][ "allowedRecipientsRegistry" ] + token_registry_address = tx.events["AllowedTokensRegistryDeployed"][ + "allowedTokensRegistry" + ] - registry = Contract.from_abi( - "AllowedRecipientsRegistry", registry_address, AllowedRecipientsRegistry.abi + recipient_registry = Contract.from_abi( + "AllowedRecipientsRegistry", recipient_registry_address, AllowedRecipientsRegistry.abi + ) + token_registry = Contract.from_abi( + "AllowedTokensRegistry", token_registry_address, AllowedRecipientsRegistry.abi ) - assert len(registry.getAllowedRecipients()) == len(recipients) + assert len(recipient_registry.getAllowedRecipients()) == len(recipients) for recipient in recipients: - assert registry.isRecipientAllowed(recipient) + assert recipient_registry.isRecipientAllowed(recipient) - registry_limit, registry_period_duration = registry.getLimitParameters() + registry_limit, registry_period_duration = recipient_registry.getLimitParameters() assert registry_limit == limit assert registry_period_duration == period - assert registry.spendableBalance() == limit - spentAmount + assert recipient_registry.spendableBalance() == limit - spentAmount - assert registry.hasRole(ADD_RECIPIENT_TO_ALLOWED_LIST_ROLE, agent) - assert registry.hasRole(REMOVE_RECIPIENT_FROM_ALLOWED_LIST_ROLE, agent) - assert registry.hasRole(SET_PARAMETERS_ROLE, agent) - assert registry.hasRole(UPDATE_SPENT_AMOUNT_ROLE, agent) - assert registry.hasRole(DEFAULT_ADMIN_ROLE, agent) + assert recipient_registry.hasRole(ADD_RECIPIENT_TO_ALLOWED_LIST_ROLE, agent) + assert recipient_registry.hasRole(REMOVE_RECIPIENT_FROM_ALLOWED_LIST_ROLE, agent) + assert recipient_registry.hasRole(SET_PARAMETERS_ROLE, agent) + assert recipient_registry.hasRole(UPDATE_SPENT_AMOUNT_ROLE, agent) + assert recipient_registry.hasRole(DEFAULT_ADMIN_ROLE, agent) - assert registry.hasRole(ADD_RECIPIENT_TO_ALLOWED_LIST_ROLE, evm_script_executor) - assert registry.hasRole( + assert recipient_registry.hasRole(ADD_RECIPIENT_TO_ALLOWED_LIST_ROLE, evm_script_executor) + assert recipient_registry.hasRole( REMOVE_RECIPIENT_FROM_ALLOWED_LIST_ROLE, evm_script_executor ) - assert registry.hasRole(UPDATE_SPENT_AMOUNT_ROLE, evm_script_executor) - assert not registry.hasRole(SET_PARAMETERS_ROLE, evm_script_executor) - assert not registry.hasRole(DEFAULT_ADMIN_ROLE, evm_script_executor) + assert recipient_registry.hasRole(UPDATE_SPENT_AMOUNT_ROLE, evm_script_executor) + assert not recipient_registry.hasRole(SET_PARAMETERS_ROLE, evm_script_executor) + assert not recipient_registry.hasRole(DEFAULT_ADMIN_ROLE, evm_script_executor) - assert not registry.hasRole(ADD_RECIPIENT_TO_ALLOWED_LIST_ROLE, registry_address) - assert not registry.hasRole( - REMOVE_RECIPIENT_FROM_ALLOWED_LIST_ROLE, registry_address + assert not recipient_registry.hasRole(ADD_RECIPIENT_TO_ALLOWED_LIST_ROLE, recipient_registry_address) + assert not recipient_registry.hasRole( + REMOVE_RECIPIENT_FROM_ALLOWED_LIST_ROLE, recipient_registry_address ) - assert not registry.hasRole(SET_PARAMETERS_ROLE, registry_address) - assert not registry.hasRole(UPDATE_SPENT_AMOUNT_ROLE, registry_address) - assert not registry.hasRole(DEFAULT_ADMIN_ROLE, registry_address) + assert not recipient_registry.hasRole(SET_PARAMETERS_ROLE, recipient_registry_address) + assert not recipient_registry.hasRole(UPDATE_SPENT_AMOUNT_ROLE, recipient_registry_address) + assert not recipient_registry.hasRole(DEFAULT_ADMIN_ROLE, recipient_registry_address) + + assert token_registry.hasRole(ADD_TOKEN_TO_ALLOWED_LIST_ROLE, agent) + assert token_registry.hasRole(REMOVE_TOKEN_FROM_ALLOWED_LIST_ROLE, agent) + assert token_registry.hasRole(ADD_TOKEN_TO_ALLOWED_LIST_ROLE, evm_script_executor) + assert token_registry.hasRole(REMOVE_TOKEN_FROM_ALLOWED_LIST_ROLE, evm_script_executor) def test_deploy_recipients_registry_reverts_recipients_length( - allowed_recipients_builder, accounts, stranger + allowed_recipients_builder, accounts, stranger, ldo ): limit = 1e18 period = 1 recipients = [accounts[3], accounts[4]] + tokens = [ldo] titles = ["account 3"] spentAmount = 1e10 with reverts(): - allowed_recipients_builder.deployAllowedRecipientsRegistry( - limit, period, recipients, titles, spentAmount, False, {"from": stranger} + allowed_recipients_builder.deployRegistries( + limit, period, tokens, recipients, titles, spentAmount, False, {"from": stranger} ) def test_deploy_recipients_registry_reverts_spentAmount_gt_limit( - allowed_recipients_builder, accounts, stranger + allowed_recipients_builder, accounts, stranger, ldo ): limit = 1e5 period = 1 + tokens = [ldo] recipients = [accounts[3], accounts[4]] titles = ["account 3", "account 4"] spentAmount = 1e10 with reverts("_spentAmount must be lower or equal to limit"): - allowed_recipients_builder.deployAllowedRecipientsRegistry( - limit, period, recipients, titles, spentAmount, False, {"from": stranger} + allowed_recipients_builder.deployRegistries( + limit, period, tokens, recipients, titles, spentAmount, False, {"from": stranger} ) @@ -299,9 +318,9 @@ def test_deploy_full_setup( tx = allowed_recipients_builder.deployFullSetup( trusted_caller, - ldo, limit, period, + [ldo], recipients, titles, spent_amount, @@ -398,7 +417,7 @@ def test_deploy_deploy_single_recipient_top_up_only_setup( spent_amount = 1e10 tx = allowed_recipients_builder.deploySingleRecipientTopUpOnlySetup( - recipient, title, ldo, limit, period, spent_amount, {"from": stranger} + recipient, title, [ldo], limit, period, spent_amount, {"from": stranger} ) registry_address = tx.events["AllowedRecipientsRegistryDeployed"][ diff --git a/tests/test_allowed_tokens_registry.py b/tests/test_allowed_tokens_registry.py new file mode 100644 index 00000000..c9521b21 --- /dev/null +++ b/tests/test_allowed_tokens_registry.py @@ -0,0 +1,244 @@ +import pytest + +from brownie import reverts, ZERO_ADDRESS, MockERC20, accounts + +from utils.test_helpers import ( + access_revert_message, + ADD_TOKEN_TO_ALLOWED_LIST_ROLE, + REMOVE_TOKEN_FROM_ALLOWED_LIST_ROLE, +) + +# ------------ +# constructor +# ------------ + + +def test_registry_initial_state(AllowedTokensRegistry, accounts, owner): + add_token_role_holder = accounts[6] + remove_token_role_holder = accounts[7] + + registry = owner.deploy( + AllowedTokensRegistry, + owner, + [add_token_role_holder], + [remove_token_role_holder], + ) + + assert registry.hasRole(ADD_TOKEN_TO_ALLOWED_LIST_ROLE, add_token_role_holder) + assert registry.hasRole( + REMOVE_TOKEN_FROM_ALLOWED_LIST_ROLE, remove_token_role_holder + ) + + for role_holder in [add_token_role_holder, remove_token_role_holder]: + assert not registry.hasRole(registry.DEFAULT_ADMIN_ROLE(), role_holder) + + assert len(registry.getAllowedTokens()) == 0 + + +def test_registry_zero_admin_allowed(AllowedTokensRegistry, owner): + """Checking no revert""" + owner.deploy(AllowedTokensRegistry, ZERO_ADDRESS, [owner], [owner]) + + +def test_registry_none_role_holders_allowed(AllowedTokensRegistry, owner): + """Checking no revert""" + owner.deploy(AllowedTokensRegistry, owner, [], []) + + +# ------------ +# access control +# ------------ + + +def test_rights_are_not_shared_by_different_roles( + AllowedTokensRegistry, + owner, + stranger, + voting, + ldo, + accounts, +): + deployer = owner + add_role_holder = accounts[6] + remove_role_holder = accounts[7] + + registry = deployer.deploy( + AllowedTokensRegistry, voting, [add_role_holder], [remove_role_holder] + ) + assert registry.hasRole(registry.DEFAULT_ADMIN_ROLE(), voting) + + for caller in [ + deployer, + remove_role_holder, + stranger, + ]: + with reverts(access_revert_message(caller, ADD_TOKEN_TO_ALLOWED_LIST_ROLE)): + registry.addToken(ldo, {"from": caller}) + + for caller in [ + deployer, + add_role_holder, + stranger, + ]: + with reverts( + access_revert_message(caller, REMOVE_TOKEN_FROM_ALLOWED_LIST_ROLE) + ): + registry.removeToken(ldo, {"from": caller}) + + +def test_multiple_role_holders( + AllowedTokensRegistry, owner, voting, accounts, ldo, steth +): + deployer = owner + add_role_holders = (accounts[2], accounts[3]) + remove_role_holders = (accounts[4], accounts[5]) + + registry = deployer.deploy( + AllowedTokensRegistry, voting, add_role_holders, remove_role_holders + ) + + for caller in accounts: + if not caller in add_role_holders: + with reverts(access_revert_message(caller, ADD_TOKEN_TO_ALLOWED_LIST_ROLE)): + registry.addToken(ldo, {"from": caller}) + + registry.addToken(ldo, {"from": add_role_holders[0]}) + registry.addToken(steth, {"from": add_role_holders[1]}) + + for caller in accounts: + if not caller in remove_role_holders: + with reverts( + access_revert_message(caller, REMOVE_TOKEN_FROM_ALLOWED_LIST_ROLE) + ): + registry.removeToken(ldo, {"from": caller}) + + registry.removeToken(ldo, {"from": remove_role_holders[0]}) + registry.removeToken(steth, {"from": remove_role_holders[1]}) + + +# ------------ +# logic +# ------------ + + + +def test_add_tokens(allowed_tokens_registry, ldo): + (registry, _, add_token_role_holder, _) = allowed_tokens_registry + + registry.addToken(ldo, {"from": add_token_role_holder}) + + assert registry.isTokenAllowed(ldo) + assert len(registry.getAllowedTokens()) == 1 + assert registry.getAllowedTokens()[0] == ldo + + +def test_add_multiple_tokens(allowed_tokens_registry, ldo, steth): + (registry, _, add_token_role_holder, _) = allowed_tokens_registry + + registry.addToken(ldo, {"from": add_token_role_holder}) + registry.addToken(steth, {"from": add_token_role_holder}) + + assert registry.isTokenAllowed(ldo) + assert registry.isTokenAllowed(steth) + assert len(registry.getAllowedTokens()) == 2 + assert registry.getAllowedTokens()[0] == ldo + assert registry.getAllowedTokens()[1] == steth + + +def test_add_zero_token(allowed_tokens_registry): + (registry, _, add_token_role_holder, _) = allowed_tokens_registry + + with reverts("TOKEN_ADDRESS_IS_ZERO"): + registry.addToken(ZERO_ADDRESS, {"from": add_token_role_holder}) + + +def test_add_the_same_token(allowed_tokens_registry, ldo): + (registry, _, add_token_role_holder, _) = allowed_tokens_registry + + registry.addToken(ldo, {"from": add_token_role_holder}) + + with reverts("TOKEN_ALREADY_ADDED_TO_ALLOWED_LIST"): + registry.addToken(ldo, {"from": add_token_role_holder}) + + +def test_remove_token(allowed_tokens_registry, ldo): + (registry, _, add_token_holder, remove_token_role_holder) = allowed_tokens_registry + + registry.addToken(ldo, {"from": add_token_holder}) + + assert registry.isTokenAllowed(ldo) + assert len(registry.getAllowedTokens()) == 1 + + registry.removeToken(ldo, {"from": remove_token_role_holder}) + + assert not registry.isTokenAllowed(ldo) + assert len(registry.getAllowedTokens()) == 0 + + +def test_remove_multiple_tokens(allowed_tokens_registry, ldo, steth): + (registry, _, add_token_holder, remove_token_role_holder) = allowed_tokens_registry + + registry.addToken(ldo, {"from": add_token_holder}) + registry.addToken(steth, {"from": add_token_holder}) + + assert registry.isTokenAllowed(ldo) + assert registry.isTokenAllowed(steth) + assert len(registry.getAllowedTokens()) == 2 + + registry.removeToken(ldo, {"from": remove_token_role_holder}) + + assert not registry.isTokenAllowed(ldo) + assert registry.isTokenAllowed(steth) + assert len(registry.getAllowedTokens()) == 1 + + registry.removeToken(steth, {"from": remove_token_role_holder}) + + assert not registry.isTokenAllowed(ldo) + assert not registry.isTokenAllowed(steth) + assert len(registry.getAllowedTokens()) == 0 + + +def test_remove_the_same_token(allowed_tokens_registry, ldo): + (registry, _, add_token_holder, remove_token_role_holder) = allowed_tokens_registry + + registry.addToken(ldo, {"from": add_token_holder}) + + assert registry.isTokenAllowed(ldo) + assert len(registry.getAllowedTokens()) == 1 + + registry.removeToken(ldo, {"from": remove_token_role_holder}) + + assert not registry.isTokenAllowed(ldo) + assert len(registry.getAllowedTokens()) == 0 + + with reverts("TOKEN_NOT_FOUND_IN_ALLOWED_LIST"): + registry.removeToken(ldo, {"from": remove_token_role_holder}) + + +def test_remove_not_existing_token(allowed_tokens_registry, ldo): + (registry, _, _, remove_token_role_holder) = allowed_tokens_registry + + with reverts("TOKEN_NOT_FOUND_IN_ALLOWED_LIST"): + registry.removeToken(ldo, {"from": remove_token_role_holder}) + + +def test_normalize_amount(allowed_tokens_registry): + (registry, _, _, _) = allowed_tokens_registry + + erc20decimals18 = MockERC20.deploy(18, {"from": accounts[0]}) + + with reverts("TOKEN_ADDRESS_IS_ZERO"): + registry.normalizeAmount(1, ZERO_ADDRESS) + + + amount1 = 1000000000000000000 + assert registry.normalizeAmount(amount1, erc20decimals18) == amount1 + + erc20decimals21 = MockERC20.deploy(21, {"from": accounts[0]}) + amount2 = 1000000000000000000000 + assert registry.normalizeAmount(amount2, erc20decimals21) == amount1 + assert registry.normalizeAmount(amount2 + 1, erc20decimals21) == amount1 + 1 + + erc20decimals12 = MockERC20.deploy(12, {"from": accounts[0]}) + amount3 = 1000000000000 # 1 token with 12 decimals + assert registry.normalizeAmount(amount3, erc20decimals12) == amount1 diff --git a/utils/lido.py b/utils/lido.py index 55f4fe64..b425dd56 100644 --- a/utils/lido.py +++ b/utils/lido.py @@ -37,6 +37,20 @@ def addresses(network=DEFAULT_NETWORK): ) +def external_contracts(network=DEFAULT_NETWORK): + if network == "mainnet" or network == "mainnet-fork": + return { + "usdc": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + } + if network == "goerli" or network == "goerli-fork": + return { + "usdc": "0x07865c6E87B9F70255377e024ace6630C1Eaa37F" + } + raise NameError( + f"""Unknown network "{network}". Supported networks: mainnet, mainnet-fork goerli, goerli-fork""" + ) + + def contracts(network=DEFAULT_NETWORK): return LidoContractsSetup(brownie.interface, lido_addresses=addresses(network)) diff --git a/utils/test_helpers.py b/utils/test_helpers.py index ee0ada4a..6833b6ae 100644 --- a/utils/test_helpers.py +++ b/utils/test_helpers.py @@ -16,6 +16,12 @@ REMOVE_RECIPIENT_FROM_ALLOWED_LIST_ROLE = ( "0x491d7752c25cfca0f73715cde1130022a9b815373f91a996bbb1ba8943efc99b" ) +ADD_TOKEN_TO_ALLOWED_LIST_ROLE = ( + "0xf171689cfd5919fb6ea45c7db72005f66d37a9d2ecad9a9102caf8177435cf54" +) +REMOVE_TOKEN_FROM_ALLOWED_LIST_ROLE = ( + "0x9328ef869700347b81959a69acbca4adf93a9ee617e796e5692b2660ee007a81" +) PERMISSION_ERROR_TEMPLATE = "AccessControl: account %s is missing role %s"