diff --git a/.changeset/many-tigers-kneel.md b/.changeset/many-tigers-kneel.md new file mode 100644 index 00000000..0742f1cf --- /dev/null +++ b/.changeset/many-tigers-kneel.md @@ -0,0 +1,5 @@ +--- +'@axelar-network/interchain-token-service': minor +--- + +Interchain tokens now get minted/burnt by the token manager to be consistent with custom tokens diff --git a/DESIGN.md b/DESIGN.md index a9187a40..aae5c644 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -41,6 +41,8 @@ Most current bridge designs aim to transfer a pre-existing, popular token to dif We designed an [interface](./contracts/interfaces/IInterchainTokenStandard.sol) along with an [example implementation](./contracts/interchain-token/InterchainTokenStandard.sol) of an ERC20 that can use the `InterchainTokenService` internally. This has the main benefit that for `TokenManagers` that require user approval (Lock/Unlock, Lock/Unlock Fee and Mint/BurnFrom), the token can provide this approval within the same call, providing better UX for users, and saving them some gas. +Interchain Tokens function the same as mint/burn tokens do: The `tokenManager` that manages them will ask them to `burn` tokens on the sending chain, and to `mint` tokens on the receiving chain. + ## Interchain Communication Spec The messages going through the Axelar Network between `InterchainTokenServices` need to have a consistent format to be understood properly. We chose to use `abi` encoding because it is easy to use in EVM chains, which are at the front and center of programmable blockchains, and because it is easy to implement in other ecosystems which tend to be more gas efficient. There are currently three supported message types: `INTERCHAIN_TRANSFER`, `DEPLOY_INTERCHAIN_TOKEN`, `DEPLOY_TOKEN_MANAGER`. diff --git a/contracts/TokenHandler.sol b/contracts/TokenHandler.sol index 8db4492d..baabe92d 100644 --- a/contracts/TokenHandler.sol +++ b/contracts/TokenHandler.sol @@ -11,8 +11,8 @@ import { Create3AddressFixed } from './utils/Create3AddressFixed.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 { IMinter } from './interfaces/IMinter.sol'; /** * @title TokenHandler @@ -38,13 +38,12 @@ contract TokenHandler is ITokenHandler, ITokenManagerType, ReentrancyGuard, Crea /// @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)) { - _mintToken(tokenManager, tokenAddress, to, amount); + if ( + tokenManagerType == uint256(TokenManagerType.NATIVE_INTERCHAIN_TOKEN) || + tokenManagerType == uint256(TokenManagerType.MINT_BURN) || + tokenManagerType == uint256(TokenManagerType.MINT_BURN_FROM) + ) { + _mintToken(ITokenManager(tokenManager), tokenAddress, to, amount); return (amount, tokenAddress); } @@ -76,10 +75,10 @@ contract TokenHandler is ITokenHandler, ITokenManagerType, ReentrancyGuard, Crea if (tokenOnly && msg.sender != tokenAddress) revert NotToken(msg.sender, tokenAddress); - if (tokenManagerType == uint256(TokenManagerType.NATIVE_INTERCHAIN_TOKEN)) { - _takeInterchainToken(tokenAddress, from, amount); - } else if (tokenManagerType == uint256(TokenManagerType.MINT_BURN)) { - _burnToken(tokenManager, tokenAddress, from, amount); + if ( + tokenManagerType == uint256(TokenManagerType.NATIVE_INTERCHAIN_TOKEN) || tokenManagerType == uint256(TokenManagerType.MINT_BURN) + ) { + _burnToken(ITokenManager(tokenManager), tokenAddress, from, amount); } else if (tokenManagerType == uint256(TokenManagerType.MINT_BURN_FROM)) { _burnTokenFrom(tokenAddress, from, amount); } else if (tokenManagerType == uint256(TokenManagerType.LOCK_UNLOCK)) { @@ -133,10 +132,16 @@ contract TokenHandler is ITokenHandler, ITokenManagerType, ReentrancyGuard, Crea * @param tokenManager The address of the token manager. */ // slither-disable-next-line locked-ether - function postTokenManagerDeploy(uint256 tokenManagerType, address tokenManager) external payable { - // For lock/unlock token managers, the ITS contract needs an approval from the token manager to transfer tokens on its behalf - if (tokenManagerType == uint256(TokenManagerType.LOCK_UNLOCK) || tokenManagerType == uint256(TokenManagerType.LOCK_UNLOCK_FEE)) { - ITokenManager(tokenManager).approveService(); + function postTokenManagerDeploy(uint256 tokenManagerType, ITokenManager tokenManager) external payable { + // For native interchain tokens, we transfer mintership to the token manager. + // This is done here because the InterchainToken bytecode is preferred to be fixed to avoid having multiple versions of the Token code used in production. + if (tokenManagerType == uint256(TokenManagerType.NATIVE_INTERCHAIN_TOKEN)) { + IMinter(tokenManager.tokenAddress()).transferMintership(address(tokenManager)); + // For lock/unlock token managers, the ITS contract needs an approval from the token manager to transfer tokens on its behalf. + } else if ( + tokenManagerType == uint256(TokenManagerType.LOCK_UNLOCK) || tokenManagerType == uint256(TokenManagerType.LOCK_UNLOCK_FEE) + ) { + tokenManager.approveService(); } } @@ -160,20 +165,12 @@ contract TokenHandler is ITokenHandler, ITokenManagerType, ReentrancyGuard, Crea return diff < amount ? diff : amount; } - function _giveInterchainToken(address tokenAddress, address to, uint256 amount) internal { - IERC20(tokenAddress).safeCall(abi.encodeWithSelector(IERC20MintableBurnable.mint.selector, to, amount)); - } - - function _takeInterchainToken(address tokenAddress, address from, uint256 amount) internal { - IERC20(tokenAddress).safeCall(abi.encodeWithSelector(IERC20MintableBurnable.burn.selector, from, amount)); - } - - function _mintToken(address tokenManager, address tokenAddress, address to, uint256 amount) internal { - ITokenManager(tokenManager).mintToken(tokenAddress, to, amount); + function _mintToken(ITokenManager tokenManager, address tokenAddress, address to, uint256 amount) internal { + tokenManager.mintToken(tokenAddress, to, amount); } - function _burnToken(address tokenManager, address tokenAddress, address from, uint256 amount) internal { - ITokenManager(tokenManager).burnToken(tokenAddress, from, amount); + function _burnToken(ITokenManager tokenManager, address tokenAddress, address from, uint256 amount) internal { + tokenManager.burnToken(tokenAddress, from, amount); } function _burnTokenFrom(address tokenAddress, address from, uint256 amount) internal { diff --git a/contracts/interfaces/ITokenHandler.sol b/contracts/interfaces/ITokenHandler.sol index 7650de21..4d697b9b 100644 --- a/contracts/interfaces/ITokenHandler.sol +++ b/contracts/interfaces/ITokenHandler.sol @@ -2,6 +2,8 @@ pragma solidity ^0.8.0; +import { ITokenManager } from './ITokenManager.sol'; + /** * @title ITokenHandler Interface * @notice This interface is responsible for handling tokens before initiating an interchain token transfer, or after receiving one. @@ -47,5 +49,5 @@ interface ITokenHandler { * @param tokenManagerType The token manager type. * @param tokenManager The address of the token manager. */ - function postTokenManagerDeploy(uint256 tokenManagerType, address tokenManager) external payable; + function postTokenManagerDeploy(uint256 tokenManagerType, ITokenManager tokenManager) external payable; } diff --git a/test/InterchainTokenFactory.js b/test/InterchainTokenFactory.js index a27c5325..b75e2c49 100644 --- a/test/InterchainTokenFactory.js +++ b/test/InterchainTokenFactory.js @@ -216,7 +216,7 @@ describe('InterchainTokenFactory', () => { const checkRoles = async (tokenManager, minter) => { const token = await getContractAt('InterchainToken', await tokenManager.tokenAddress(), wallet); expect(await token.isMinter(minter)).to.be.true; - expect(await token.isMinter(service.address)).to.be.true; + expect(await token.isMinter(tokenManager.address)).to.be.true; expect(await tokenManager.isOperator(minter)).to.be.true; expect(await tokenManager.isOperator(service.address)).to.be.true; @@ -342,7 +342,11 @@ describe('InterchainTokenFactory', () => { .and.to.emit(tokenManager, 'RolesRemoved') .withArgs(tokenFactory.address, 1 << OPERATOR_ROLE) .and.to.emit(tokenManager, 'RolesRemoved') - .withArgs(tokenFactory.address, 1 << FLOW_LIMITER_ROLE); + .withArgs(tokenFactory.address, 1 << FLOW_LIMITER_ROLE) + .and.to.emit(token, 'RolesRemoved') + .withArgs(service.address, 1 << MINTER_ROLE) + .and.to.emit(token, 'RolesAdded') + .withArgs(tokenManager.address, 1 << MINTER_ROLE); const payload = defaultAbiCoder.encode( ['uint256', 'bytes32', 'string', 'string', 'uint8', 'bytes'], @@ -398,7 +402,7 @@ describe('InterchainTokenFactory', () => { }, ), tokenFactory, - 'InvalidMinter', + 'NotMinter', [service.address], ); @@ -451,7 +455,7 @@ describe('InterchainTokenFactory', () => { }, ), tokenFactory, - 'InvalidMinter', + 'NotMinter', [service.address], ); diff --git a/test/InterchainTokenService.js b/test/InterchainTokenService.js index 19ee9633..61c0643a 100644 --- a/test/InterchainTokenService.js +++ b/test/InterchainTokenService.js @@ -680,6 +680,26 @@ describe('Interchain Token Service', () => { }); }); + describe('Deploy and Register Interchain Token', () => { + const tokenName = 'Token Name'; + const tokenSymbol = 'TN'; + const tokenDecimals = 13; + const salt = getRandomBytes32(); + + it('Should revert when registering an interchain token when service is paused', async () => { + await service.setPauseStatus(true).then((tx) => tx.wait); + + await expectRevert( + (gasOptions) => + service.deployInterchainToken(salt, '', tokenName, tokenSymbol, tokenDecimals, wallet.address, 0, gasOptions), + service, + 'Pause', + ); + + await service.setPauseStatus(false).then((tx) => tx.wait); + }); + }); + describe('Deploy and Register remote Interchain Token', () => { const tokenName = 'Token Name'; const tokenSymbol = 'TN';