diff --git a/contracts/InterchainTokenService.sol b/contracts/InterchainTokenService.sol index 6a1b510b..6e0d500b 100644 --- a/contracts/InterchainTokenService.sol +++ b/contracts/InterchainTokenService.sol @@ -266,7 +266,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. @@ -278,9 +278,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); @@ -325,7 +330,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); } @@ -760,7 +765,6 @@ contract InterchainTokenService is /** * @notice Processes a deploy token manager payload. - * @param payload The encoded data payload to be processed */ function _processDeployTokenManagerPayload(bytes memory payload) internal { (, bytes32 tokenId, TokenManagerType tokenManagerType, bytes memory params) = abi.decode( @@ -768,6 +772,8 @@ contract InterchainTokenService is (uint256, bytes32, TokenManagerType, bytes) ); + if (tokenManagerType == TokenManagerType.NATIVE_INTERCHAIN_TOKEN) revert CannotDeploy(tokenManagerType); + _deployTokenManager(tokenId, tokenManagerType, params); } @@ -784,7 +790,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)); } /** diff --git a/contracts/TokenHandler.sol b/contracts/TokenHandler.sol index 35c5ff5f..95a82c47 100644 --- a/contracts/TokenHandler.sol +++ b/contracts/TokenHandler.sol @@ -50,8 +50,13 @@ 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)) { - _giveTokenMintBurn(tokenAddress, to, amount); + _mintToken(tokenManager, tokenAddress, to, amount); return (amount, tokenAddress); } @@ -90,15 +95,16 @@ contract TokenHandler is ITokenHandler, ITokenManagerType, ReentrancyGuard, Crea uint256 amount ) 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); - if (tokenManagerType == uint256(TokenManagerType.MINT_BURN)) { - _takeTokenMintBurn(tokenAddress, from, amount); + if (tokenManagerType == uint256(TokenManagerType.NATIVE_INTERCHAIN_TOKEN)) { + _takeInterchainToken(tokenAddress, from, amount); + } else if (tokenManagerType == uint256(TokenManagerType.MINT_BURN)) { + _burnToken(tokenManager, tokenAddress, from, amount); } else if (tokenManagerType == uint256(TokenManagerType.MINT_BURN_FROM)) { - _takeTokenMintBurnFrom(tokenAddress, from, amount); + _burnTokenFrom(tokenAddress, from, amount); } else if (tokenManagerType == uint256(TokenManagerType.LOCK_UNLOCK)) { _transferTokenFrom(tokenAddress, from, tokenManager, amount); } else if (tokenManagerType == uint256(TokenManagerType.LOCK_UNLOCK_FEE)) { @@ -128,10 +134,10 @@ contract TokenHandler is ITokenHandler, ITokenManagerType, ReentrancyGuard, Crea // slither-disable-next-line locked-ether 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) || @@ -195,15 +201,23 @@ contract TokenHandler is ITokenHandler, ITokenManagerType, ReentrancyGuard, Crea 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 f9fc592d..33787cca 100644 --- a/contracts/interfaces/IInterchainTokenService.sol +++ b/contracts/interfaces/IInterchainTokenService.sol @@ -47,6 +47,7 @@ interface IInterchainTokenService is error EmptyData(); error PostDeployFailed(bytes data); error ZeroAmount(); + error CannotDeploy(TokenManagerType); error InvalidGatewayTokenTransfer(bytes32 tokenId, bytes payload, string tokenSymbol, uint256 amount); event InterchainTransfer( @@ -167,7 +168,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/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..552f6df7 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 sent throught the gateway 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 f492b1d8..cdefab02 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; @@ -185,7 +186,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); @@ -583,7 +584,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); @@ -719,32 +720,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); @@ -762,7 +737,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; @@ -791,7 +766,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 () => { @@ -1152,6 +1149,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', () => { @@ -1620,7 +1642,8 @@ describe('Interchain Token Service', () => { it(`Should initiate an interchain token transfer via the interchainTransfer standard contract call & express call [gateway]`, async () => { const symbol = 'TT1'; - const [token, , tokenId] = await deployFunctions.gateway(`Test Token gateway`, symbol, 12, amount * 3); + const [token, tokenManager, tokenId] = await deployFunctions.gateway(`Test Token gateway`, symbol, 12, amount * 3); + console.log(await tokenManager.implementationType()); const sendAmount = amount; const metadata = '0x00000000'; const payload = defaultAbiCoder.encode( @@ -1632,7 +1655,6 @@ describe('Interchain Token Service', () => { const metadataExpress = '0x00000001'; const transferToAddress = service.address; - await expectRevert( (gasOptions) => service.interchainTransfer(tokenId, 'Untrusted Chain', destAddress, amount, metadata, gasValue, { @@ -2562,6 +2584,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); @@ -2729,6 +2776,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); 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);