diff --git a/contracts/InterchainTokenService.sol b/contracts/InterchainTokenService.sol index 923f1ad5..1515e572 100644 --- a/contracts/InterchainTokenService.sol +++ b/contracts/InterchainTokenService.sol @@ -14,14 +14,12 @@ import { Pausable } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/util import { InterchainAddressTracker } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/utils/InterchainAddressTracker.sol'; import { IInterchainTokenService } from './interfaces/IInterchainTokenService.sol'; -import { ITokenManagerProxy } from './interfaces/ITokenManagerProxy.sol'; import { ITokenHandler } from './interfaces/ITokenHandler.sol'; import { ITokenManagerDeployer } from './interfaces/ITokenManagerDeployer.sol'; import { IInterchainTokenDeployer } from './interfaces/IInterchainTokenDeployer.sol'; import { IInterchainTokenExecutable } from './interfaces/IInterchainTokenExecutable.sol'; import { IInterchainTokenExpressExecutable } from './interfaces/IInterchainTokenExpressExecutable.sol'; import { ITokenManager } from './interfaces/ITokenManager.sol'; -import { IERC20Named } from './interfaces/IERC20Named.sol'; import { Operator } from './utils/Operator.sol'; @@ -264,7 +262,7 @@ contract InterchainTokenService is * part of a multicall involving multiple functions that could make remote contract calls. * @param salt The salt to be used during deployment. * @param destinationChain The name of the chain to deploy the TokenManager and standardized token to. - * @param tokenManagerType The type of TokenManager to be deployed. + * @param tokenManagerType The type of token manager to be deployed. Cannot be NATIVE_INTERCHAIN_TOKEN. * @param params The params that will be used to initialize the TokenManager. * @param gasValue The amount of native tokens to be used to pay for gas for the remote deployment. * @return tokenId The tokenId corresponding to the deployed TokenManager. @@ -276,9 +274,14 @@ contract InterchainTokenService is bytes calldata params, uint256 gasValue ) external payable whenNotPaused returns (bytes32 tokenId) { + // Custom token managers can't be deployed with Interchain token mint burn type, which is reserved for interchain tokens + if (tokenManagerType == TokenManagerType.NATIVE_INTERCHAIN_TOKEN) revert CannotDeploy(tokenManagerType); + address deployer = msg.sender; - if (deployer == interchainTokenFactory) deployer = TOKEN_FACTORY_DEPLOYER; + if (deployer == interchainTokenFactory) { + deployer = TOKEN_FACTORY_DEPLOYER; + } tokenId = interchainTokenId(deployer, salt); @@ -323,7 +326,7 @@ contract InterchainTokenService is if (bytes(destinationChain).length == 0) { address tokenAddress = _deployInterchainToken(tokenId, minter, name, symbol, decimals); - _deployTokenManager(tokenId, TokenManagerType.MINT_BURN, abi.encode(minter, tokenAddress)); + _deployTokenManager(tokenId, TokenManagerType.NATIVE_INTERCHAIN_TOKEN, abi.encode(minter, tokenAddress)); } else { _deployRemoteInterchainToken(tokenId, name, symbol, decimals, minter, destinationChain, gasValue); } @@ -396,21 +399,11 @@ contract InterchainTokenService is IERC20 token; { - ITokenManager tokenManager_ = ITokenManager(tokenManagerAddress(tokenId)); - token = IERC20(tokenManager_.tokenAddress()); - (bool success, bytes memory returnData) = tokenHandler.delegatecall( - abi.encodeWithSelector( - ITokenHandler.transferTokenFrom.selector, - tokenManager_.implementationType(), - address(token), - msg.sender, - destinationAddress, - amount - ) + abi.encodeWithSelector(ITokenHandler.transferTokenFrom.selector, tokenId, msg.sender, destinationAddress, amount) ); if (!success) revert TokenHandlerFailed(returnData); - amount = abi.decode(returnData, (uint256)); + (amount, token) = abi.decode(returnData, (uint256, IERC20)); } // slither-disable-next-line reentrancy-events @@ -738,7 +731,6 @@ contract InterchainTokenService is /** * @notice Processes a deploy token manager payload. - * @param payload The encoded data payload to be processed */ function _processDeployTokenManagerPayload(bytes calldata payload) internal { (, bytes32 tokenId, TokenManagerType tokenManagerType, bytes memory params) = abi.decode( @@ -746,6 +738,8 @@ contract InterchainTokenService is (uint256, bytes32, TokenManagerType, bytes) ); + if (tokenManagerType == TokenManagerType.NATIVE_INTERCHAIN_TOKEN) revert CannotDeploy(tokenManagerType); + _deployTokenManager(tokenId, tokenManagerType, params); } @@ -762,7 +756,7 @@ contract InterchainTokenService is tokenAddress = _deployInterchainToken(tokenId, minterBytes, name, symbol, decimals); - _deployTokenManager(tokenId, TokenManagerType.MINT_BURN, abi.encode(minterBytes, tokenAddress)); + _deployTokenManager(tokenId, TokenManagerType.NATIVE_INTERCHAIN_TOKEN, abi.encode(minterBytes, tokenAddress)); } /** @@ -1073,44 +1067,24 @@ contract InterchainTokenService is * @dev Takes token from a sender via the token service. `tokenOnly` indicates if the caller should be restricted to the token only. */ function _takeToken(bytes32 tokenId, address from, uint256 amount, bool tokenOnly) internal returns (uint256, string memory symbol) { - address tokenManager_ = tokenManagerAddress(tokenId); - uint256 tokenManagerType; - address tokenAddress; - - (tokenManagerType, tokenAddress) = ITokenManagerProxy(tokenManager_).getImplementationTypeAndTokenAddress(); - - if (tokenOnly && msg.sender != tokenAddress) revert NotToken(msg.sender, tokenAddress); - (bool success, bytes memory data) = tokenHandler.delegatecall( - abi.encodeWithSelector(ITokenHandler.takeToken.selector, tokenManagerType, tokenAddress, tokenManager_, from, amount) + abi.encodeWithSelector(ITokenHandler.takeToken.selector, tokenId, tokenOnly, from, amount) ); if (!success) revert TakeTokenFailed(data); - amount = abi.decode(data, (uint256)); + (amount, symbol) = abi.decode(data, (uint256, string)); - /// @dev Track the flow amount being sent out as a message - ITokenManager(tokenManager_).addFlowOut(amount); - if (tokenManagerType == uint256(TokenManagerType.GATEWAY)) { - symbol = IERC20Named(tokenAddress).symbol(); - } return (amount, symbol); } /** * @dev Gives token to recipient via the token service. */ - function _giveToken(bytes32 tokenId, address to, uint256 amount) internal returns (uint256, address) { - address tokenManager_ = tokenManagerAddress(tokenId); - - (uint256 tokenManagerType, address tokenAddress) = ITokenManagerProxy(tokenManager_).getImplementationTypeAndTokenAddress(); - - /// @dev Track the flow amount being received via the message - ITokenManager(tokenManager_).addFlowIn(amount); - + function _giveToken(bytes32 tokenId, address to, uint256 amount) internal returns (uint256, address tokenAddress) { (bool success, bytes memory data) = tokenHandler.delegatecall( - abi.encodeWithSelector(ITokenHandler.giveToken.selector, tokenManagerType, tokenAddress, tokenManager_, to, amount) + abi.encodeWithSelector(ITokenHandler.giveToken.selector, tokenId, to, amount) ); if (!success) revert GiveTokenFailed(data); - amount = abi.decode(data, (uint256)); + (amount, tokenAddress) = abi.decode(data, (uint256, address)); return (amount, tokenAddress); } diff --git a/contracts/TokenHandler.sol b/contracts/TokenHandler.sol index 0a9c432b..4f55cf91 100644 --- a/contracts/TokenHandler.sol +++ b/contracts/TokenHandler.sol @@ -6,17 +6,20 @@ import { ITokenHandler } from './interfaces/ITokenHandler.sol'; import { IERC20 } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IERC20.sol'; import { SafeTokenTransfer, SafeTokenTransferFrom, SafeTokenCall } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/libs/SafeTransfer.sol'; import { ReentrancyGuard } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/utils/ReentrancyGuard.sol'; +import { Create3Address } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/deploy/Create3Address.sol'; import { ITokenManagerType } from './interfaces/ITokenManagerType.sol'; import { ITokenManager } from './interfaces/ITokenManager.sol'; +import { ITokenManagerProxy } from './interfaces/ITokenManagerProxy.sol'; import { IERC20MintableBurnable } from './interfaces/IERC20MintableBurnable.sol'; import { IERC20BurnableFrom } from './interfaces/IERC20BurnableFrom.sol'; +import { IERC20Named } from './interfaces/IERC20Named.sol'; /** * @title TokenHandler * @notice This interface is responsible for handling tokens before initiating an interchain token transfer, or after receiving one. */ -contract TokenHandler is ITokenHandler, ITokenManagerType, ReentrancyGuard { +contract TokenHandler is ITokenHandler, ITokenManagerType, ReentrancyGuard, Create3Address { using SafeTokenTransferFrom for IERC20; using SafeTokenCall for IERC20; using SafeTokenTransfer for IERC20; @@ -32,39 +35,44 @@ contract TokenHandler is ITokenHandler, ITokenManagerType, ReentrancyGuard { /** * @notice This function gives token to a specified address from the token manager. - * @param tokenManagerType The token manager type. - * @param tokenAddress The address of the token to give. - * @param tokenManager The address of the token manager. + * @param tokenId The token id of the tokenManager. * @param to The address to give tokens to. * @param amount The amount of tokens to give. * @return uint256 The amount of token actually given, which could be different for certain token type. + * @return address the address of the token. */ // slither-disable-next-line locked-ether - function giveToken( - uint256 tokenManagerType, - address tokenAddress, - address tokenManager, - address to, - uint256 amount - ) external payable returns (uint256) { + function giveToken(bytes32 tokenId, address to, uint256 amount) external payable returns (uint256, address) { + address tokenManager = _create3Address(tokenId); + + (uint256 tokenManagerType, address tokenAddress) = ITokenManagerProxy(tokenManager).getImplementationTypeAndTokenAddress(); + + /// @dev Track the flow amount being received via the message + ITokenManager(tokenManager).addFlowIn(amount); + + if (tokenManagerType == uint256(TokenManagerType.NATIVE_INTERCHAIN_TOKEN)) { + _giveInterchainToken(tokenAddress, to, amount); + return (amount, tokenAddress); + } + if (tokenManagerType == uint256(TokenManagerType.MINT_BURN) || tokenManagerType == uint256(TokenManagerType.MINT_BURN_FROM)) { - _giveTokenMintBurn(tokenAddress, to, amount); - return amount; + _mintToken(tokenManager, tokenAddress, to, amount); + return (amount, tokenAddress); } if (tokenManagerType == uint256(TokenManagerType.LOCK_UNLOCK)) { _transferTokenFrom(tokenAddress, tokenManager, to, amount); - return amount; + return (amount, tokenAddress); } if (tokenManagerType == uint256(TokenManagerType.LOCK_UNLOCK_FEE)) { amount = _transferTokenFromWithFee(tokenAddress, tokenManager, to, amount); - return amount; + return (amount, tokenAddress); } if (tokenManagerType == uint256(TokenManagerType.GATEWAY)) { _transferToken(tokenAddress, to, amount); - return amount; + return (amount, tokenAddress); } revert UnsupportedTokenManagerType(tokenManagerType); @@ -72,44 +80,59 @@ contract TokenHandler is ITokenHandler, ITokenManagerType, ReentrancyGuard { /** * @notice This function takes token from a specified address to the token manager. - * @param tokenManagerType The token manager type. - * @param tokenAddress The address of the token to give. - * @param tokenManager The address of the token manager. + * @param tokenId The tokenId for the token. + * @param tokenOnly can onky be called from the token. * @param from The address to take tokens from. * @param amount The amount of token to take. * @return uint256 The amount of token actually taken, which could be different for certain token type. + * @return symbol The symbol for the token, if not empty the token is a gateway token and a callContractWith token has to be made. */ // slither-disable-next-line locked-ether function takeToken( - uint256 tokenManagerType, - address tokenAddress, - address tokenManager, + bytes32 tokenId, + bool tokenOnly, address from, uint256 amount - ) external payable returns (uint256) { + ) external payable returns (uint256, string memory symbol) { + address tokenManager = _create3Address(tokenId); + (uint256 tokenManagerType, address tokenAddress) = ITokenManagerProxy(tokenManager).getImplementationTypeAndTokenAddress(); + + if (tokenOnly && msg.sender != tokenAddress) revert NotToken(msg.sender, tokenAddress); + + /// @dev Track the flow amount being sent out as a message + ITokenManager(tokenManager).addFlowOut(amount); + if (tokenManagerType == uint256(TokenManagerType.GATEWAY)) { + symbol = IERC20Named(tokenAddress).symbol(); + } + + if (tokenManagerType == uint256(TokenManagerType.NATIVE_INTERCHAIN_TOKEN)) { + _takeInterchainToken(tokenAddress, from, amount); + return (amount, symbol); + } + if (tokenManagerType == uint256(TokenManagerType.MINT_BURN)) { - _takeTokenMintBurn(tokenAddress, from, amount); - return amount; + _burnToken(tokenManager, tokenAddress, from, amount); + return (amount, symbol); } if (tokenManagerType == uint256(TokenManagerType.MINT_BURN_FROM)) { - _takeTokenMintBurnFrom(tokenAddress, from, amount); - return amount; + _burnTokenFrom(tokenAddress, from, amount); + return (amount, symbol); } if (tokenManagerType == uint256(TokenManagerType.LOCK_UNLOCK)) { _transferTokenFrom(tokenAddress, from, tokenManager, amount); - return amount; + return (amount, symbol); } if (tokenManagerType == uint256(TokenManagerType.LOCK_UNLOCK_FEE)) { amount = _transferTokenFromWithFee(tokenAddress, from, tokenManager, amount); - return amount; + return (amount, symbol); } if (tokenManagerType == uint256(TokenManagerType.GATEWAY)) { _transferTokenFrom(tokenAddress, from, address(this), amount); - return amount; + return (amount, symbol); } revert UnsupportedTokenManagerType(tokenManagerType); @@ -117,34 +140,31 @@ contract TokenHandler is ITokenHandler, ITokenManagerType, ReentrancyGuard { /** * @notice This function transfers token from and to a specified address. - * @param tokenManagerType The token manager type. - * @param tokenAddress the address of the token to give. + * @param tokenId The token id of the token manager. * @param from The address to transfer tokens from. * @param to The address to transfer tokens to. * @param amount The amount of token to transfer. * @return uint256 The amount of token actually transferred, which could be different for certain token type. + * @return address The address of the token corresponding to the input tokenId. */ // slither-disable-next-line locked-ether - function transferTokenFrom( - uint256 tokenManagerType, - address tokenAddress, - address from, - address to, - uint256 amount - ) external payable returns (uint256) { + function transferTokenFrom(bytes32 tokenId, address from, address to, uint256 amount) external payable returns (uint256, address) { + address tokenManager = _create3Address(tokenId); + (uint256 tokenManagerType, address tokenAddress) = ITokenManagerProxy(tokenManager).getImplementationTypeAndTokenAddress(); if ( + tokenManagerType == uint256(TokenManagerType.NATIVE_INTERCHAIN_TOKEN) || tokenManagerType == uint256(TokenManagerType.LOCK_UNLOCK) || tokenManagerType == uint256(TokenManagerType.MINT_BURN) || tokenManagerType == uint256(TokenManagerType.MINT_BURN_FROM) || tokenManagerType == uint256(TokenManagerType.GATEWAY) ) { _transferTokenFrom(tokenAddress, from, to, amount); - return amount; + return (amount, tokenAddress); } if (tokenManagerType == uint256(TokenManagerType.LOCK_UNLOCK_FEE)) { amount = _transferTokenFromWithFee(tokenAddress, from, to, amount); - return amount; + return (amount, tokenAddress); } revert UnsupportedTokenManagerType(tokenManagerType); @@ -196,15 +216,23 @@ contract TokenHandler is ITokenHandler, ITokenManagerType, ReentrancyGuard { return amount; } - function _giveTokenMintBurn(address tokenAddress, address to, uint256 amount) internal { + function _giveInterchainToken(address tokenAddress, address to, uint256 amount) internal { IERC20(tokenAddress).safeCall(abi.encodeWithSelector(IERC20MintableBurnable.mint.selector, to, amount)); } - function _takeTokenMintBurn(address tokenAddress, address from, uint256 amount) internal { + function _takeInterchainToken(address tokenAddress, address from, uint256 amount) internal { IERC20(tokenAddress).safeCall(abi.encodeWithSelector(IERC20MintableBurnable.burn.selector, from, amount)); } - function _takeTokenMintBurnFrom(address tokenAddress, address from, uint256 amount) internal { + function _mintToken(address tokenManager, address tokenAddress, address to, uint256 amount) internal { + ITokenManager(tokenManager).mintToken(tokenAddress, to, amount); + } + + function _burnToken(address tokenManager, address tokenAddress, address from, uint256 amount) internal { + ITokenManager(tokenManager).burnToken(tokenAddress, from, amount); + } + + function _burnTokenFrom(address tokenAddress, address from, uint256 amount) internal { IERC20(tokenAddress).safeCall(abi.encodeWithSelector(IERC20BurnableFrom.burnFrom.selector, from, amount)); } diff --git a/contracts/interfaces/IInterchainTokenService.sol b/contracts/interfaces/IInterchainTokenService.sol index d069871e..0d6a0741 100644 --- a/contracts/interfaces/IInterchainTokenService.sol +++ b/contracts/interfaces/IInterchainTokenService.sol @@ -32,7 +32,6 @@ interface IInterchainTokenService is error InvalidChainName(); error NotRemoteService(); error TokenManagerDoesNotExist(bytes32 tokenId); - error NotToken(address caller, address token); error ExecuteWithInterchainTokenFailed(address contractAddress); error ExpressExecuteWithInterchainTokenFailed(address contractAddress); error GatewayToken(); @@ -47,6 +46,7 @@ interface IInterchainTokenService is error TokenHandlerFailed(bytes data); error EmptyData(); error PostDeployFailed(bytes data); + error CannotDeploy(TokenManagerType); error ZeroAmount(); event InterchainTransfer( @@ -167,7 +167,7 @@ interface IInterchainTokenService is * @notice Deploys a custom token manager contract on a remote chain. * @param salt The salt used for token manager deployment. * @param destinationChain The name of the destination chain. - * @param tokenManagerType The type of token manager. + * @param tokenManagerType The type of token manager. Cannot be NATIVE_INTERCHAIN_TOKEN. * @param params The deployment parameters. * @param gasValue The gas value for deployment. * @return tokenId The tokenId associated with the token manager. diff --git a/contracts/interfaces/ITokenHandler.sol b/contracts/interfaces/ITokenHandler.sol index b2167cf5..2bc8da8a 100644 --- a/contracts/interfaces/ITokenHandler.sol +++ b/contracts/interfaces/ITokenHandler.sol @@ -9,6 +9,7 @@ pragma solidity ^0.8.0; interface ITokenHandler { error UnsupportedTokenManagerType(uint256 tokenManagerType); error AddressZero(); + error NotToken(address caller, address token); /** * @notice Returns the address of the axelar gateway on this chain. @@ -18,54 +19,42 @@ interface ITokenHandler { /** * @notice This function gives token to a specified address from the token manager. - * @param tokenManagerType The token manager type. - * @param tokenAddress The address of the token to give. - * @param tokenManager The address of the token manager. + * @param tokenId The token id of the tokenManager. * @param to The address to give tokens to. * @param amount The amount of tokens to give. * @return uint256 The amount of token actually given, which could be different for certain token type. + * @return address the address of the token. */ - function giveToken( - uint256 tokenManagerType, - address tokenAddress, - address tokenManager, - address to, - uint256 amount - ) external payable returns (uint256); + function giveToken(bytes32 tokenId, address to, uint256 amount) external payable returns (uint256, address); /** * @notice This function takes token from a specified address to the token manager. - * @param tokenManagerType The token manager type. - * @param tokenAddress The address of the token to give. - * @param tokenManager The address of the token manager. + * @param tokenId The tokenId for the token. + * @param tokenOnly can onky be called from the token. * @param from The address to take tokens from. * @param amount The amount of token to take. * @return uint256 The amount of token actually taken, which could be different for certain token type. + * @return symbol The symbol for the token, if not empty the token is a gateway token and a callContractWith token has to be made. */ + // slither-disable-next-line locked-ether function takeToken( - uint256 tokenManagerType, - address tokenAddress, - address tokenManager, + bytes32 tokenId, + bool tokenOnly, address from, uint256 amount - ) external payable returns (uint256); + ) external payable returns (uint256, string memory symbol); /** * @notice This function transfers token from and to a specified address. - * @param tokenManagerType The token manager type. - * @param tokenAddress the address of the token to give. + * @param tokenId The token id of the token manager. * @param from The address to transfer tokens from. * @param to The address to transfer tokens to. * @param amount The amount of token to transfer. * @return uint256 The amount of token actually transferred, which could be different for certain token type. + * @return address The address of the token corresponding to the input tokenId. */ - function transferTokenFrom( - uint256 tokenManagerType, - address tokenAddress, - address from, - address to, - uint256 amount - ) external payable returns (uint256); + // slither-disable-next-line locked-ether + function transferTokenFrom(bytes32 tokenId, address from, address to, uint256 amount) external payable returns (uint256, address); /** * @notice This function prepares a token manager after it is deployed diff --git a/contracts/interfaces/ITokenManager.sol b/contracts/interfaces/ITokenManager.sol index 435570a9..50c582a9 100644 --- a/contracts/interfaces/ITokenManager.sol +++ b/contracts/interfaces/ITokenManager.sol @@ -74,4 +74,22 @@ interface ITokenManager is IBaseTokenManager, IOperator, IFlowLimit, IImplementa * @return params_ The resulting params to be passed to custom TokenManager deployments. */ function params(bytes calldata operator_, address tokenAddress_) external pure returns (bytes memory params_); + + /** + * @notice External function to allow the service to mint tokens through the tokenManager + * @dev This function should revert if called by anyone but the service. + * @param tokenAddress_ The address of the token, since its cheaper to pass it in instead of reading it as the token manager. + * @param to The recipient. + * @param amount The amount to mint. + */ + function mintToken(address tokenAddress_, address to, uint256 amount) external; + + /** + * @notice External function to allow the service to burn tokens through the tokenManager + * @dev This function should revert if called by anyone but the service. + * @param tokenAddress_ The address of the token, since its cheaper to pass it in instead of reading it as the token manager. + * @param from The address to burn the token from. + * @param amount The amount to burn. + */ + function burnToken(address tokenAddress_, address from, uint256 amount) external; } diff --git a/contracts/interfaces/ITokenManagerType.sol b/contracts/interfaces/ITokenManagerType.sol index 889a7334..9c8406be 100644 --- a/contracts/interfaces/ITokenManagerType.sol +++ b/contracts/interfaces/ITokenManagerType.sol @@ -8,10 +8,11 @@ pragma solidity ^0.8.0; */ interface ITokenManagerType { enum TokenManagerType { - MINT_BURN, - MINT_BURN_FROM, - LOCK_UNLOCK, - LOCK_UNLOCK_FEE, - GATEWAY + NATIVE_INTERCHAIN_TOKEN, // This type is reserved for interchain tokens deployed by ITS, and can't be used by custom token managers. + MINT_BURN_FROM, // The token will be minted/burned on transfers. The token needs to give mint permission to the token manager, but burning happens via an approval. + LOCK_UNLOCK, // The token will be locked/unlocked at the token manager. + LOCK_UNLOCK_FEE, // The token will be locked/unlocked at the token manager, which will account for any fee-on-transfer behaviour. + MINT_BURN, // The token will be minted/burned on transfers. The token needs to give mint and burn permission to the token manager. + GATEWAY // The token will be transferred through the AxelarGateway via callContractWithToken } } diff --git a/contracts/test/TestInterchainTokenStandard.sol b/contracts/test/TestInterchainTokenStandard.sol index 4599820a..c9d39b10 100644 --- a/contracts/test/TestInterchainTokenStandard.sol +++ b/contracts/test/TestInterchainTokenStandard.sol @@ -73,7 +73,7 @@ contract TestInterchainTokenStandard is InterchainTokenStandard, Minter, ERC20, _burn(account, amount); } - function burnFrom(address account, uint256 amount) external onlyRole(uint8(Roles.MINTER)) { + function burnFrom(address account, uint256 amount) external { uint256 currentAllowance = allowance[account][msg.sender]; if (currentAllowance < amount) revert AllowanceExceeded(); _approve(account, msg.sender, currentAllowance - amount); diff --git a/contracts/token-manager/TokenManager.sol b/contracts/token-manager/TokenManager.sol index c94d1c17..e6d30174 100644 --- a/contracts/token-manager/TokenManager.sol +++ b/contracts/token-manager/TokenManager.sol @@ -10,6 +10,7 @@ import { SafeTokenCall } from '@axelar-network/axelar-gmp-sdk-solidity/contracts import { Multicall } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/utils/Multicall.sol'; import { ITokenManager } from '../interfaces/ITokenManager.sol'; +import { IERC20MintableBurnable } from '../interfaces/IERC20MintableBurnable.sol'; import { Operator } from '../utils/Operator.sol'; import { FlowLimit } from '../utils/FlowLimit.sol'; @@ -190,4 +191,26 @@ contract TokenManager is ITokenManager, Operator, FlowLimit, Implementation, Mul function params(bytes calldata operator_, address tokenAddress_) external pure returns (bytes memory params_) { params_ = abi.encode(operator_, tokenAddress_); } + + /** + * @notice External function to allow the service to mint tokens through the tokenManager + * @dev This function should revert if called by anyone but the service. + * @param tokenAddress_ The address of the token, since its cheaper to pass it in instead of reading it as the token manager. + * @param to The recipient. + * @param amount The amount to mint. + */ + function mintToken(address tokenAddress_, address to, uint256 amount) external onlyService { + IERC20(tokenAddress_).safeCall(abi.encodeWithSelector(IERC20MintableBurnable.mint.selector, to, amount)); + } + + /** + * @notice External function to allow the service to burn tokens through the tokenManager + * @dev This function should revert if called by anyone but the service. + * @param tokenAddress_ The address of the token, since its cheaper to pass it in instead of reading it as the token manager. + * @param from The address to burn the token from. + * @param amount The amount to burn. + */ + function burnToken(address tokenAddress_, address from, uint256 amount) external onlyService { + IERC20(tokenAddress_).safeCall(abi.encodeWithSelector(IERC20MintableBurnable.burn.selector, from, amount)); + } } diff --git a/docs/index.md b/docs/index.md index 28d3a181..a8cefe68 100644 --- a/docs/index.md +++ b/docs/index.md @@ -703,7 +703,7 @@ part of a multicall involving multiple functions that could make remote contract | ---- | ---- | ----------- | | salt | bytes32 | The salt to be used during deployment. | | destinationChain | string | The name of the chain to deploy the TokenManager and standardized token to. | -| tokenManagerType | enum ITokenManagerType.TokenManagerType | The type of TokenManager to be deployed. | +| tokenManagerType | enum ITokenManagerType.TokenManagerType | The type of token manager to be deployed. Cannot be NATIVE_INTERCHAIN_TOKEN. | | params | bytes | The params that will be used to initialize the TokenManager. | | gasValue | uint256 | The amount of native tokens to be used to pay for gas for the remote deployment. | @@ -987,12 +987,6 @@ function _processDeployTokenManagerPayload(bytes payload) internal Processes a deploy token manager payload. -#### Parameters - -| Name | Type | Description | -| ---- | ---- | ----------- | -| payload | bytes | The encoded data payload to be processed | - ### _processDeployInterchainTokenPayload ```solidity @@ -1264,22 +1258,34 @@ function _transferTokenFrom(address tokenAddress, address from, address to, uint function _transferTokenFromWithFee(address tokenAddress, address from, address to, uint256 amount) internal returns (uint256) ``` -### _giveTokenMintBurn +### _giveInterchainToken + +```solidity +function _giveInterchainToken(address tokenAddress, address to, uint256 amount) internal +``` + +### _takeInterchainToken + +```solidity +function _takeInterchainToken(address tokenAddress, address from, uint256 amount) internal +``` + +### _mintToken ```solidity -function _giveTokenMintBurn(address tokenAddress, address to, uint256 amount) internal +function _mintToken(address tokenManager, address tokenAddress, address to, uint256 amount) internal ``` -### _takeTokenMintBurn +### _burnToken ```solidity -function _takeTokenMintBurn(address tokenAddress, address from, uint256 amount) internal +function _burnToken(address tokenManager, address tokenAddress, address from, uint256 amount) internal ``` -### _takeTokenMintBurnFrom +### _burnTokenFrom ```solidity -function _takeTokenMintBurnFrom(address tokenAddress, address from, uint256 amount) internal +function _burnTokenFrom(address tokenAddress, address from, uint256 amount) internal ``` ## InterchainTokenExecutable @@ -2856,6 +2862,12 @@ error TokenHandlerFailed(bytes data) error EmptyData() ``` +### CannotDeploy + +```solidity +error CannotDeploy(enum ITokenManagerType.TokenManagerType) +``` + ### InterchainTransfer ```solidity @@ -3097,7 +3109,7 @@ Deploys a custom token manager contract on a remote chain. | ---- | ---- | ----------- | | salt | bytes32 | The salt used for token manager deployment. | | destinationChain | string | The name of the destination chain. | -| tokenManagerType | enum ITokenManagerType.TokenManagerType | The type of token manager. | +| tokenManagerType | enum ITokenManagerType.TokenManagerType | The type of token manager. Cannot be NATIVE_INTERCHAIN_TOKEN. | | params | bytes | The deployment parameters. | | gasValue | uint256 | The gas value for deployment. | @@ -3716,6 +3728,42 @@ _This function will be mainly used by frontends._ | ---- | ---- | ----------- | | params_ | bytes | The resulting params to be passed to custom TokenManager deployments. | +### mintToken + +```solidity +function mintToken(address tokenAddress_, address to, uint256 amount) external +``` + +External function to allow the service to mint tokens through the tokenManager + +_This function should revert if called by anyone but the service._ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| tokenAddress_ | address | The address of the token, since its cheaper to pass it in instead of reading it as the token manager. | +| to | address | The recipient. | +| amount | uint256 | The amount to mint. | + +### burnToken + +```solidity +function burnToken(address tokenAddress_, address from, uint256 amount) external +``` + +External function to allow the service to burn tokens through the tokenManager + +_This function should revert if called by anyone but the service._ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| tokenAddress_ | address | The address of the token, since its cheaper to pass it in instead of reading it as the token manager. | +| from | address | The address to burn the token from. | +| amount | uint256 | The amount to burn. | + ## ITokenManagerDeployer This interface is used to deploy new instances of the TokenManagerProxy contract. @@ -3853,10 +3901,11 @@ A simple interface that defines all the token manager types. ```solidity enum TokenManagerType { - MINT_BURN, + NATIVE_INTERCHAIN_TOKEN, MINT_BURN_FROM, LOCK_UNLOCK, - LOCK_UNLOCK_FEE + LOCK_UNLOCK_FEE, + MINT_BURN } ``` @@ -5069,6 +5118,42 @@ _This function will be mainly used by frontends._ | ---- | ---- | ----------- | | params_ | bytes | The resulting params to be passed to custom TokenManager deployments. | +### mintToken + +```solidity +function mintToken(address tokenAddress_, address to, uint256 amount) external +``` + +External function to allow the service to mint tokens through the tokenManager + +_This function should revert if called by anyone but the service._ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| tokenAddress_ | address | The address of the token, since its cheaper to pass it in instead of reading it as the token manager. | +| to | address | The recipient. | +| amount | uint256 | The amount to mint. | + +### burnToken + +```solidity +function burnToken(address tokenAddress_, address from, uint256 amount) external +``` + +External function to allow the service to burn tokens through the tokenManager + +_This function should revert if called by anyone but the service._ + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| tokenAddress_ | address | The address of the token, since its cheaper to pass it in instead of reading it as the token manager. | +| from | address | The address to burn the token from. | +| amount | uint256 | The amount to burn. | + ## FlowLimit Implements flow limit logic for interchain token transfers. diff --git a/package-lock.json b/package-lock.json index 82b030c4..652eef3f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@axelar-network/interchain-token-service", - "version": "1.2.1", + "version": "1.2.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@axelar-network/interchain-token-service", - "version": "1.2.1", + "version": "1.2.4", "license": "MIT", "dependencies": { "@axelar-network/axelar-cgp-solidity": "6.2.1", @@ -5332,9 +5332,9 @@ "dev": true }, "node_modules/follow-redirects": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "dev": true, "funding": [ { @@ -10165,9 +10165,9 @@ } }, "node_modules/undici": { - "version": "5.27.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.27.0.tgz", - "integrity": "sha512-l3ydWhlhOJzMVOYkymLykcRRXqbUaQriERtR70B9LzNkZ4bX52Fc8wbTDneMiwo8T+AemZXvXaTx+9o5ROxrXg==", + "version": "5.28.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.3.tgz", + "integrity": "sha512-3ItfzbrhDlINjaP0duwnNsKpDQk3acHI3gVJ1z4fmwMK31k5G9OVIAMLSIaP6w4FaGkaAkN6zaQO9LUvZ1t7VA==", "dev": true, "dependencies": { "@fastify/busboy": "^2.0.0" diff --git a/package.json b/package.json index b4748570..3e4ce080 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@axelar-network/interchain-token-service", - "version": "1.2.1", + "version": "1.2.4", "repository": { "type": "git", "url": "https://github.com/axelarnetwork/interchain-token-service" diff --git a/test/InterchainTokenFactory.js b/test/InterchainTokenFactory.js index b98bae38..ffdbeb06 100644 --- a/test/InterchainTokenFactory.js +++ b/test/InterchainTokenFactory.js @@ -15,8 +15,8 @@ const { getRandomBytes32, expectRevert } = require('./utils'); const MESSAGE_TYPE_DEPLOY_INTERCHAIN_TOKEN = 1; const LOCK_UNLOCK = 2; -const MINT_BURN = 0; -const GATEWAY = 4; +const GATEWAY = 5; +const NATIVE_INTERCHAIN_TOKEN = 0; const MINTER_ROLE = 0; const OPERATOR_ROLE = 1; @@ -281,7 +281,7 @@ describe('InterchainTokenFactory', () => { .to.emit(service, 'InterchainTokenDeployed') .withArgs(tokenId, tokenAddress, minter, name, symbol, decimals) .and.to.emit(service, 'TokenManagerDeployed') - .withArgs(tokenId, tokenManager.address, MINT_BURN, params); + .withArgs(tokenId, tokenManager.address, NATIVE_INTERCHAIN_TOKEN, params); await checkRoles(tokenManager, minter); }); @@ -298,7 +298,7 @@ describe('InterchainTokenFactory', () => { .to.emit(service, 'InterchainTokenDeployed') .withArgs(tokenId, tokenAddress, AddressZero, name, symbol, decimals) .and.to.emit(service, 'TokenManagerDeployed') - .withArgs(tokenId, tokenManager.address, MINT_BURN, params); + .withArgs(tokenId, tokenManager.address, NATIVE_INTERCHAIN_TOKEN, params); await checkRoles(tokenManager, AddressZero); }); @@ -314,7 +314,7 @@ describe('InterchainTokenFactory', () => { .to.emit(service, 'InterchainTokenDeployed') .withArgs(tokenId, tokenAddress, tokenFactory.address, name, symbol, decimals) .and.to.emit(service, 'TokenManagerDeployed') - .withArgs(tokenId, tokenManager.address, MINT_BURN, params); + .withArgs(tokenId, tokenManager.address, NATIVE_INTERCHAIN_TOKEN, params); await checkRoles(tokenManager, AddressZero); }); @@ -331,7 +331,7 @@ describe('InterchainTokenFactory', () => { .to.emit(service, 'InterchainTokenDeployed') .withArgs(tokenId, tokenAddress, tokenFactory.address, name, symbol, decimals) .and.to.emit(service, 'TokenManagerDeployed') - .withArgs(tokenId, tokenManager.address, MINT_BURN, params) + .withArgs(tokenId, tokenManager.address, NATIVE_INTERCHAIN_TOKEN, params) .and.to.emit(token, 'Transfer') .withArgs(AddressZero, wallet.address, mintAmount) .and.to.emit(tokenManager, 'RolesAdded') @@ -368,7 +368,7 @@ describe('InterchainTokenFactory', () => { .to.emit(service, 'InterchainTokenDeployed') .withArgs(tokenId, tokenAddress, tokenFactory.address, name, symbol, decimals) .and.to.emit(service, 'TokenManagerDeployed') - .withArgs(tokenId, tokenManager.address, MINT_BURN, params) + .withArgs(tokenId, tokenManager.address, NATIVE_INTERCHAIN_TOKEN, params) .and.to.emit(token, 'Transfer') .withArgs(AddressZero, wallet.address, mintAmount) .and.to.emit(token, 'RolesAdded') @@ -439,7 +439,7 @@ describe('InterchainTokenFactory', () => { .to.emit(service, 'InterchainTokenDeployed') .withArgs(tokenId, tokenAddress, tokenFactory.address, name, symbol, decimals) .and.to.emit(service, 'TokenManagerDeployed') - .withArgs(tokenId, tokenManager.address, MINT_BURN, params) + .withArgs(tokenId, tokenManager.address, NATIVE_INTERCHAIN_TOKEN, params) .and.to.emit(token, 'Transfer') .withArgs(AddressZero, wallet.address, mintAmount) .and.to.emit(token, 'RolesAdded') diff --git a/test/InterchainTokenService.js b/test/InterchainTokenService.js index 868b7f0e..065d8c4b 100644 --- a/test/InterchainTokenService.js +++ b/test/InterchainTokenService.js @@ -19,11 +19,12 @@ const MESSAGE_TYPE_DEPLOY_INTERCHAIN_TOKEN = 1; const MESSAGE_TYPE_DEPLOY_TOKEN_MANAGER = 2; const INVALID_MESSAGE_TYPE = 3; -const MINT_BURN = 0; +const NATIVE_INTERCHAIN_TOKEN = 0; const MINT_BURN_FROM = 1; const LOCK_UNLOCK = 2; const LOCK_UNLOCK_FEE_ON_TRANSFER = 3; -const GATEWAY = 4; +const GATEWAY = 5; +const MINT_BURN = 4; const OPERATOR_ROLE = 1; const FLOW_LIMITER_ROLE = 2; @@ -191,7 +192,7 @@ describe('Interchain Token Service', () => { await token.mint(wallet.address, mintAmount).then((tx) => tx.wait); } - await token.transferMintership(service.address).then((tx) => tx.wait); + await token.transferMintership(tokenManager.address).then((tx) => tx.wait); const params = defaultAbiCoder.encode(['bytes', 'address'], [wallet.address, token.address]); await service.deployTokenManager(salt, '', type, params, 0).then((tx) => tx.wait); @@ -552,62 +553,6 @@ describe('Interchain Token Service', () => { }); }); - describe('Token Handler', () => { - const tokenManagerType = 5; - const amount = 1234; - - it('Should revert on give token with unsupported token type', async () => { - await expectRevert( - (gasOptions) => - tokenHandler.giveToken( - tokenManagerType, - otherWallet.address, - otherWallet.address, - otherWallet.address, - amount, - gasOptions, - ), - tokenHandler, - 'UnsupportedTokenManagerType', - [tokenManagerType], - ); - }); - - it('Should revert on take token with unsupported token type', async () => { - await expectRevert( - (gasOptions) => - tokenHandler.takeToken( - tokenManagerType, - otherWallet.address, - otherWallet.address, - otherWallet.address, - amount, - gasOptions, - ), - tokenHandler, - 'UnsupportedTokenManagerType', - [tokenManagerType], - ); - }); - - it('Should revert on transfer token from with unsupported token type', async () => { - await expectRevert( - (gasOptions) => - tokenHandler.transferTokenFrom( - tokenManagerType, - otherWallet.address, - otherWallet.address, - otherWallet.address, - amount, - gasOptions, - ), - tokenHandler, - 'UnsupportedTokenManagerType', - [tokenManagerType], - ); - }); - }); - describe('Deploy and Register Interchain Token', () => { const tokenName = 'Token Name'; const tokenSymbol = 'TN'; @@ -630,7 +575,7 @@ describe('Interchain Token Service', () => { .to.emit(service, 'InterchainTokenDeployed') .withArgs(tokenId, tokenAddress, wallet.address, tokenName, tokenSymbol, tokenDecimals) .to.emit(service, 'TokenManagerDeployed') - .withArgs(tokenId, expectedTokenManagerAddress, MINT_BURN, params); + .withArgs(tokenId, expectedTokenManagerAddress, NATIVE_INTERCHAIN_TOKEN, params); const tokenManagerAddress = await service.validTokenManagerAddress(tokenId); expect(tokenManagerAddress).to.not.equal(AddressZero); @@ -766,32 +711,6 @@ describe('Interchain Token Service', () => { ); }); - it('Should be able to receive a remote interchain token deployment with a mint/burn token manager', async () => { - const tokenId = getRandomBytes32(); - const minter = wallet.address; - const operator = wallet.address; - - const tokenManagerAddress = await service.tokenManagerAddress(tokenId); - const tokenAddress = await service.interchainTokenAddress(tokenId); - const params = defaultAbiCoder.encode(['bytes', 'address'], [operator, tokenAddress]); - const payload = defaultAbiCoder.encode( - ['uint256', 'bytes32', 'string', 'string', 'uint8', 'bytes'], - [MESSAGE_TYPE_DEPLOY_INTERCHAIN_TOKEN, tokenId, tokenName, tokenSymbol, tokenDecimals, minter], - ); - const commandId = await approveContractCall(gateway, sourceChain, sourceAddress, service.address, payload); - - await expect(reportGas(service.execute(commandId, sourceChain, sourceAddress, payload), 'Receive GMP DEPLOY_INTERCHAIN_TOKEN')) - .to.emit(service, 'InterchainTokenDeployed') - .withArgs(tokenId, tokenAddress, minter, tokenName, tokenSymbol, tokenDecimals) - .and.to.emit(service, 'TokenManagerDeployed') - .withArgs(tokenId, tokenManagerAddress, MINT_BURN, params); - - const tokenManager = await getContractAt('TokenManager', tokenManagerAddress, wallet); - - expect(await tokenManager.tokenAddress()).to.equal(tokenAddress); - expect(await tokenManager.hasRole(operator, OPERATOR_ROLE)).to.be.true; - }); - it('Should be able to receive a remote interchain token deployment with a mint/burn token manager with empty minter and operator', async () => { const tokenId = getRandomBytes32(); const tokenManagerAddress = await service.tokenManagerAddress(tokenId); @@ -809,7 +728,7 @@ describe('Interchain Token Service', () => { .to.emit(service, 'InterchainTokenDeployed') .withArgs(tokenId, tokenAddress, AddressZero, tokenName, tokenSymbol, tokenDecimals) .and.to.emit(service, 'TokenManagerDeployed') - .withArgs(tokenId, tokenManagerAddress, MINT_BURN, params); + .withArgs(tokenId, tokenManagerAddress, NATIVE_INTERCHAIN_TOKEN, params); const tokenManager = await getContractAt('TokenManager', tokenManagerAddress, wallet); expect(await tokenManager.tokenAddress()).to.equal(tokenAddress); expect(await tokenManager.hasRole(service.address, OPERATOR_ROLE)).to.be.true; @@ -838,7 +757,29 @@ describe('Interchain Token Service', () => { it('Should revert on deploying an invalid token manager', async () => { const params = defaultAbiCoder.encode(['bytes', 'address'], [wallet.address, token.address]); - await expectRevert((gasOptions) => service.deployTokenManager(salt, '', 5, params, 0, gasOptions)); + await expectRevert((gasOptions) => service.deployTokenManager(salt, '', 6, params, 0, gasOptions)); + }); + + it('Should revert on deploying a local token manager with interchain token manager type', async () => { + const params = defaultAbiCoder.encode(['bytes', 'address'], [wallet.address, token.address]); + + await expectRevert( + (gasOptions) => service.deployTokenManager(salt, '', NATIVE_INTERCHAIN_TOKEN, params, 0, gasOptions), + service, + 'CannotDeploy', + [NATIVE_INTERCHAIN_TOKEN], + ); + }); + + it('Should revert on deploying a remote token manager with interchain token manager type', async () => { + const params = defaultAbiCoder.encode(['bytes', 'address'], [wallet.address, token.address]); + + await expectRevert( + (gasOptions) => service.deployTokenManager(salt, destinationChain, NATIVE_INTERCHAIN_TOKEN, params, 0, gasOptions), + service, + 'CannotDeploy', + [NATIVE_INTERCHAIN_TOKEN], + ); }); it('Should revert on deploying a token manager if token handler post deploy fails', async () => { @@ -1200,6 +1141,31 @@ describe('Interchain Token Service', () => { expect(await tokenManager.tokenAddress()).to.equal(token.address); expect(await tokenManager.hasRole(wallet.address, OPERATOR_ROLE)).to.be.true; }); + + it('Should not be able to receive a remote interchain token manager deployment', async () => { + const tokenId = getRandomBytes32(); + const token = await deployContract(wallet, 'TestInterchainTokenStandard', [ + tokenName, + tokenSymbol, + tokenDecimals, + service.address, + tokenId, + ]); + + const params = defaultAbiCoder.encode(['bytes', 'address'], [wallet.address, token.address]); + const payload = defaultAbiCoder.encode( + ['uint256', 'bytes32', 'uint256', 'bytes'], + [MESSAGE_TYPE_DEPLOY_TOKEN_MANAGER, tokenId, NATIVE_INTERCHAIN_TOKEN, params], + ); + const commandId = await approveContractCall(gateway, sourceChain, sourceAddress, service.address, payload); + + await expectRevert( + (gasOptions) => service.execute(commandId, sourceChain, sourceAddress, payload, gasOptions), + service, + 'CannotDeploy', + [NATIVE_INTERCHAIN_TOKEN], + ); + }); }); describe('Send Token', () => { @@ -1299,6 +1265,10 @@ describe('Interchain Token Service', () => { }); it(`Should revert on transmit send token when not called by interchain token`, async () => { + const errorSignatureHash = id('NotToken(address,address)'); + const selector = errorSignatureHash.substring(0, 10); + const errorData = defaultAbiCoder.encode(['address', 'address'], [wallet.address, token.address]); + await expectRevert( (gasOptions) => service.transmitInterchainTransfer(tokenId, wallet.address, destinationChain, destAddress, amount, '0x', { @@ -1306,8 +1276,8 @@ describe('Interchain Token Service', () => { value: gasValue, }), service, - 'NotToken', - [wallet.address, token.address], + 'TakeTokenFailed', + [selector + errorData.substring(2)], ); }); }); @@ -2381,6 +2351,31 @@ describe('Interchain Token Service', () => { .withArgs(commandId, sourceChain, sourceAddress, keccak256(payload), wallet.address); }); + it('Should be able to receive interchain mint/burn token', async () => { + const salt = getRandomBytes32(); + await (await service.deployInterchainToken(salt, '', `Test Token Mint Burn`, 'TT', 12, wallet.address, 0)).wait(); + const tokenId = await service.interchainTokenId(wallet.address, salt); + const token = await getContractAt('InterchainToken', await service.interchainTokenAddress(tokenId), wallet); + + await (await token.mint(wallet.address, amount)).wait(); + await (await token.approve(service.address, amount)).wait(); + + const payload = defaultAbiCoder.encode( + ['uint256', 'bytes32', 'bytes', 'bytes', 'uint256', 'bytes'], + [MESSAGE_TYPE_INTERCHAIN_TRANSFER, tokenId, hexlify(wallet.address), destAddress, amount, '0x'], + ); + + const commandId = getRandomBytes32(); + await (await service.expressExecute(commandId, sourceChain, sourceAddress, payload)).wait(); + await approveContractCall(gateway, sourceChain, sourceAddress, service.address, payload, getRandomBytes32(), 0, commandId); + + await expect(service.execute(commandId, sourceChain, sourceAddress, payload)) + .to.emit(token, 'Transfer') + .withArgs(AddressZero, wallet.address, amount) + .and.to.emit(service, 'ExpressExecutionFulfilled') + .withArgs(commandId, sourceChain, sourceAddress, keccak256(payload), wallet.address); + }); + it('Should be able to receive mint/burn token', async () => { const [token, , tokenId] = await deployFunctions.mintBurn(`Test Token Mint Burn`, 'TT', 12, amount); @@ -2548,6 +2543,34 @@ describe('Interchain Token Service', () => { expect(await executable.lastMessage()).to.equal(msg); }); + it('Should be able to receive interchain mint/burn token', async () => { + const salt = getRandomBytes32(); + await (await service.deployInterchainToken(salt, '', `Test Token Mint Burn`, 'TT', 12, wallet.address, 0)).wait(); + const tokenId = await service.interchainTokenId(wallet.address, salt); + const token = await getContractAt('InterchainToken', await service.interchainTokenAddress(tokenId), wallet); + + await (await token.mint(wallet.address, amount)).wait(); + await (await token.approve(service.address, amount)).wait(); + + const msg = `mint/burn`; + const data = defaultAbiCoder.encode(['address', 'string'], [wallet.address, msg]); + const payload = defaultAbiCoder.encode( + ['uint256', 'bytes32', 'bytes', 'bytes', 'uint256', 'bytes'], + [MESSAGE_TYPE_INTERCHAIN_TRANSFER, tokenId, sourceAddressForService, destAddress, amount, data], + ); + + const commandId = getRandomBytes32(); + await (await service.expressExecute(commandId, sourceChain, sourceAddress, payload)).wait(); + await approveContractCall(gateway, sourceChain, sourceAddress, service.address, payload, getRandomBytes32(), 0, commandId); + + await expect(service.execute(commandId, sourceChain, sourceAddress, payload)) + .to.emit(token, 'Transfer') + .withArgs(AddressZero, wallet.address, amount) + .and.to.emit(service, 'ExpressExecutionFulfilled') + .withArgs(commandId, sourceChain, sourceAddress, keccak256(payload), wallet.address); + + expect(await executable.lastMessage()).to.equal(msg); + }); it('Should be able to receive mint/burn token', async () => { const [token, , tokenId] = await deployFunctions.mintBurn(`Test Token Mint Burn`, 'TT', 12, amount); await token.approve(service.address, amount).then((tx) => tx.wait); @@ -2603,11 +2626,16 @@ describe('Interchain Token Service', () => { it('Should be able to send token only if it does not trigger the mint limit', async () => { await service.interchainTransfer(tokenId, destinationChain, destinationAddress, sendAmount, '0x', 0).then((tx) => tx.wait); + + const errorSignatureHash = id('FlowLimitExceeded(uint256,uint256,address)'); + const selector = errorSignatureHash.substring(0, 10); + const errorData = defaultAbiCoder.encode(['uint256', 'uint256', 'address'], [flowLimit, 2 * sendAmount, tokenManager.address]); + await expectRevert( (gasOptions) => service.interchainTransfer(tokenId, destinationChain, destinationAddress, sendAmount, '0x', 0, gasOptions), - tokenManager, - 'FlowLimitExceeded', - [flowLimit, 2 * sendAmount, tokenManager.address], + service, + 'TakeTokenFailed', + [selector + errorData.substring(2)], ); }); @@ -2639,10 +2667,15 @@ describe('Interchain Token Service', () => { expect(flowIn).to.eq(sendAmount); expect(flowOut).to.eq(sendAmount); - await expectRevert((gasOptions) => receiveToken(2 * sendAmount, gasOptions), tokenManager, 'FlowLimitExceeded', [ - (5 * sendAmount) / 2, - 3 * sendAmount, - tokenManager.address, + const errorSignatureHash = id('FlowLimitExceeded(uint256,uint256,address)'); + const selector = errorSignatureHash.substring(0, 10); + const errorData = defaultAbiCoder.encode( + ['uint256', 'uint256', 'address'], + [(5 * sendAmount) / 2, 3 * sendAmount, tokenManager.address], + ); + + await expectRevert((gasOptions) => receiveToken(2 * sendAmount, gasOptions), service, 'GiveTokenFailed', [ + selector + errorData.substring(2), ]); }); diff --git a/test/InterchainTokenServiceFullFlow.js b/test/InterchainTokenServiceFullFlow.js index 0d207eab..810ff245 100644 --- a/test/InterchainTokenServiceFullFlow.js +++ b/test/InterchainTokenServiceFullFlow.js @@ -22,7 +22,8 @@ const MESSAGE_TYPE_DEPLOY_TOKEN_MANAGER = 2; const MINTER_ROLE = 0; -const MINT_BURN = 0; +const NATIVE_INTERCHAIN_TOKEN = 0; +const MINT_BURN = 4; const LOCK_UNLOCK = 2; describe('Interchain Token Service Full Flow', () => { @@ -196,7 +197,7 @@ describe('Interchain Token Service Full Flow', () => { .to.emit(service, 'InterchainTokenDeployed') .withArgs(tokenId, expectedTokenAddress, tokenFactory.address, name, symbol, decimals) .and.to.emit(service, 'TokenManagerDeployed') - .withArgs(tokenId, expectedTokenManagerAddress, MINT_BURN, params) + .withArgs(tokenId, expectedTokenManagerAddress, NATIVE_INTERCHAIN_TOKEN, params) .and.to.emit(service, 'InterchainTokenDeploymentStarted') .withArgs(tokenId, name, symbol, decimals, wallet.address.toLowerCase(), otherChains[0]) .and.to.emit(gasService, 'NativeGasPaidForContractCall') @@ -393,17 +394,18 @@ describe('Interchain Token Service Full Flow', () => { * Transfer the minter to ITS on all chains to allow it to mint/burn */ it('Should be able to change the token minter', async () => { + const tokenManagerAddress = await service.tokenManagerAddress(tokenId); 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.transferMintership(service.address)) + await expect(token.transferMintership(tokenManagerAddress)) .to.emit(token, 'RolesRemoved') .withArgs(wallet.address, 1 << MINTER_ROLE) .to.emit(token, 'RolesAdded') - .withArgs(service.address, 1 << MINTER_ROLE); + .withArgs(tokenManagerAddress, 1 << MINTER_ROLE); await expectRevert((gasOptions) => token.mint(newAddress, amount, gasOptions), token, 'MissingRole', [ wallet.address, @@ -503,7 +505,7 @@ describe('Interchain Token Service Full Flow', () => { .to.emit(service, 'InterchainTokenDeployed') .withArgs(tokenId, expectedTokenAddress, tokenFactory.address, name, symbol, decimals) .and.to.emit(service, 'TokenManagerDeployed') - .withArgs(tokenId, expectedTokenManagerAddress, MINT_BURN, params) + .withArgs(tokenId, expectedTokenManagerAddress, NATIVE_INTERCHAIN_TOKEN, params) .and.to.emit(service, 'InterchainTokenDeploymentStarted') .withArgs(tokenId, name, symbol, decimals, '0x', otherChains[0]) .and.to.emit(gasService, 'NativeGasPaidForContractCall') @@ -592,7 +594,7 @@ describe('Interchain Token Service Full Flow', () => { .to.emit(service, 'InterchainTokenDeployed') .withArgs(tokenId, tokenAddress, tokenFactory.address, name, symbol, decimals) .and.to.emit(service, 'TokenManagerDeployed') - .withArgs(tokenId, expectedTokenManagerAddress, MINT_BURN, params); + .withArgs(tokenId, expectedTokenManagerAddress, NATIVE_INTERCHAIN_TOKEN, params); token = await getContractAt('InterchainToken', tokenAddress, wallet); executable = await deployContract(wallet, 'TestInterchainExecutable', [service.address]); diff --git a/test/InterchainTokenServiceUpgradeFlow.js b/test/InterchainTokenServiceUpgradeFlow.js index 5aa8d78b..86c5f44e 100644 --- a/test/InterchainTokenServiceUpgradeFlow.js +++ b/test/InterchainTokenServiceUpgradeFlow.js @@ -16,6 +16,8 @@ const { getBytecodeHash } = require('@axelar-network/axelar-chains-config'); const AxelarServiceGovernance = getContractJSON('AxelarServiceGovernance'); const Create3Deployer = getContractJSON('Create3Deployer'); +const MINT_BURN = 4; + describe('Interchain Token Service Upgrade Flow', () => { let wallet, otherWallet, signer; let service, gateway, gasService; @@ -51,9 +53,9 @@ describe('Interchain Token Service Upgrade Flow', () => { ]); const params = defaultAbiCoder.encode(['bytes', 'address'], [wallet.address, token.address]); - await expect(service.deployTokenManager(salt, '', 0, params, 0)) + await expect(service.deployTokenManager(salt, '', MINT_BURN, params, 0)) .to.emit(service, 'TokenManagerDeployed') - .withArgs(tokenId, tokenManager.address, 0, params); + .withArgs(tokenId, tokenManager.address, MINT_BURN, params); } before(async () => { diff --git a/test/TokenManager.js b/test/TokenManager.js index e62e325e..18fd33de 100644 --- a/test/TokenManager.js +++ b/test/TokenManager.js @@ -61,6 +61,24 @@ describe('Token Manager', () => { await expectRevert((gasOptions) => TestTokenManager.approveService(gasOptions), TestTokenManager, 'NotService', [owner.address]); }); + it('Should revert on mintToken when calling directly', async () => { + await expectRevert( + (gasOptions) => TestTokenManager.mintToken(other.address, owner.address, 1234, gasOptions), + TestTokenManager, + 'NotService', + [owner.address], + ); + }); + + it('Should revert on burnToken when calling directly', async () => { + await expectRevert( + (gasOptions) => TestTokenManager.burnToken(other.address, owner.address, 1234, gasOptions), + TestTokenManager, + 'NotService', + [owner.address], + ); + }); + it('Should return the correct parameters for a token manager', async () => { const expectedParams = defaultAbiCoder.encode(['bytes', 'address'], [toUtf8Bytes(owner.address), other.address]); const params = await TestTokenManager.params(toUtf8Bytes(owner.address), other.address);