Skip to content

Commit

Permalink
feat: modify top up factories to work with different stablecoins
Browse files Browse the repository at this point in the history
  • Loading branch information
rkolpakov committed Oct 6, 2023
1 parent 1a8d519 commit a3e9ac3
Show file tree
Hide file tree
Showing 16 changed files with 1,139 additions and 293 deletions.
236 changes: 115 additions & 121 deletions contracts/AllowedRecipientsBuilder.sol

Large diffs are not rendered by default.

33 changes: 33 additions & 0 deletions contracts/AllowedRecipientsFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -75,16 +85,38 @@ 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
) public returns (TopUpAllowedRecipients topUpAllowedRecipients) {
topUpAllowedRecipients = new TopUpAllowedRecipients(
_trustedCaller,
_allowedRecipientsRegistry,
_allowedTokensRegistry,
_finance,
_token,
_easyTrack
Expand All @@ -95,6 +127,7 @@ contract AllowedRecipientsFactory {
address(topUpAllowedRecipients),
_trustedCaller,
_allowedRecipientsRegistry,
_allowedTokensRegistry,
_finance,
_token,
_easyTrack
Expand Down
127 changes: 127 additions & 0 deletions contracts/AllowedTokensRegistry.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// SPDX-FileCopyrightText: 2022 Lido <info@lido.fi>
// 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;
}
}
28 changes: 13 additions & 15 deletions contracts/EVMScriptFactories/TopUpAllowedRecipients.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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";

Expand All @@ -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
// -------------
Expand All @@ -50,13 +55,15 @@ contract TopUpAllowedRecipients is TrustedCaller, IEVMScriptFactory {
constructor(
address _trustedCaller,
address _allowedRecipientsRegistry,
address _allowedTokensRegistry,
address _finance,
address _token,
address _easyTrack
) TrustedCaller(_trustedCaller) {
finance = IFinance(_finance);
token = _token;
allowedRecipientsRegistry = AllowedRecipientsRegistry(_allowedRecipientsRegistry);
allowedTokensRegistry = AllowedTokensRegistry(_allowedTokensRegistry);
easyTrack = EasyTrack(_easyTrack);
}

Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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)
Expand Down
13 changes: 13 additions & 0 deletions contracts/test/MockERC20.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// SPDX-FileCopyrightText: 2021 Lido <info@lido.fi>
// SPDX-License-Identifier: GPL-3.0

pragma solidity ^0.8.4;

contract MockERC20 {

uint8 public decimals;

constructor(uint8 _decimals) {
decimals = _decimals;
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
Loading

0 comments on commit a3e9ac3

Please sign in to comment.