From 73119518ff27c19471f97053d7fa1307697dc22a Mon Sep 17 00:00:00 2001 From: Foivos Date: Tue, 3 Oct 2023 12:55:08 +0300 Subject: [PATCH] feat: another example (#107) * renamed folder and changed version * npmignore * npmignore * change version * using include pattern instead. * Fixed most of the things least auhority suggested. * made lint happy * Apply suggestions from code review * fixed some bugs * added events * rename set to transfer for distributor and operator * changed standardized token to always allow token managers to mint/burn it. * using immutable storage for remoteAddressValidator address to save gas * Added some recommended changes * added milap's suggested changes * Fixed some names and some minor gas optimizations * prettier and lint * stash * import .env in hardhat.config * trying to fix .env.example * Added some getters in IRemoteAddressValidator and removed useless check for distributor in the InterchainTokenService. * removed ternary operators * made lint happy * made lint happy * Added a new token manager to handle fee on transfer and added some tests for it as well * fixed the liquidity pool check. * fix a duplication bug * lint * added some more tests * Added more tests * Added proper re-entrancy protection for fee on transfer token managers. * change to tx.origin for refunds * Added support for more kinds of addresses. * some minor gas opts * some more gas optimizations. * Added a getter for chain name to the remote address validator. * moved the tokenManager getter functionality to a separate contract which saves almost a kilobyte of codesize. * made lint happy * Removed tokenManagerGetter and put params into tokenManagers * Added separate tokenManager interfaces * addressed ackeeblockchains's 3.0 report * prettier * added interchain transfer methods to the service and unified receiving tokens a bit. * made lint happy * rename sendToken to interchainTransfer * changed sendToken everywhere * changed from uint256.max to a const * change setting to zero to delete for storage slots. * rearange storage variables to save a bit of gas. * Removed unecesairy casts * made as many event params inexed as possible * Removed unused imports * domain separator is calculated each time. * added some natspec * added an example for using pre-existing custom tokens. * added a comment * feat(TokenManager): added MintBurnFrom and MintBurnFromAddress (#108) * feat(TokenManager): MintBurnFrom and MintBurnFromAddress * fix(TokenManager): removed MintBurnFromAddress as deprecated * Update contracts/interfaces/IERC20BurnableFrom.sol --------- Co-authored-by: Milap Sheth --------- Co-authored-by: Milap Sheth Co-authored-by: Kiryl Yermakou --- contracts/interfaces/IERC20BurnableFrom.sol | 25 ++++ ...intable.sol => IERC20MintableBurnable.sol} | 6 +- contracts/interfaces/IStandardizedToken.sol | 4 +- contracts/interfaces/ITokenManagerType.sol | 3 +- contracts/test/FeeOnTransferTokenTest.sol | 4 +- contracts/test/InterchainTokenTest.sol | 4 +- .../StandardizedToken.sol | 4 +- contracts/token-manager/TokenManager.sol | 24 +++- .../TokenManagerAddressStorage.sol | 41 ------- .../TokenManagerLiquidityPool.sol | 12 +- .../TokenManagerLockUnlock.sol | 15 ++- .../TokenManagerLockUnlockFeeOnTransfer.sol | 13 +- .../implementations/TokenManagerMintBurn.sol | 22 ++-- .../TokenManagerMintBurnFrom.sol | 44 +++++++ contracts/utils/NoReEntrancy.sol | 10 +- scripts/deploy.js | 2 +- test/tokenService.js | 41 ++++--- test/tokenServiceFullFlow.js | 113 +++++++++++++++++- 18 files changed, 276 insertions(+), 111 deletions(-) create mode 100644 contracts/interfaces/IERC20BurnableFrom.sol rename contracts/interfaces/{IERC20BurnableMintable.sol => IERC20MintableBurnable.sol} (79%) delete mode 100644 contracts/token-manager/implementations/TokenManagerAddressStorage.sol create mode 100644 contracts/token-manager/implementations/TokenManagerMintBurnFrom.sol diff --git a/contracts/interfaces/IERC20BurnableFrom.sol b/contracts/interfaces/IERC20BurnableFrom.sol new file mode 100644 index 00000000..5c086542 --- /dev/null +++ b/contracts/interfaces/IERC20BurnableFrom.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +/** + * @dev Interface of the ERC20 standard as defined in the EIP. + */ +interface IERC20BurnableFrom { + /** + * @notice Function to burn tokens from a burn deposit address + * @notice It is needed to support legacy Axelar Gateway tokens + * @dev Can only be called after token is transferred to a deposit address. + * @param salt The address that will have its tokens burnt + */ + function burn(bytes32 salt) external; + + /** + * @notice Function to burn tokens + * @notice Requires the caller to have allowance for `amount` on `from` + * @dev Can only be called by the distributor address. + * @param from The address that will have its tokens burnt + * @param amount The amount of tokens to burn + */ + function burnFrom(address from, uint256 amount) external; +} diff --git a/contracts/interfaces/IERC20BurnableMintable.sol b/contracts/interfaces/IERC20MintableBurnable.sol similarity index 79% rename from contracts/interfaces/IERC20BurnableMintable.sol rename to contracts/interfaces/IERC20MintableBurnable.sol index 12925185..f6fe5049 100644 --- a/contracts/interfaces/IERC20BurnableMintable.sol +++ b/contracts/interfaces/IERC20MintableBurnable.sol @@ -5,10 +5,10 @@ pragma solidity ^0.8.0; /** * @dev Interface of the ERC20 standard as defined in the EIP. */ -interface IERC20BurnableMintable { +interface IERC20MintableBurnable { /** * @notice Function to mint new tokens - * Can only be called by the distributor address. + * @dev Can only be called by the distributor address. * @param to The address that will receive the minted tokens * @param amount The amount of tokens to mint */ @@ -16,7 +16,7 @@ interface IERC20BurnableMintable { /** * @notice Function to burn tokens - * Can only be called by the distributor address. + * @dev Can only be called by the distributor address. * @param from The address that will have its tokens burnt * @param amount The amount of tokens to burn */ diff --git a/contracts/interfaces/IStandardizedToken.sol b/contracts/interfaces/IStandardizedToken.sol index 79b61315..9763a2ff 100644 --- a/contracts/interfaces/IStandardizedToken.sol +++ b/contracts/interfaces/IStandardizedToken.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.0; import { IInterchainToken } from './IInterchainToken.sol'; import { IDistributable } from './IDistributable.sol'; -import { IERC20BurnableMintable } from './IERC20BurnableMintable.sol'; +import { IERC20MintableBurnable } from './IERC20MintableBurnable.sol'; import { ITokenManager } from './ITokenManager.sol'; import { IERC20 } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IERC20.sol'; @@ -13,7 +13,7 @@ import { IERC20 } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interf * @notice This contract implements a standardized token which extends InterchainToken functionality. * This contract also inherits Distributable and Implementation logic. */ -interface IStandardizedToken is IInterchainToken, IDistributable, IERC20BurnableMintable, IERC20 { +interface IStandardizedToken is IInterchainToken, IDistributable, IERC20MintableBurnable, IERC20 { /** * @notice Returns the contract id, which a proxy can check to ensure no false implementation was used. */ diff --git a/contracts/interfaces/ITokenManagerType.sol b/contracts/interfaces/ITokenManagerType.sol index b9a42ade..925bff8c 100644 --- a/contracts/interfaces/ITokenManagerType.sol +++ b/contracts/interfaces/ITokenManagerType.sol @@ -8,8 +8,9 @@ pragma solidity ^0.8.0; */ interface ITokenManagerType { enum TokenManagerType { - LOCK_UNLOCK, MINT_BURN, + MINT_BURN_FROM, + LOCK_UNLOCK, LOCK_UNLOCK_FEE_ON_TRANSFER, LIQUIDITY_POOL } diff --git a/contracts/test/FeeOnTransferTokenTest.sol b/contracts/test/FeeOnTransferTokenTest.sol index d0816dd1..05664232 100644 --- a/contracts/test/FeeOnTransferTokenTest.sol +++ b/contracts/test/FeeOnTransferTokenTest.sol @@ -5,9 +5,9 @@ pragma solidity ^0.8.0; import { InterchainToken } from '../interchain-token/InterchainToken.sol'; import { Distributable } from '../utils/Distributable.sol'; import { ITokenManager } from '../interfaces/ITokenManager.sol'; -import { IERC20BurnableMintable } from '../interfaces/IERC20BurnableMintable.sol'; +import { IERC20MintableBurnable } from '../interfaces/IERC20MintableBurnable.sol'; -contract FeeOnTransferTokenTest is InterchainToken, Distributable, IERC20BurnableMintable { +contract FeeOnTransferTokenTest is InterchainToken, Distributable, IERC20MintableBurnable { ITokenManager public tokenManager_; bool internal tokenManagerRequiresApproval_ = true; diff --git a/contracts/test/InterchainTokenTest.sol b/contracts/test/InterchainTokenTest.sol index 96fa8cbb..8a44f732 100644 --- a/contracts/test/InterchainTokenTest.sol +++ b/contracts/test/InterchainTokenTest.sol @@ -5,9 +5,9 @@ pragma solidity ^0.8.0; import { InterchainToken } from '../interchain-token/InterchainToken.sol'; import { Distributable } from '../utils/Distributable.sol'; import { ITokenManager } from '../interfaces/ITokenManager.sol'; -import { IERC20BurnableMintable } from '../interfaces/IERC20BurnableMintable.sol'; +import { IERC20MintableBurnable } from '../interfaces/IERC20MintableBurnable.sol'; -contract InterchainTokenTest is InterchainToken, Distributable, IERC20BurnableMintable { +contract InterchainTokenTest is InterchainToken, Distributable, IERC20MintableBurnable { ITokenManager public tokenManager_; bool internal tokenManagerRequiresApproval_ = true; string public name; diff --git a/contracts/token-implementations/StandardizedToken.sol b/contracts/token-implementations/StandardizedToken.sol index e4e150a8..769fa53e 100644 --- a/contracts/token-implementations/StandardizedToken.sol +++ b/contracts/token-implementations/StandardizedToken.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.0; -import { IERC20BurnableMintable } from '../interfaces/IERC20BurnableMintable.sol'; +import { IERC20MintableBurnable } from '../interfaces/IERC20MintableBurnable.sol'; import { ITokenManager } from '../interfaces/ITokenManager.sol'; import { InterchainToken } from '../interchain-token/InterchainToken.sol'; @@ -16,7 +16,7 @@ import { Distributable } from '../utils/Distributable.sol'; * @notice This contract implements a standardized token which extends InterchainToken functionality. * This contract also inherits Distributable and Implementation logic. */ -contract StandardizedToken is IERC20BurnableMintable, InterchainToken, ERC20Permit, Implementation, Distributable { +contract StandardizedToken is IERC20MintableBurnable, InterchainToken, ERC20Permit, Implementation, Distributable { using AddressBytesUtils for bytes; string public name; diff --git a/contracts/token-manager/TokenManager.sol b/contracts/token-manager/TokenManager.sol index c0e58663..516baea5 100644 --- a/contracts/token-manager/TokenManager.sol +++ b/contracts/token-manager/TokenManager.sol @@ -20,6 +20,9 @@ abstract contract TokenManager is ITokenManager, Operatable, FlowLimit, Implemen IInterchainTokenService public immutable interchainTokenService; + // uint256(keccak256('token-address')) - 1 + uint256 internal constant TOKEN_ADDRESS_SLOT = 0xc4e632779a6a7838736dd7e5e6a0eadf171dd37dfb6230720e265576dfcf42ba; + /** * @notice Constructs the TokenManager contract. * @param interchainTokenService_ The address of the interchain token service @@ -46,11 +49,14 @@ abstract contract TokenManager is ITokenManager, Operatable, FlowLimit, Implemen } /** - * @notice A function that should return the address of the token. - * Must be overridden in the inheriting contract. - * @return address address of the token. + * @dev Reads the stored token address from the predetermined storage slot + * @return tokenAddress_ The address of the token */ - function tokenAddress() public view virtual returns (address); + function tokenAddress() public view virtual returns (address tokenAddress_) { + assembly { + tokenAddress_ := sload(TOKEN_ADDRESS_SLOT) + } + } /** * @notice A function that returns the token id. @@ -197,6 +203,16 @@ abstract contract TokenManager is ITokenManager, Operatable, FlowLimit, Implemen _setFlowLimit(flowLimit); } + /** + * @dev Stores the token address in the predetermined storage slot + * @param tokenAddress_ The address of the token to store + */ + function _setTokenAddress(address tokenAddress_) internal { + assembly { + sstore(TOKEN_ADDRESS_SLOT, tokenAddress_) + } + } + /** * @notice Transfers tokens from a specific address to this contract. * Must be overridden in the inheriting contract. diff --git a/contracts/token-manager/implementations/TokenManagerAddressStorage.sol b/contracts/token-manager/implementations/TokenManagerAddressStorage.sol deleted file mode 100644 index 23a1559f..00000000 --- a/contracts/token-manager/implementations/TokenManagerAddressStorage.sol +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -import { TokenManager } from '../TokenManager.sol'; - -/** - * @title TokenManagerAddressStorage - * @notice This contract extends the TokenManager contract and provides additional functionality to store and retrieve - * the token address using a predetermined storage slot - */ -abstract contract TokenManagerAddressStorage is TokenManager { - // uint256(keccak256('token-address')) - 1 - uint256 internal constant TOKEN_ADDRESS_SLOT = 0xc4e632779a6a7838736dd7e5e6a0eadf171dd37dfb6230720e265576dfcf42ba; - - /** - * @dev Creates an instance of the TokenManagerAddressStorage contract. - * @param interchainTokenService_ The address of the interchain token service contract - */ - constructor(address interchainTokenService_) TokenManager(interchainTokenService_) {} - - /** - * @dev Reads the stored token address from the predetermined storage slot - * @return tokenAddress_ The address of the token - */ - function tokenAddress() public view override returns (address tokenAddress_) { - assembly { - tokenAddress_ := sload(TOKEN_ADDRESS_SLOT) - } - } - - /** - * @dev Stores the token address in the predetermined storage slot - * @param tokenAddress_ The address of the token to store - */ - function _setTokenAddress(address tokenAddress_) internal { - assembly { - sstore(TOKEN_ADDRESS_SLOT, tokenAddress_) - } - } -} diff --git a/contracts/token-manager/implementations/TokenManagerLiquidityPool.sol b/contracts/token-manager/implementations/TokenManagerLiquidityPool.sol index 1539fa9f..5089a444 100644 --- a/contracts/token-manager/implementations/TokenManagerLiquidityPool.sol +++ b/contracts/token-manager/implementations/TokenManagerLiquidityPool.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.0; -import { TokenManagerAddressStorage } from './TokenManagerAddressStorage.sol'; +import { TokenManager } from '../TokenManager.sol'; import { NoReEntrancy } from '../../utils/NoReEntrancy.sol'; import { IERC20 } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IERC20.sol'; import { ITokenManagerLiquidityPool } from '../../interfaces/ITokenManagerLiquidityPool.sol'; @@ -16,7 +16,9 @@ import { SafeTokenTransferFrom } from '@axelar-network/axelar-gmp-sdk-solidity/c * @dev This contract extends TokenManagerAddressStorage and provides implementation for its abstract methods. * It uses the Axelar SDK to safely transfer tokens. */ -contract TokenManagerLiquidityPool is TokenManagerAddressStorage, NoReEntrancy { +contract TokenManagerLiquidityPool is TokenManager, NoReEntrancy { + using SafeTokenTransferFrom for IERC20; + // uint256(keccak256('liquidity-pool-slot')) - 1 uint256 internal constant LIQUIDITY_POOL_SLOT = 0x8e02741a3381812d092c5689c9fc701c5185c1742fdf7954c4c4472be4cc4807; @@ -25,7 +27,7 @@ contract TokenManagerLiquidityPool is TokenManagerAddressStorage, NoReEntrancy { * of TokenManagerAddressStorage which calls the constructor of TokenManager. * @param interchainTokenService_ The address of the interchain token service contract */ - constructor(address interchainTokenService_) TokenManagerAddressStorage(interchainTokenService_) {} + constructor(address interchainTokenService_) TokenManager(interchainTokenService_) {} function implementationType() external pure returns (uint256) { return uint256(TokenManagerType.LIQUIDITY_POOL); @@ -81,7 +83,7 @@ contract TokenManagerLiquidityPool is TokenManagerAddressStorage, NoReEntrancy { address liquidityPool_ = liquidityPool(); uint256 balance = token.balanceOf(liquidityPool_); - SafeTokenTransferFrom.safeTransferFrom(token, from, liquidityPool_, amount); + token.safeTransferFrom(from, liquidityPool_, amount); uint256 diff = token.balanceOf(liquidityPool_) - balance; if (diff < amount) { @@ -100,7 +102,7 @@ contract TokenManagerLiquidityPool is TokenManagerAddressStorage, NoReEntrancy { IERC20 token = IERC20(tokenAddress()); uint256 balance = token.balanceOf(to); - SafeTokenTransferFrom.safeTransferFrom(token, liquidityPool(), to, amount); + token.safeTransferFrom(liquidityPool(), to, amount); uint256 diff = token.balanceOf(to) - balance; if (diff < amount) { diff --git a/contracts/token-manager/implementations/TokenManagerLockUnlock.sol b/contracts/token-manager/implementations/TokenManagerLockUnlock.sol index e9d9fb0f..96ebce7e 100644 --- a/contracts/token-manager/implementations/TokenManagerLockUnlock.sol +++ b/contracts/token-manager/implementations/TokenManagerLockUnlock.sol @@ -2,11 +2,11 @@ pragma solidity ^0.8.0; -import { TokenManagerAddressStorage } from './TokenManagerAddressStorage.sol'; +import { TokenManager } from '../TokenManager.sol'; import { IERC20 } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IERC20.sol'; import { ITokenManagerLockUnlock } from '../../interfaces/ITokenManagerLockUnlock.sol'; -import { SafeTokenTransferFrom, SafeTokenTransfer } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/utils/SafeTransfer.sol'; +import { SafeTokenTransfer, SafeTokenTransferFrom } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/utils/SafeTransfer.sol'; /** * @title TokenManagerLockUnlock @@ -14,13 +14,16 @@ import { SafeTokenTransferFrom, SafeTokenTransfer } from '@axelar-network/axelar * @dev This contract extends TokenManagerAddressStorage and provides implementation for its abstract methods. * It uses the Axelar SDK to safely transfer tokens. */ -contract TokenManagerLockUnlock is TokenManagerAddressStorage, ITokenManagerLockUnlock { +contract TokenManagerLockUnlock is TokenManager, ITokenManagerLockUnlock { + using SafeTokenTransfer for IERC20; + using SafeTokenTransferFrom for IERC20; + /** * @dev Constructs an instance of TokenManagerLockUnlock. Calls the constructor * of TokenManagerAddressStorage which calls the constructor of TokenManager. * @param interchainTokenService_ The address of the interchain token service contract */ - constructor(address interchainTokenService_) TokenManagerAddressStorage(interchainTokenService_) {} + constructor(address interchainTokenService_) TokenManager(interchainTokenService_) {} function implementationType() external pure returns (uint256) { return uint256(TokenManagerType.LOCK_UNLOCK); @@ -45,7 +48,7 @@ contract TokenManagerLockUnlock is TokenManagerAddressStorage, ITokenManagerLock function _takeToken(address from, uint256 amount) internal override returns (uint256) { IERC20 token = IERC20(tokenAddress()); - SafeTokenTransferFrom.safeTransferFrom(token, from, address(this), amount); + token.safeTransferFrom(from, address(this), amount); return amount; } @@ -59,7 +62,7 @@ contract TokenManagerLockUnlock is TokenManagerAddressStorage, ITokenManagerLock function _giveToken(address to, uint256 amount) internal override returns (uint256) { IERC20 token = IERC20(tokenAddress()); - SafeTokenTransfer.safeTransfer(token, to, amount); + token.safeTransfer(to, amount); return amount; } diff --git a/contracts/token-manager/implementations/TokenManagerLockUnlockFeeOnTransfer.sol b/contracts/token-manager/implementations/TokenManagerLockUnlockFeeOnTransfer.sol index 01b21ab5..733ec6aa 100644 --- a/contracts/token-manager/implementations/TokenManagerLockUnlockFeeOnTransfer.sol +++ b/contracts/token-manager/implementations/TokenManagerLockUnlockFeeOnTransfer.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.0; -import { TokenManagerAddressStorage } from './TokenManagerAddressStorage.sol'; +import { TokenManager } from '../TokenManager.sol'; import { NoReEntrancy } from '../../utils/NoReEntrancy.sol'; import { IERC20 } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IERC20.sol'; @@ -14,13 +14,16 @@ import { SafeTokenTransferFrom, SafeTokenTransfer } from '@axelar-network/axelar * @dev This contract extends TokenManagerAddressStorage and provides implementation for its abstract methods. * It uses the Axelar SDK to safely transfer tokens. */ -contract TokenManagerLockUnlockFee is TokenManagerAddressStorage, NoReEntrancy { +contract TokenManagerLockUnlockFee is TokenManager, NoReEntrancy { + using SafeTokenTransfer for IERC20; + using SafeTokenTransferFrom for IERC20; + /** * @dev Constructs an instance of TokenManagerLockUnlock. Calls the constructor * of TokenManagerAddressStorage which calls the constructor of TokenManager. * @param interchainTokenService_ The address of the interchain token service contract */ - constructor(address interchainTokenService_) TokenManagerAddressStorage(interchainTokenService_) {} + constructor(address interchainTokenService_) TokenManager(interchainTokenService_) {} function implementationType() external pure returns (uint256) { return uint256(TokenManagerType.LOCK_UNLOCK_FEE_ON_TRANSFER); @@ -46,7 +49,7 @@ contract TokenManagerLockUnlockFee is TokenManagerAddressStorage, NoReEntrancy { IERC20 token = IERC20(tokenAddress()); uint256 balance = token.balanceOf(address(this)); - SafeTokenTransferFrom.safeTransferFrom(token, from, address(this), amount); + token.safeTransferFrom(from, address(this), amount); uint256 diff = token.balanceOf(address(this)) - balance; if (diff < amount) { @@ -65,7 +68,7 @@ contract TokenManagerLockUnlockFee is TokenManagerAddressStorage, NoReEntrancy { IERC20 token = IERC20(tokenAddress()); uint256 balance = token.balanceOf(to); - SafeTokenTransfer.safeTransfer(token, to, amount); + token.safeTransfer(to, amount); uint256 diff = token.balanceOf(to) - balance; if (diff < amount) { diff --git a/contracts/token-manager/implementations/TokenManagerMintBurn.sol b/contracts/token-manager/implementations/TokenManagerMintBurn.sol index f226b4b7..779fd80f 100644 --- a/contracts/token-manager/implementations/TokenManagerMintBurn.sol +++ b/contracts/token-manager/implementations/TokenManagerMintBurn.sol @@ -2,28 +2,30 @@ pragma solidity ^0.8.0; -import { TokenManagerAddressStorage } from './TokenManagerAddressStorage.sol'; -import { IERC20BurnableMintable } from '../../interfaces/IERC20BurnableMintable.sol'; -import { ITokenManagerMintBurn } from '../../interfaces/ITokenManagerMintBurn.sol'; - import { IERC20 } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IERC20.sol'; import { SafeTokenCall } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/utils/SafeTransfer.sol'; +import { TokenManager } from '../TokenManager.sol'; +import { IERC20MintableBurnable } from '../../interfaces/IERC20MintableBurnable.sol'; +import { ITokenManagerMintBurn } from '../../interfaces/ITokenManagerMintBurn.sol'; + /** * @title TokenManagerMintBurn * @notice This contract is an implementation of TokenManager that mints and burns a specific token on behalf of the interchain token service. * @dev This contract extends TokenManagerAddressStorage and provides implementation for its abstract methods. * It uses the Axelar SDK to safely transfer tokens. */ -contract TokenManagerMintBurn is TokenManagerAddressStorage, ITokenManagerMintBurn { +contract TokenManagerMintBurn is TokenManager, ITokenManagerMintBurn { + using SafeTokenCall for IERC20; + /** * @dev Constructs an instance of TokenManagerMintBurn. Calls the constructor * of TokenManagerAddressStorage which calls the constructor of TokenManager. * @param interchainTokenService_ The address of the interchain token service contract */ - constructor(address interchainTokenService_) TokenManagerAddressStorage(interchainTokenService_) {} + constructor(address interchainTokenService_) TokenManager(interchainTokenService_) {} - function implementationType() external pure returns (uint256) { + function implementationType() external pure virtual returns (uint256) { return uint256(TokenManagerType.MINT_BURN); } @@ -43,10 +45,10 @@ contract TokenManagerMintBurn is TokenManagerAddressStorage, ITokenManagerMintBu * @param amount Amount of tokens to burn * @return uint Amount of tokens burned */ - function _takeToken(address from, uint256 amount) internal override returns (uint256) { + function _takeToken(address from, uint256 amount) internal virtual override returns (uint256) { IERC20 token = IERC20(tokenAddress()); - SafeTokenCall.safeCall(token, abi.encodeWithSelector(IERC20BurnableMintable.burn.selector, from, amount)); + token.safeCall(abi.encodeWithSelector(IERC20MintableBurnable.burn.selector, from, amount)); return amount; } @@ -60,7 +62,7 @@ contract TokenManagerMintBurn is TokenManagerAddressStorage, ITokenManagerMintBu function _giveToken(address to, uint256 amount) internal override returns (uint256) { IERC20 token = IERC20(tokenAddress()); - SafeTokenCall.safeCall(token, abi.encodeWithSelector(IERC20BurnableMintable.mint.selector, to, amount)); + token.safeCall(abi.encodeWithSelector(IERC20MintableBurnable.mint.selector, to, amount)); return amount; } diff --git a/contracts/token-manager/implementations/TokenManagerMintBurnFrom.sol b/contracts/token-manager/implementations/TokenManagerMintBurnFrom.sol new file mode 100644 index 00000000..0ef1e552 --- /dev/null +++ b/contracts/token-manager/implementations/TokenManagerMintBurnFrom.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import { IERC20 } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IERC20.sol'; +import { SafeTokenCall } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/utils/SafeTransfer.sol'; + +import { IERC20BurnableFrom } from '../../interfaces/IERC20BurnableFrom.sol'; +import { TokenManagerMintBurn } from './TokenManagerMintBurn.sol'; + +/** + * @title TokenManagerMintBurn + * @notice This contract is an implementation of TokenManager that mints and burns a specific token on behalf of the interchain token service. + * @dev This contract extends TokenManagerAddressStorage and provides implementation for its abstract methods. + * It uses the Axelar SDK to safely transfer tokens. + */ +contract TokenManagerMintBurnFrom is TokenManagerMintBurn { + using SafeTokenCall for IERC20; + + /** + * @dev Constructs an instance of TokenManagerMintBurn. Calls the constructor + * of TokenManagerAddressStorage which calls the constructor of TokenManager. + * @param interchainTokenService_ The address of the interchain token service contract + */ + constructor(address interchainTokenService_) TokenManagerMintBurn(interchainTokenService_) {} + + function implementationType() external pure override returns (uint256) { + return uint256(TokenManagerType.MINT_BURN_FROM); + } + + /** + * @dev Burns the specified amount of tokens from a particular address. + * @param from Address to burn tokens from + * @param amount Amount of tokens to burn + * @return uint Amount of tokens burned + */ + function _takeToken(address from, uint256 amount) internal override returns (uint256) { + IERC20 token = IERC20(tokenAddress()); + + token.safeCall(abi.encodeWithSelector(IERC20BurnableFrom.burnFrom.selector, from, amount)); + + return amount; + } +} diff --git a/contracts/utils/NoReEntrancy.sol b/contracts/utils/NoReEntrancy.sol index 16d81bbc..5fe91829 100644 --- a/contracts/utils/NoReEntrancy.sol +++ b/contracts/utils/NoReEntrancy.sol @@ -12,6 +12,8 @@ import { INoReEntrancy } from '../interfaces/INoReEntrancy.sol'; contract NoReEntrancy is INoReEntrancy { // uint256(keccak256('entered')) - 1 uint256 internal constant ENTERED_SLOT = 0x01f33dd720a8dea3c4220dc5074a2239fb442c4c775306a696f97a7c54f785fc; + uint256 internal constant NOT_ENTERED = 1; + uint256 internal constant HAS_ENTERED = 2; /** * @notice A modifier that throws a ReEntrancy custom error if the contract is entered @@ -19,9 +21,9 @@ contract NoReEntrancy is INoReEntrancy { */ modifier noReEntrancy() { if (hasEntered()) revert ReEntrancy(); - _setEntered(true); + _setEntered(HAS_ENTERED); _; - _setEntered(false); + _setEntered(NOT_ENTERED); } /** @@ -30,7 +32,7 @@ contract NoReEntrancy is INoReEntrancy { */ function hasEntered() public view returns (bool entered) { assembly { - entered := sload(ENTERED_SLOT) + entered := eq(sload(ENTERED_SLOT), HAS_ENTERED) } } @@ -38,7 +40,7 @@ contract NoReEntrancy is INoReEntrancy { * @notice Sets the entered status of the contract * @param entered A boolean representing the entered status. True if already executing, false otherwise. */ - function _setEntered(bool entered) internal { + function _setEntered(uint256 entered) internal { assembly { sstore(ENTERED_SLOT, entered) } diff --git a/scripts/deploy.js b/scripts/deploy.js index be3f5752..87da2b26 100644 --- a/scripts/deploy.js +++ b/scripts/deploy.js @@ -66,7 +66,7 @@ async function deployInterchainTokenService( async function deployTokenManagerImplementations(wallet, interchainTokenServiceAddress) { const implementations = []; - for (const type of ['LockUnlock', 'MintBurn', 'LockUnlockFee', 'LiquidityPool']) { + for (const type of ['MintBurn', 'MintBurnFrom', 'LockUnlock', 'LockUnlockFee', 'LiquidityPool']) { const impl = await deployContract(wallet, `TokenManager${type}`, [interchainTokenServiceAddress]); implementations.push(impl); } diff --git a/test/tokenService.js b/test/tokenService.js index 582c908c..f2e2a39c 100644 --- a/test/tokenService.js +++ b/test/tokenService.js @@ -19,10 +19,11 @@ const SELECTOR_SEND_TOKEN_WITH_DATA = 2; const SELECTOR_DEPLOY_TOKEN_MANAGER = 3; const SELECTOR_DEPLOY_AND_REGISTER_STANDARDIZED_TOKEN = 4; -const LOCK_UNLOCK = 0; -const MINT_BURN = 1; -const LOCK_UNLOCK_FEE_ON_TRANSFER = 2; -const LIQUIDITY_POOL = 3; +const MINT_BURN = 0; +const MINT_BURN_FROM = 1; +const LOCK_UNLOCK = 2; +const LOCK_UNLOCK_FEE_ON_TRANSFER = 3; +const LIQUIDITY_POOL = 4; describe('Interchain Token Service', () => { let wallet, liquidityPool; @@ -78,25 +79,29 @@ describe('Interchain Token Service', () => { return [token, tokenManager, tokenId]; }; - deployFunctions.mintBurn = async function deployNewMintBurn(tokenName, tokenSymbol, tokenDecimals, mintAmount = 0) { - const salt = getRandomBytes32(); - const tokenId = await service.getCustomTokenId(wallet.address, salt); - const tokenManagerAddress = await service.getTokenManagerAddress(tokenId); - const token = await deployContract(wallet, 'InterchainTokenTest', [tokenName, tokenSymbol, tokenDecimals, tokenManagerAddress]); + const makeDeployNewMintBurn = (type) => + async function deployNewMintBurn(tokenName, tokenSymbol, tokenDecimals, mintAmount = 0) { + const salt = getRandomBytes32(); + const tokenId = await service.getCustomTokenId(wallet.address, salt); + const tokenManagerAddress = await service.getTokenManagerAddress(tokenId); + const token = await deployContract(wallet, 'InterchainTokenTest', [tokenName, tokenSymbol, tokenDecimals, tokenManagerAddress]); - const tokenManager = new Contract(await service.getTokenManagerAddress(tokenId), TokenManager.abi, wallet); + const tokenManager = new Contract(await service.getTokenManagerAddress(tokenId), TokenManager.abi, wallet); - if (mintAmount > 0) { - await (await token.mint(wallet.address, mintAmount)).wait(); - } + if (mintAmount > 0) { + await (await token.mint(wallet.address, mintAmount)).wait(); + } - await (await token.transferDistributorship(tokenManagerAddress)).wait(); + await (await token.transferDistributorship(tokenManagerAddress)).wait(); - const params = defaultAbiCoder.encode(['bytes', 'address'], [wallet.address, token.address]); - await (await service.deployCustomTokenManager(salt, MINT_BURN, params)).wait(); + const params = defaultAbiCoder.encode(['bytes', 'address'], [wallet.address, token.address]); + await (await service.deployCustomTokenManager(salt, type, params)).wait(); - return [token, tokenManager, tokenId]; - }; + return [token, tokenManager, tokenId]; + }; + + deployFunctions.mintBurn = makeDeployNewMintBurn(MINT_BURN); + deployFunctions.mintBurnFrom = makeDeployNewMintBurn(MINT_BURN_FROM); deployFunctions.liquidityPool = async function deployNewLiquidityPool( tokenName, diff --git a/test/tokenServiceFullFlow.js b/test/tokenServiceFullFlow.js index 8dd2c420..1fa94921 100644 --- a/test/tokenServiceFullFlow.js +++ b/test/tokenServiceFullFlow.js @@ -10,20 +10,23 @@ const { Contract, Wallet } = ethers; const IStandardizedToken = require('../artifacts/contracts/interfaces/IStandardizedToken.sol/IStandardizedToken.json'); const ITokenManager = require('../artifacts/contracts/interfaces/ITokenManager.sol/ITokenManager.json'); +const ITokenManagerMintBurn = require('../artifacts/contracts/interfaces/ITokenManagerMintBurn.sol/ITokenManagerMintBurn.json'); const { getRandomBytes32 } = require('../scripts/utils'); const { deployAll, deployContract } = require('../scripts/deploy'); const SELECTOR_SEND_TOKEN = 1; // const SELECTOR_SEND_TOKEN_WITH_DATA = 2; -// const SELECTOR_DEPLOY_TOKEN_MANAGER = 3; +const SELECTOR_DEPLOY_TOKEN_MANAGER = 3; const SELECTOR_DEPLOY_AND_REGISTER_STANDARDIZED_TOKEN = 4; -const LOCK_UNLOCK = 0; -const MINT_BURN = 1; -// const LIQUIDITY_POOL = 2; +const MINT_BURN = 0; +// const MINT_BURN_FROM = 1; +const LOCK_UNLOCK = 2; +// const LOCK_UNLOCK_FEE_ON_TRANSFER = 3; +// const LIQUIDITY_POOL = 4; -describe('Interchain Token Service', () => { +describe('Interchain Token Service Flow', () => { let wallet; let service, gateway, gasService, tokenManager, tokenId; const name = 'tokenName'; @@ -235,4 +238,104 @@ describe('Interchain Token Service', () => { await expect(token.burn(newAddress, amount)).to.be.revertedWithCustomError(token, 'NotDistributor'); }); }); + + describe('Full pre-existing token registration and token send', async () => { + let token; + const otherChains = ['chain 1', 'chain 2']; + const gasValues = [1234, 5678]; + const tokenCap = BigInt(1e18); + const salt = keccak256('0x697858'); + + before(async () => { + // The below is used to deploy a token, but any ERC20 that has a mint capability can be used instead. + token = await deployContract(wallet, 'InterchainTokenTest', [name, symbol, decimals, wallet.address]); + + tokenId = await service.getCustomTokenId(wallet.address, salt); + const tokenManagerAddress = await service.getTokenManagerAddress(tokenId); + await (await token.mint(wallet.address, tokenCap)).wait(); + await (await token.setTokenManager(tokenManagerAddress)).wait(); + tokenManager = new Contract(tokenManagerAddress, ITokenManager.abi, wallet); + }); + + it('Should register the token and initiate its deployment on other chains', async () => { + const implAddress = await service.getImplementation(MINT_BURN); + const impl = new Contract(implAddress, ITokenManagerMintBurn.abi, wallet); + const params = await impl.getParams(wallet.address, token.address); + const tx1 = await service.populateTransaction.deployCustomTokenManager(salt, MINT_BURN, params); + const data = [tx1.data]; + let value = 0; + + for (const i in otherChains) { + const tx = await service.populateTransaction.deployRemoteCustomTokenManager( + salt, + otherChains[i], + MINT_BURN, + params, + gasValues[i], + ); + data.push(tx.data); + value += gasValues[i]; + } + + const payload = defaultAbiCoder.encode( + ['uint256', 'bytes32', 'uint256', 'bytes'], + [SELECTOR_DEPLOY_TOKEN_MANAGER, tokenId, MINT_BURN, params], + ); + await expect(service.multicall(data, { value })) + .to.emit(service, 'TokenManagerDeployed') + .withArgs(tokenId, MINT_BURN, params) + .and.to.emit(service, 'RemoteTokenManagerDeploymentInitialized') + .withArgs(tokenId, otherChains[0], gasValues[0], MINT_BURN, params) + .and.to.emit(gasService, 'NativeGasPaidForContractCall') + .withArgs(service.address, otherChains[0], service.address.toLowerCase(), keccak256(payload), gasValues[0], wallet.address) + .and.to.emit(gateway, 'ContractCall') + .withArgs(service.address, otherChains[0], service.address.toLowerCase(), keccak256(payload), payload) + .and.to.emit(service, 'RemoteTokenManagerDeploymentInitialized') + .withArgs(tokenId, otherChains[1], gasValues[1], MINT_BURN, params) + .and.to.emit(gasService, 'NativeGasPaidForContractCall') + .withArgs(service.address, otherChains[1], service.address.toLowerCase(), keccak256(payload), gasValues[1], wallet.address) + .and.to.emit(gateway, 'ContractCall') + .withArgs(service.address, otherChains[1], service.address.toLowerCase(), keccak256(payload), payload); + }); + + // For this test the token must be a standardized token (or a distributable token in general) + it('Should be able to change the token distributor', async () => { + const newAddress = new Wallet(getRandomBytes32()).address; + const amount = 1234; + + await expect(token.mint(newAddress, amount)).to.emit(token, 'Transfer').withArgs(AddressZero, newAddress, amount); + await expect(token.burn(newAddress, amount)).to.emit(token, 'Transfer').withArgs(newAddress, AddressZero, amount); + + await expect(token.transferDistributorship(tokenManager.address)) + .to.emit(token, 'DistributorshipTransferred') + .withArgs(tokenManager.address); + + await expect(token.mint(newAddress, amount)).to.be.revertedWithCustomError(token, 'NotDistributor'); + await expect(token.burn(newAddress, amount)).to.be.revertedWithCustomError(token, 'NotDistributor'); + }); + + // In order to be able to receive tokens the distributorship should be changed on other chains as well. + it('Should send some token to another chain', async () => { + const amount = 1234; + const destAddress = '0x1234'; + const destChain = otherChains[0]; + const gasValue = 6789; + + const payload = defaultAbiCoder.encode( + ['uint256', 'bytes32', 'bytes', 'uint256'], + [SELECTOR_SEND_TOKEN, tokenId, destAddress, amount], + ); + const payloadHash = keccak256(payload); + + await expect(tokenManager.interchainTransfer(destChain, destAddress, amount, '0x', { value: gasValue })) + .and.to.emit(token, 'Transfer') + .withArgs(wallet.address, AddressZero, amount) + .and.to.emit(gateway, 'ContractCall') + .withArgs(service.address, destChain, service.address.toLowerCase(), payloadHash, payload) + .and.to.emit(gasService, 'NativeGasPaidForContractCall') + .withArgs(service.address, destChain, service.address.toLowerCase(), payloadHash, gasValue, wallet.address) + .to.emit(service, 'TokenSent') + .withArgs(tokenId, destChain, destAddress, amount); + }); + }); });