diff --git a/contracts/InterchainTokenFactory.sol b/contracts/InterchainTokenFactory.sol index f3be98d3..51c0e439 100644 --- a/contracts/InterchainTokenFactory.sol +++ b/contracts/InterchainTokenFactory.sol @@ -119,6 +119,7 @@ contract InterchainTokenFactory is IInterchainTokenFactory, ITokenManagerType, M * @notice Deploys a new interchain token with specified parameters. * @dev Creates a new token and optionally mints an initial amount to a specified minter. * This function is `payable` because non-payable functions cannot be called in a multicall that calls other `payable` functions. + * Cannot deploy tokens with empty supply and no minter. * @param salt The unique salt for deploying the token. * @param name The name of the token. * @param symbol The symbol of the token. @@ -149,6 +150,8 @@ contract InterchainTokenFactory is IInterchainTokenFactory, ITokenManagerType, M if (minter == address(interchainTokenService)) revert InvalidMinter(minter); minterBytes = minter.toBytes(); + } else { + revert ZeroSupplyToken(); } tokenId = _deployInterchainToken(deploySalt, currentChain, name, symbol, decimals, minterBytes, gasValue); @@ -381,12 +384,14 @@ contract InterchainTokenFactory is IInterchainTokenFactory, ITokenManagerType, M bytes memory minter, uint256 gasValue ) internal returns (bytes32 tokenId) { + // Ensure that a token is registered locally for the tokenId before allowing a remote deployment bytes32 expectedTokenId = _interchainTokenId(deploySalt); - // Ensure that a local token has been registered for the tokenId - IERC20Named token = IERC20Named(interchainTokenService.registeredTokenAddress(expectedTokenId)); + address tokenAddress = interchainTokenService.registeredTokenAddress(expectedTokenId); // The local token must expose the name, symbol, and decimals metadata - tokenId = _deployInterchainToken(deploySalt, destinationChain, token.name(), token.symbol(), token.decimals(), minter, gasValue); + (string memory name, string memory symbol, uint8 decimals) = _getTokenMetadata(tokenAddress); + + tokenId = _deployInterchainToken(deploySalt, destinationChain, name, symbol, decimals, minter, gasValue); if (tokenId != expectedTokenId) revert InvalidTokenId(tokenId, expectedTokenId); } @@ -402,9 +407,42 @@ contract InterchainTokenFactory is IInterchainTokenFactory, ITokenManagerType, M string memory currentChain = ''; uint256 gasValue = 0; + // Ensure that the ERC20 token has metadata before registering it + // slither-disable-next-line unused-return + _getTokenMetadata(tokenAddress); + tokenId = interchainTokenService.deployTokenManager(deploySalt, currentChain, TokenManagerType.LOCK_UNLOCK, params, gasValue); } + /** + * @notice Retrieves the metadata of an ERC20 token. Reverts with `NotToken` error if metadata is not available. + * @param tokenAddress The address of the token. + * @return name The name of the token. + * @return symbol The symbol of the token. + * @return decimals The number of decimals for the token. + */ + function _getTokenMetadata(address tokenAddress) internal view returns (string memory name, string memory symbol, uint8 decimals) { + IERC20Named token = IERC20Named(tokenAddress); + + try token.name() returns (string memory name_) { + name = name_; + } catch { + revert NotToken(tokenAddress); + } + + try token.symbol() returns (string memory symbol_) { + symbol = symbol_; + } catch { + revert NotToken(tokenAddress); + } + + try token.decimals() returns (uint8 decimals_) { + decimals = decimals_; + } catch { + revert NotToken(tokenAddress); + } + } + /** * @notice Deploys a canonical interchain token on a remote chain. * @param originalTokenAddress The address of the original token on the original chain. diff --git a/contracts/InterchainTokenService.sol b/contracts/InterchainTokenService.sol index 7c83b429..84053af1 100644 --- a/contracts/InterchainTokenService.sol +++ b/contracts/InterchainTokenService.sol @@ -395,6 +395,26 @@ contract InterchainTokenService is return _contractCallValue(payload); } + /** + * @notice Executes the cross-chain ITS message. + * @param commandId The unique message id. + * @param sourceChain The chain where the transaction originates from. + * @param sourceAddress The address of the remote ITS where the transaction originates from. + * @param payload The encoded data payload for the transaction. + */ + function execute( + bytes32 commandId, + string calldata sourceChain, + string calldata sourceAddress, + bytes calldata payload + ) external onlyRemoteService(sourceChain, sourceAddress) whenNotPaused { + bytes32 payloadHash = keccak256(payload); + + if (!gateway.validateContractCall(commandId, sourceChain, sourceAddress, payloadHash)) revert NotApprovedByGateway(); + + _execute(commandId, sourceChain, sourceAddress, payload, payloadHash); + } + /** * @notice Express executes operations based on the payload and selector. * @dev This function is `payable` because non-payable functions cannot be called in a multicall that calls other `payable` functions. @@ -646,26 +666,6 @@ contract InterchainTokenService is } } - /** - * @notice Executes operations based on the payload and selector. - * @param commandId The unique message id. - * @param sourceChain The chain where the transaction originates from. - * @param sourceAddress The address of the remote ITS where the transaction originates from. - * @param payload The encoded data payload for the transaction. - */ - function execute( - bytes32 commandId, - string calldata sourceChain, - string calldata sourceAddress, - bytes calldata payload - ) external onlyRemoteService(sourceChain, sourceAddress) whenNotPaused { - bytes32 payloadHash = keccak256(payload); - - if (!gateway.validateContractCall(commandId, sourceChain, sourceAddress, payloadHash)) revert NotApprovedByGateway(); - - _execute(commandId, sourceChain, sourceAddress, payload, payloadHash); - } - /** * @notice Processes the payload data for a send token call. * @param commandId The unique message id. diff --git a/contracts/TokenHandler.sol b/contracts/TokenHandler.sol index d7e36209..5db715f6 100644 --- a/contracts/TokenHandler.sol +++ b/contracts/TokenHandler.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.0; 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 { 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 { Create3AddressFixed } from './utils/Create3AddressFixed.sol'; @@ -16,12 +16,11 @@ import { IMinter } from './interfaces/IMinter.sol'; /** * @title TokenHandler - * @notice This interface is responsible for handling tokens before initiating an interchain token transfer, or after receiving one. + * @notice This contract is responsible for handling tokens before initiating an interchain token transfer, or after receiving one. */ contract TokenHandler is ITokenHandler, ITokenManagerType, ReentrancyGuard, Create3AddressFixed { using SafeTokenTransferFrom for IERC20; using SafeTokenCall for IERC20; - using SafeTokenTransfer for IERC20; /** * @notice This function gives token to a specified address from the token manager. diff --git a/contracts/interfaces/IInterchainTokenFactory.sol b/contracts/interfaces/IInterchainTokenFactory.sol index e425ecbf..fd63f3c7 100644 --- a/contracts/interfaces/IInterchainTokenFactory.sol +++ b/contracts/interfaces/IInterchainTokenFactory.sol @@ -16,11 +16,11 @@ interface IInterchainTokenFactory is IUpgradable, IMulticall { error InvalidChainName(); error InvalidMinter(address minter); error NotMinter(address minter); - error NotOperator(address operator); - error NotServiceOwner(address sender); error NotSupported(); error RemoteDeploymentNotApproved(); error InvalidTokenId(bytes32 tokenId, bytes32 expectedTokenId); + error ZeroSupplyToken(); + error NotToken(address tokenAddress); /// @notice Emitted when a minter approves a deployer for a remote interchain token deployment that uses a custom destinationMinter address. event DeployRemoteInterchainTokenApproval( diff --git a/contracts/interfaces/IInterchainTokenService.sol b/contracts/interfaces/IInterchainTokenService.sol index b3f5fc12..f6aef9cd 100644 --- a/contracts/interfaces/IInterchainTokenService.sol +++ b/contracts/interfaces/IInterchainTokenService.sol @@ -29,7 +29,6 @@ interface IInterchainTokenService is IAddressTracker, IUpgradable { - error InvalidTokenManagerImplementationType(address implementation); error InvalidChainName(); error NotRemoteService(); error TokenManagerDoesNotExist(bytes32 tokenId); diff --git a/contracts/interfaces/ITokenManagerDeployer.sol b/contracts/interfaces/ITokenManagerDeployer.sol index 8e9acdd6..5478d0c1 100644 --- a/contracts/interfaces/ITokenManagerDeployer.sol +++ b/contracts/interfaces/ITokenManagerDeployer.sol @@ -7,7 +7,6 @@ pragma solidity ^0.8.0; * @notice This interface is used to deploy new instances of the TokenManagerProxy contract. */ interface ITokenManagerDeployer { - error AddressZero(); error TokenManagerDeploymentFailed(); /** diff --git a/contracts/types/InterchainTokenServiceTypes.sol b/contracts/types/InterchainTokenServiceTypes.sol index 9e293028..89d886ef 100644 --- a/contracts/types/InterchainTokenServiceTypes.sol +++ b/contracts/types/InterchainTokenServiceTypes.sol @@ -5,7 +5,9 @@ pragma solidity ^0.8.0; enum MessageType { INTERCHAIN_TRANSFER, DEPLOY_INTERCHAIN_TOKEN, - DEPLOY_TOKEN_MANAGER + DEPLOY_TOKEN_MANAGER, + SEND_TO_HUB, + RECEIVE_FROM_HUB } struct InterchainTransfer { diff --git a/package-lock.json b/package-lock.json index d2daf0e2..3b9f970a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@axelar-network/interchain-token-service", - "version": "1.2.4", + "version": "2.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@axelar-network/interchain-token-service", - "version": "1.2.4", + "version": "2.0.1", "license": "MIT", "dependencies": { "@axelar-network/axelar-cgp-solidity": "6.4.0", diff --git a/test/InterchainTokenFactory.js b/test/InterchainTokenFactory.js index da105371..f0b833bd 100644 --- a/test/InterchainTokenFactory.js +++ b/test/InterchainTokenFactory.js @@ -115,6 +115,15 @@ describe('InterchainTokenFactory', () => { .withArgs(tokenId, tokenManagerAddress, LOCK_UNLOCK, params); }); + it('Should not register a non-existing token', async () => { + await expectRevert( + (gasOptions) => tokenFactory.registerCanonicalInterchainToken(tokenFactory.address, { gasOptions }), + tokenFactory, + 'NotToken', + [tokenFactory.address], + ); + }); + it('Should initiate a remote interchain token deployment with no original chain name provided', async () => { const gasValue = 1234; const payload = defaultAbiCoder.encode( @@ -232,21 +241,16 @@ describe('InterchainTokenFactory', () => { await checkRoles(tokenManager, minter); }); - it('Should register a token if the mint amount is zero and minter is the zero address', async () => { + it('Should revert when trying to register a token if the mint amount is zero and minter is the zero address', async () => { const salt = keccak256('0x123456'); tokenId = await tokenFactory.interchainTokenId(wallet.address, salt); - const tokenAddress = await service.interchainTokenAddress(tokenId); - const minterBytes = new Uint8Array(); - const params = defaultAbiCoder.encode(['bytes', 'address'], [minterBytes, tokenAddress]); - const tokenManager = await getContractAt('TokenManager', await service.tokenManagerAddress(tokenId), wallet); - - await expect(tokenFactory.deployInterchainToken(salt, name, symbol, decimals, 0, AddressZero)) - .to.emit(service, 'InterchainTokenDeployed') - .withArgs(tokenId, tokenAddress, AddressZero, name, symbol, decimals) - .and.to.emit(service, 'TokenManagerDeployed') - .withArgs(tokenId, tokenManager.address, NATIVE_INTERCHAIN_TOKEN, params); - await checkRoles(tokenManager, AddressZero); + await expectRevert( + (gasOptions) => tokenFactory.deployInterchainToken(salt, name, symbol, decimals, 0, AddressZero, { gasOptions }), + tokenFactory, + 'ZeroSupplyToken', + [], + ); }); it('Should register a token if the mint amount is greater than zero and the minter is the zero address', async () => {