From 6c9056598b9bc78121c7e70acce51c4b8cf308ce Mon Sep 17 00:00:00 2001 From: Foivos Date: Tue, 10 Dec 2024 16:02:00 +0200 Subject: [PATCH 01/16] Addressed ackee's report. --- contracts/InterchainTokenFactory.sol | 2 + contracts/InterchainTokenService.sol | 40 +++++++++---------- contracts/TokenHandler.sol | 3 +- contracts/interchain-token/ERC20Permit.sol | 10 +++++ .../interchain-token/InterchainToken.sol | 7 +--- contracts/interfaces/IBaseTokenManager.sol | 2 + .../interfaces/IInterchainTokenFactory.sol | 3 +- .../interfaces/IInterchainTokenService.sol | 1 - contracts/interfaces/ITokenManager.sol | 8 +--- .../interfaces/ITokenManagerDeployer.sol | 1 - contracts/token-manager/TokenManager.sol | 7 +++- .../types/InterchainTokenServiceTypes.sol | 4 +- 12 files changed, 47 insertions(+), 41 deletions(-) diff --git a/contracts/InterchainTokenFactory.sol b/contracts/InterchainTokenFactory.sol index f3be98d3..31b4422b 100644 --- a/contracts/InterchainTokenFactory.sol +++ b/contracts/InterchainTokenFactory.sol @@ -149,6 +149,8 @@ contract InterchainTokenFactory is IInterchainTokenFactory, ITokenManagerType, M if (minter == address(interchainTokenService)) revert InvalidMinter(minter); minterBytes = minter.toBytes(); + } else { + revert EmptyInterchainToken(); } tokenId = _deployInterchainToken(deploySalt, currentChain, name, symbol, decimals, minterBytes, gasValue); diff --git a/contracts/InterchainTokenService.sol b/contracts/InterchainTokenService.sol index 7c83b429..fff6d471 100644 --- a/contracts/InterchainTokenService.sol +++ b/contracts/InterchainTokenService.sol @@ -395,6 +395,26 @@ contract InterchainTokenService is return _contractCallValue(payload); } + /** + * @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 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 be794e9a..69932021 100644 --- a/contracts/TokenHandler.sol +++ b/contracts/TokenHandler.sol @@ -16,12 +16,11 @@ import { IERC20BurnableFrom } from './interfaces/IERC20BurnableFrom.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/interchain-token/ERC20Permit.sol b/contracts/interchain-token/ERC20Permit.sol index b692d723..00826284 100644 --- a/contracts/interchain-token/ERC20Permit.sol +++ b/contracts/interchain-token/ERC20Permit.sol @@ -23,6 +23,7 @@ abstract contract ERC20Permit is IERC20, IERC20Permit, ERC20 { */ bytes32 public nameHash; + // Prefix for the EIP712 structured data. string private constant EIP191_PREFIX_FOR_EIP712_STRUCTURED_DATA = '\x19\x01'; // keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)') @@ -71,6 +72,15 @@ abstract contract ERC20Permit is IERC20, IERC20Permit, ERC20 { function permit(address issuer, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external { if (block.timestamp > deadline) revert PermitExpired(); + // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature + // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines + // the valid range for s in (281): 0 < s < secp256k1n ÷ 2 + 1, and for v in (282): v ∈ {27, 28}. Most + // signatures from current libraries generate a unique signature with an s-value in the lower half order. + // + // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value + // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or + // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept + // these malleable signatures as well. if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) revert InvalidS(); if (v != 27 && v != 28) revert InvalidV(); diff --git a/contracts/interchain-token/InterchainToken.sol b/contracts/interchain-token/InterchainToken.sol index 1dea362a..03265886 100644 --- a/contracts/interchain-token/InterchainToken.sol +++ b/contracts/interchain-token/InterchainToken.sol @@ -2,12 +2,9 @@ pragma solidity ^0.8.0; -import { AddressBytes } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/libs/AddressBytes.sol'; - import { IInterchainToken } from '../interfaces/IInterchainToken.sol'; import { InterchainTokenStandard } from './InterchainTokenStandard.sol'; -import { ERC20 } from './ERC20.sol'; import { ERC20Permit } from './ERC20Permit.sol'; import { Minter } from '../utils/Minter.sol'; @@ -16,9 +13,7 @@ import { Minter } from '../utils/Minter.sol'; * @notice This contract implements an interchain token which extends InterchainToken functionality. * @dev This contract also inherits Minter and Implementation logic. */ -contract InterchainToken is InterchainTokenStandard, ERC20, ERC20Permit, Minter, IInterchainToken { - using AddressBytes for bytes; - +contract InterchainToken is InterchainTokenStandard, ERC20Permit, Minter, IInterchainToken { string public name; string public symbol; uint8 public decimals; diff --git a/contracts/interfaces/IBaseTokenManager.sol b/contracts/interfaces/IBaseTokenManager.sol index 2ceff77d..b3dba446 100644 --- a/contracts/interfaces/IBaseTokenManager.sol +++ b/contracts/interfaces/IBaseTokenManager.sol @@ -9,12 +9,14 @@ pragma solidity ^0.8.0; interface IBaseTokenManager { /** * @notice A function that returns the token id. + * @dev This is stored in the proxy and should not be called in the implementation, but it is included here so that the interface properly tells us what functions exist. */ function interchainTokenId() external view returns (bytes32); /** * @notice A function that should return the address of the token. * Must be overridden in the inheriting contract. + * @dev This is stored in the proxy and should not be called in the implementation, but it is included here so that the interface properly tells us what functions exist. * @return address address of the token. */ function tokenAddress() external view returns (address); diff --git a/contracts/interfaces/IInterchainTokenFactory.sol b/contracts/interfaces/IInterchainTokenFactory.sol index e425ecbf..0db2aa76 100644 --- a/contracts/interfaces/IInterchainTokenFactory.sol +++ b/contracts/interfaces/IInterchainTokenFactory.sol @@ -16,11 +16,10 @@ 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 EmptyInterchainToken(); /// @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/ITokenManager.sol b/contracts/interfaces/ITokenManager.sol index 50c582a9..23c778b2 100644 --- a/contracts/interfaces/ITokenManager.sol +++ b/contracts/interfaces/ITokenManager.sol @@ -15,16 +15,12 @@ import { IFlowLimit } from './IFlowLimit.sol'; interface ITokenManager is IBaseTokenManager, IOperator, IFlowLimit, IImplementation { error TokenLinkerZeroAddress(); error NotService(address caller); - error TakeTokenFailed(); - error GiveTokenFailed(); - error NotToken(address caller); - error ZeroAddress(); - error AlreadyFlowLimiter(address flowLimiter); - error NotFlowLimiter(address flowLimiter); + error ZeroTokenAddress(); error NotSupported(); /** * @notice Returns implementation type of this token manager. + * @dev This is stored in the proxy and should not be called in the implementation, but it is included here so that the interface properly tells us what functions exist. * @return uint256 The implementation type of this token manager. */ function implementationType() external view returns (uint256); 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/token-manager/TokenManager.sol b/contracts/token-manager/TokenManager.sol index e6d30174..dbcc61db 100644 --- a/contracts/token-manager/TokenManager.sol +++ b/contracts/token-manager/TokenManager.sol @@ -18,6 +18,7 @@ import { FlowLimit } from '../utils/FlowLimit.sol'; /** * @title TokenManager * @notice This contract is responsible for managing tokens, such as setting locking token balances, or setting flow limits, for interchain transfers. + * @dev Should only be used as an implementation for TokenManagerProxy. */ contract TokenManager is ITokenManager, Operator, FlowLimit, Implementation, Multicall { using AddressBytes for bytes; @@ -57,8 +58,8 @@ contract TokenManager is ITokenManager, Operator, FlowLimit, Implementation, Mul /** * @notice Reads the token address from the proxy. - * @dev This function is not supported when directly called on the implementation. It - * must be called by the proxy. + * @dev This function is not supported when directly called on the implementation. + * It must be called by the proxy. It is included here so that the interace shows this function as existing, for better UX. * @return tokenAddress_ The address of the token. */ function tokenAddress() external view virtual returns (address) { @@ -68,6 +69,7 @@ contract TokenManager is ITokenManager, Operator, FlowLimit, Implementation, Mul /** * @notice A function that returns the token id. * @dev This will only work when implementation is called by a proxy, which stores the tokenId as an immutable. + * It is included here so that the interace shows this function as existing, for better UX. * @return bytes32 The interchain token ID. */ function interchainTokenId() public pure returns (bytes32) { @@ -89,6 +91,7 @@ contract TokenManager is ITokenManager, Operator, FlowLimit, Implementation, Mul */ function getTokenAddressFromParams(bytes calldata params_) external pure returns (address tokenAddress_) { (, tokenAddress_) = abi.decode(params_, (bytes, address)); + if (tokenAddress_ == address(0)) revert ZeroTokenAddress(); } /** 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 { From 9aef7679169e7c1f223642d8a09946707eb81897 Mon Sep 17 00:00:00 2001 From: Foivos Date: Wed, 11 Dec 2024 17:43:52 +0200 Subject: [PATCH 02/16] fix all tests --- test/InterchainToken.js | 2 +- test/InterchainTokenFactory.js | 17 ++++++----------- test/InterchainTokenService.js | 4 ++-- test/TokenManager.js | 2 +- 4 files changed, 10 insertions(+), 15 deletions(-) diff --git a/test/InterchainToken.js b/test/InterchainToken.js index a7ff687a..300678c7 100644 --- a/test/InterchainToken.js +++ b/test/InterchainToken.js @@ -147,7 +147,7 @@ describe('InterchainToken', () => { const contractBytecodeHash = keccak256(contractBytecode); const expected = { - london: '0xa01cf28b0b6ce6dc3b466e995585d69486400d671fce0ea8d06beba583e6f3bb', + london: '0x482146829055f052063003e9cf0ffaf798a12fb58088c2667566a135b9568355', }[getEVMVersion()]; expect(contractBytecodeHash).to.be.equal(expected); diff --git a/test/InterchainTokenFactory.js b/test/InterchainTokenFactory.js index fa9e687e..0e2cd5f3 100644 --- a/test/InterchainTokenFactory.js +++ b/test/InterchainTokenFactory.js @@ -235,18 +235,13 @@ describe('InterchainTokenFactory', () => { it('Should 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, + 'EmptyInterchainToken', + [], + ); }); it('Should register a token if the mint amount is greater than zero and the minter is the zero address', async () => { diff --git a/test/InterchainTokenService.js b/test/InterchainTokenService.js index 2cb75946..2dc561c3 100644 --- a/test/InterchainTokenService.js +++ b/test/InterchainTokenService.js @@ -909,13 +909,13 @@ describe('Interchain Token Service', () => { ); }); - it('Should revert on deploying a token manager if token handler post deploy fails', async () => { + it('Should revert on deploying a token manager with an empty token', async () => { const params = defaultAbiCoder.encode(['bytes', 'address'], [wallet.address, AddressZero]); await expectRevert( (gasOptions) => service.deployTokenManager(salt, '', LOCK_UNLOCK, params, 0, gasOptions), service, - 'PostDeployFailed', + 'TokenManagerDeploymentFailed', ); }); diff --git a/test/TokenManager.js b/test/TokenManager.js index 18fd33de..5d9a4a8a 100644 --- a/test/TokenManager.js +++ b/test/TokenManager.js @@ -92,7 +92,7 @@ describe('Token Manager', () => { const proxyBytecodeHash = keccak256(proxyBytecode); const expected = { - london: '0x8080880884e00735cc1a34bdf5c1ea6c023db60a01cfa1e951ca41ecf5fd8836', + london: '0x3b336208cc75ca67bdd39bdeed72871ce795e6e9cd28e20f811599ea51973ebf', }[getEVMVersion()]; expect(proxyBytecodeHash).to.be.equal(expected); From 046e36292f8e31024d509b39e6ce3b1027a3610d Mon Sep 17 00:00:00 2001 From: Foivos Date: Fri, 20 Dec 2024 18:05:08 +0200 Subject: [PATCH 03/16] Update contracts/InterchainTokenService.sol Co-authored-by: Milap Sheth --- contracts/InterchainTokenService.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/InterchainTokenService.sol b/contracts/InterchainTokenService.sol index fff6d471..84053af1 100644 --- a/contracts/InterchainTokenService.sol +++ b/contracts/InterchainTokenService.sol @@ -396,7 +396,7 @@ contract InterchainTokenService is } /** - * @notice Executes operations based on the payload and selector. + * @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. From d0fa4623466395200853762f0c4d1e8b54e282b6 Mon Sep 17 00:00:00 2001 From: Foivos Date: Fri, 20 Dec 2024 18:09:03 +0200 Subject: [PATCH 04/16] Change an error a bit --- contracts/InterchainTokenFactory.sol | 3 ++- contracts/interfaces/IInterchainTokenFactory.sol | 2 +- test/InterchainTokenFactory.js | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/contracts/InterchainTokenFactory.sol b/contracts/InterchainTokenFactory.sol index 31b4422b..d21d9a84 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. @@ -150,7 +151,7 @@ contract InterchainTokenFactory is IInterchainTokenFactory, ITokenManagerType, M minterBytes = minter.toBytes(); } else { - revert EmptyInterchainToken(); + revert ZeroSupplyToken(); } tokenId = _deployInterchainToken(deploySalt, currentChain, name, symbol, decimals, minterBytes, gasValue); diff --git a/contracts/interfaces/IInterchainTokenFactory.sol b/contracts/interfaces/IInterchainTokenFactory.sol index 0db2aa76..87aa83ad 100644 --- a/contracts/interfaces/IInterchainTokenFactory.sol +++ b/contracts/interfaces/IInterchainTokenFactory.sol @@ -19,7 +19,7 @@ interface IInterchainTokenFactory is IUpgradable, IMulticall { error NotSupported(); error RemoteDeploymentNotApproved(); error InvalidTokenId(bytes32 tokenId, bytes32 expectedTokenId); - error EmptyInterchainToken(); + error ZeroSupplyToken(); /// @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/test/InterchainTokenFactory.js b/test/InterchainTokenFactory.js index 0e2cd5f3..af0728c3 100644 --- a/test/InterchainTokenFactory.js +++ b/test/InterchainTokenFactory.js @@ -239,7 +239,7 @@ describe('InterchainTokenFactory', () => { await expectRevert( (gasOptions) => tokenFactory.deployInterchainToken(salt, name, symbol, decimals, 0, AddressZero, { gasOptions }), tokenFactory, - 'EmptyInterchainToken', + 'ZeroSupplyToken', [], ); }); From 16088d8bf1a4ffbd2dec0bdcb452994293f7312d Mon Sep 17 00:00:00 2001 From: Foivos Date: Fri, 20 Dec 2024 18:12:49 +0200 Subject: [PATCH 05/16] address some comments --- contracts/interchain-token/InterchainToken.sol | 7 ++++++- package-lock.json | 4 ++-- test/InterchainToken.js | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/contracts/interchain-token/InterchainToken.sol b/contracts/interchain-token/InterchainToken.sol index 03265886..1dea362a 100644 --- a/contracts/interchain-token/InterchainToken.sol +++ b/contracts/interchain-token/InterchainToken.sol @@ -2,9 +2,12 @@ pragma solidity ^0.8.0; +import { AddressBytes } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/libs/AddressBytes.sol'; + import { IInterchainToken } from '../interfaces/IInterchainToken.sol'; import { InterchainTokenStandard } from './InterchainTokenStandard.sol'; +import { ERC20 } from './ERC20.sol'; import { ERC20Permit } from './ERC20Permit.sol'; import { Minter } from '../utils/Minter.sol'; @@ -13,7 +16,9 @@ import { Minter } from '../utils/Minter.sol'; * @notice This contract implements an interchain token which extends InterchainToken functionality. * @dev This contract also inherits Minter and Implementation logic. */ -contract InterchainToken is InterchainTokenStandard, ERC20Permit, Minter, IInterchainToken { +contract InterchainToken is InterchainTokenStandard, ERC20, ERC20Permit, Minter, IInterchainToken { + using AddressBytes for bytes; + string public name; string public symbol; uint8 public decimals; 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/InterchainToken.js b/test/InterchainToken.js index 300678c7..a7ff687a 100644 --- a/test/InterchainToken.js +++ b/test/InterchainToken.js @@ -147,7 +147,7 @@ describe('InterchainToken', () => { const contractBytecodeHash = keccak256(contractBytecode); const expected = { - london: '0x482146829055f052063003e9cf0ffaf798a12fb58088c2667566a135b9568355', + london: '0xa01cf28b0b6ce6dc3b466e995585d69486400d671fce0ea8d06beba583e6f3bb', }[getEVMVersion()]; expect(contractBytecodeHash).to.be.equal(expected); From 82babdcaacca2314e1539c71e8ab2ba0decd64b1 Mon Sep 17 00:00:00 2001 From: Foivos Date: Fri, 20 Dec 2024 18:14:33 +0200 Subject: [PATCH 06/16] revert changes to preserve bytecode --- contracts/interchain-token/ERC20Permit.sol | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/contracts/interchain-token/ERC20Permit.sol b/contracts/interchain-token/ERC20Permit.sol index 00826284..b692d723 100644 --- a/contracts/interchain-token/ERC20Permit.sol +++ b/contracts/interchain-token/ERC20Permit.sol @@ -23,7 +23,6 @@ abstract contract ERC20Permit is IERC20, IERC20Permit, ERC20 { */ bytes32 public nameHash; - // Prefix for the EIP712 structured data. string private constant EIP191_PREFIX_FOR_EIP712_STRUCTURED_DATA = '\x19\x01'; // keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)') @@ -72,15 +71,6 @@ abstract contract ERC20Permit is IERC20, IERC20Permit, ERC20 { function permit(address issuer, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external { if (block.timestamp > deadline) revert PermitExpired(); - // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature - // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines - // the valid range for s in (281): 0 < s < secp256k1n ÷ 2 + 1, and for v in (282): v ∈ {27, 28}. Most - // signatures from current libraries generate a unique signature with an s-value in the lower half order. - // - // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value - // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or - // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept - // these malleable signatures as well. if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) revert InvalidS(); if (v != 27 && v != 28) revert InvalidV(); From 3435efa4e8ef08d29474a32e3add80a8215cfd2d Mon Sep 17 00:00:00 2001 From: Foivos Date: Fri, 20 Dec 2024 18:18:18 +0200 Subject: [PATCH 07/16] try to preserve tokenManager bytecode --- contracts/interfaces/ITokenManager.sol | 1 - contracts/token-manager/TokenManager.sol | 8 +++----- test/TokenManager.js | 2 +- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/contracts/interfaces/ITokenManager.sol b/contracts/interfaces/ITokenManager.sol index 23c778b2..499ab09a 100644 --- a/contracts/interfaces/ITokenManager.sol +++ b/contracts/interfaces/ITokenManager.sol @@ -15,7 +15,6 @@ import { IFlowLimit } from './IFlowLimit.sol'; interface ITokenManager is IBaseTokenManager, IOperator, IFlowLimit, IImplementation { error TokenLinkerZeroAddress(); error NotService(address caller); - error ZeroTokenAddress(); error NotSupported(); /** diff --git a/contracts/token-manager/TokenManager.sol b/contracts/token-manager/TokenManager.sol index dbcc61db..2c553a0c 100644 --- a/contracts/token-manager/TokenManager.sol +++ b/contracts/token-manager/TokenManager.sol @@ -57,9 +57,9 @@ contract TokenManager is ITokenManager, Operator, FlowLimit, Implementation, Mul } /** - * @notice Reads the token address from the proxy. - * @dev This function is not supported when directly called on the implementation. - * It must be called by the proxy. It is included here so that the interace shows this function as existing, for better UX. + * @notice Reads the token address from the proxy. + * @dev This function is not supported when directly called on the implementation. It + * must be called by the proxy. * @return tokenAddress_ The address of the token. */ function tokenAddress() external view virtual returns (address) { @@ -69,7 +69,6 @@ contract TokenManager is ITokenManager, Operator, FlowLimit, Implementation, Mul /** * @notice A function that returns the token id. * @dev This will only work when implementation is called by a proxy, which stores the tokenId as an immutable. - * It is included here so that the interace shows this function as existing, for better UX. * @return bytes32 The interchain token ID. */ function interchainTokenId() public pure returns (bytes32) { @@ -91,7 +90,6 @@ contract TokenManager is ITokenManager, Operator, FlowLimit, Implementation, Mul */ function getTokenAddressFromParams(bytes calldata params_) external pure returns (address tokenAddress_) { (, tokenAddress_) = abi.decode(params_, (bytes, address)); - if (tokenAddress_ == address(0)) revert ZeroTokenAddress(); } /** diff --git a/test/TokenManager.js b/test/TokenManager.js index 5d9a4a8a..18fd33de 100644 --- a/test/TokenManager.js +++ b/test/TokenManager.js @@ -92,7 +92,7 @@ describe('Token Manager', () => { const proxyBytecodeHash = keccak256(proxyBytecode); const expected = { - london: '0x3b336208cc75ca67bdd39bdeed72871ce795e6e9cd28e20f811599ea51973ebf', + london: '0x8080880884e00735cc1a34bdf5c1ea6c023db60a01cfa1e951ca41ecf5fd8836', }[getEVMVersion()]; expect(proxyBytecodeHash).to.be.equal(expected); From bcef24d820d45bbb74068d5c11986ba596108f14 Mon Sep 17 00:00:00 2001 From: Foivos Date: Fri, 20 Dec 2024 18:20:19 +0200 Subject: [PATCH 08/16] trying to preserve bytecode some more --- contracts/interfaces/ITokenManager.sol | 8 +++++++- contracts/token-manager/TokenManager.sol | 3 +-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/contracts/interfaces/ITokenManager.sol b/contracts/interfaces/ITokenManager.sol index 499ab09a..53dc478c 100644 --- a/contracts/interfaces/ITokenManager.sol +++ b/contracts/interfaces/ITokenManager.sol @@ -14,7 +14,13 @@ import { IFlowLimit } from './IFlowLimit.sol'; */ interface ITokenManager is IBaseTokenManager, IOperator, IFlowLimit, IImplementation { error TokenLinkerZeroAddress(); - error NotService(address caller); + error NotService(address caller); + error TakeTokenFailed(); + error GiveTokenFailed(); + error NotToken(address caller); + error ZeroAddress(); + error AlreadyFlowLimiter(address flowLimiter); + error NotFlowLimiter(address flowLimiter); error NotSupported(); /** diff --git a/contracts/token-manager/TokenManager.sol b/contracts/token-manager/TokenManager.sol index 2c553a0c..e6d30174 100644 --- a/contracts/token-manager/TokenManager.sol +++ b/contracts/token-manager/TokenManager.sol @@ -18,7 +18,6 @@ import { FlowLimit } from '../utils/FlowLimit.sol'; /** * @title TokenManager * @notice This contract is responsible for managing tokens, such as setting locking token balances, or setting flow limits, for interchain transfers. - * @dev Should only be used as an implementation for TokenManagerProxy. */ contract TokenManager is ITokenManager, Operator, FlowLimit, Implementation, Multicall { using AddressBytes for bytes; @@ -57,7 +56,7 @@ contract TokenManager is ITokenManager, Operator, FlowLimit, Implementation, Mul } /** - * @notice Reads the token address from the proxy. + * @notice Reads the token address from the proxy. * @dev This function is not supported when directly called on the implementation. It * must be called by the proxy. * @return tokenAddress_ The address of the token. From 426804b9ab51b3cd541dc347e217c8770666b117 Mon Sep 17 00:00:00 2001 From: Foivos Date: Fri, 20 Dec 2024 18:21:25 +0200 Subject: [PATCH 09/16] some more trying to preserve bytecode --- contracts/interfaces/ITokenManager.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/contracts/interfaces/ITokenManager.sol b/contracts/interfaces/ITokenManager.sol index 53dc478c..50c582a9 100644 --- a/contracts/interfaces/ITokenManager.sol +++ b/contracts/interfaces/ITokenManager.sol @@ -14,7 +14,7 @@ import { IFlowLimit } from './IFlowLimit.sol'; */ interface ITokenManager is IBaseTokenManager, IOperator, IFlowLimit, IImplementation { error TokenLinkerZeroAddress(); - error NotService(address caller); + error NotService(address caller); error TakeTokenFailed(); error GiveTokenFailed(); error NotToken(address caller); @@ -25,7 +25,6 @@ interface ITokenManager is IBaseTokenManager, IOperator, IFlowLimit, IImplementa /** * @notice Returns implementation type of this token manager. - * @dev This is stored in the proxy and should not be called in the implementation, but it is included here so that the interface properly tells us what functions exist. * @return uint256 The implementation type of this token manager. */ function implementationType() external view returns (uint256); From ab264ac1d989bff97490213e42b4b96763fb67ef Mon Sep 17 00:00:00 2001 From: Foivos Date: Fri, 20 Dec 2024 18:23:14 +0200 Subject: [PATCH 10/16] finaly fixed the tokenManager bytecode --- contracts/interfaces/IBaseTokenManager.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/contracts/interfaces/IBaseTokenManager.sol b/contracts/interfaces/IBaseTokenManager.sol index b3dba446..2ceff77d 100644 --- a/contracts/interfaces/IBaseTokenManager.sol +++ b/contracts/interfaces/IBaseTokenManager.sol @@ -9,14 +9,12 @@ pragma solidity ^0.8.0; interface IBaseTokenManager { /** * @notice A function that returns the token id. - * @dev This is stored in the proxy and should not be called in the implementation, but it is included here so that the interface properly tells us what functions exist. */ function interchainTokenId() external view returns (bytes32); /** * @notice A function that should return the address of the token. * Must be overridden in the inheriting contract. - * @dev This is stored in the proxy and should not be called in the implementation, but it is included here so that the interface properly tells us what functions exist. * @return address address of the token. */ function tokenAddress() external view returns (address); From 862eeb407266703ff1555ed8bd3601ce0106c9f3 Mon Sep 17 00:00:00 2001 From: Foivos Date: Fri, 20 Dec 2024 18:28:24 +0200 Subject: [PATCH 11/16] fixed a test and a test name --- test/InterchainTokenFactory.js | 2 +- test/InterchainTokenService.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/InterchainTokenFactory.js b/test/InterchainTokenFactory.js index af0728c3..be4f62a0 100644 --- a/test/InterchainTokenFactory.js +++ b/test/InterchainTokenFactory.js @@ -232,7 +232,7 @@ 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); diff --git a/test/InterchainTokenService.js b/test/InterchainTokenService.js index 2dc561c3..2cb75946 100644 --- a/test/InterchainTokenService.js +++ b/test/InterchainTokenService.js @@ -909,13 +909,13 @@ describe('Interchain Token Service', () => { ); }); - it('Should revert on deploying a token manager with an empty token', async () => { + it('Should revert on deploying a token manager if token handler post deploy fails', async () => { const params = defaultAbiCoder.encode(['bytes', 'address'], [wallet.address, AddressZero]); await expectRevert( (gasOptions) => service.deployTokenManager(salt, '', LOCK_UNLOCK, params, 0, gasOptions), service, - 'TokenManagerDeploymentFailed', + 'PostDeployFailed', ); }); From 5bfbc662606776a8f7b68c896d85dffd3d2b1dca Mon Sep 17 00:00:00 2001 From: Foivos Date: Mon, 23 Dec 2024 13:18:01 +0200 Subject: [PATCH 12/16] Added a check for the token to exist --- contracts/InterchainTokenFactory.sol | 14 ++++++++++++++ contracts/interfaces/IInterchainTokenFactory.sol | 1 + test/InterchainTokenFactory.js | 4 ++++ 3 files changed, 19 insertions(+) diff --git a/contracts/InterchainTokenFactory.sol b/contracts/InterchainTokenFactory.sol index d21d9a84..93aae451 100644 --- a/contracts/InterchainTokenFactory.sol +++ b/contracts/InterchainTokenFactory.sol @@ -404,10 +404,24 @@ contract InterchainTokenFactory is IInterchainTokenFactory, ITokenManagerType, M bytes32 deploySalt = canonicalInterchainTokenDeploySalt(tokenAddress); string memory currentChain = ''; uint256 gasValue = 0; + _checkToken(tokenAddress); tokenId = interchainTokenService.deployTokenManager(deploySalt, currentChain, TokenManagerType.LOCK_UNLOCK, params, gasValue); } + function _checkToken(address tokenAddress) internal view { + IERC20Named token = IERC20Named(tokenAddress); + try token.name() {} catch { + revert NotToken(tokenAddress); + } + try token.symbol() {} catch { + revert NotToken(tokenAddress); + } + try token.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/interfaces/IInterchainTokenFactory.sol b/contracts/interfaces/IInterchainTokenFactory.sol index 87aa83ad..fd63f3c7 100644 --- a/contracts/interfaces/IInterchainTokenFactory.sol +++ b/contracts/interfaces/IInterchainTokenFactory.sol @@ -20,6 +20,7 @@ interface IInterchainTokenFactory is IUpgradable, IMulticall { 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/test/InterchainTokenFactory.js b/test/InterchainTokenFactory.js index be4f62a0..b5c6d33c 100644 --- a/test/InterchainTokenFactory.js +++ b/test/InterchainTokenFactory.js @@ -115,6 +115,10 @@ 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( From 55b731357261aa17fe35e1bf4837fab548836fd4 Mon Sep 17 00:00:00 2001 From: Foivos Date: Mon, 23 Dec 2024 16:10:51 +0200 Subject: [PATCH 13/16] made lint and slither happy --- contracts/InterchainTokenFactory.sol | 6 ++++++ contracts/TokenHandler.sol | 2 +- test/InterchainTokenFactory.js | 7 ++++++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/contracts/InterchainTokenFactory.sol b/contracts/InterchainTokenFactory.sol index 93aae451..31031ecb 100644 --- a/contracts/InterchainTokenFactory.sol +++ b/contracts/InterchainTokenFactory.sol @@ -411,12 +411,18 @@ contract InterchainTokenFactory is IInterchainTokenFactory, ITokenManagerType, M function _checkToken(address tokenAddress) internal view { IERC20Named token = IERC20Named(tokenAddress); + + // slither-disable-next-line unused-return try token.name() {} catch { revert NotToken(tokenAddress); } + + // slither-disable-next-line unused-return try token.symbol() {} catch { revert NotToken(tokenAddress); } + + // slither-disable-next-line unused-return try token.decimals() {} catch { revert NotToken(tokenAddress); } diff --git a/contracts/TokenHandler.sol b/contracts/TokenHandler.sol index 69932021..8db4492d 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'; diff --git a/test/InterchainTokenFactory.js b/test/InterchainTokenFactory.js index b5c6d33c..1bc83ca2 100644 --- a/test/InterchainTokenFactory.js +++ b/test/InterchainTokenFactory.js @@ -116,7 +116,12 @@ describe('InterchainTokenFactory', () => { }); it('Should not register a non-existing token', async () => { - await expectRevert((gasOptions) => tokenFactory.registerCanonicalInterchainToken(tokenFactory.address, { gasOptions }), tokenFactory, "NotToken", [tokenFactory.address]) + 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 () => { From a56fd79f0e507c0803d59062ac5b7f83954f0722 Mon Sep 17 00:00:00 2001 From: Foivos Date: Mon, 23 Dec 2024 17:12:52 +0200 Subject: [PATCH 14/16] prettier --- contracts/InterchainTokenFactory.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/InterchainTokenFactory.sol b/contracts/InterchainTokenFactory.sol index 31031ecb..c1b016b7 100644 --- a/contracts/InterchainTokenFactory.sol +++ b/contracts/InterchainTokenFactory.sol @@ -411,7 +411,7 @@ contract InterchainTokenFactory is IInterchainTokenFactory, ITokenManagerType, M function _checkToken(address tokenAddress) internal view { IERC20Named token = IERC20Named(tokenAddress); - + // slither-disable-next-line unused-return try token.name() {} catch { revert NotToken(tokenAddress); From 24e5797c46a4dcd44bb4136581d9af9507a4c69b Mon Sep 17 00:00:00 2001 From: Foivos Date: Thu, 2 Jan 2025 13:38:07 +0200 Subject: [PATCH 15/16] using a function to get token metadata now --- contracts/InterchainTokenFactory.sol | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/contracts/InterchainTokenFactory.sol b/contracts/InterchainTokenFactory.sol index c1b016b7..a532eb47 100644 --- a/contracts/InterchainTokenFactory.sol +++ b/contracts/InterchainTokenFactory.sol @@ -385,11 +385,15 @@ contract InterchainTokenFactory is IInterchainTokenFactory, ITokenManagerType, M uint256 gasValue ) internal returns (bytes32 tokenId) { bytes32 expectedTokenId = _interchainTokenId(deploySalt); + // Ensure that a local token has been registered for the tokenId - IERC20Named token = IERC20Named(interchainTokenService.registeredTokenAddress(expectedTokenId)); + string memory name; + string memory symbol; + uint8 decimals; + (name, symbol, decimals) = _getTokenMetadata(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); + tokenId = _deployInterchainToken(deploySalt, destinationChain, name, symbol, decimals, minter, gasValue); if (tokenId != expectedTokenId) revert InvalidTokenId(tokenId, expectedTokenId); } @@ -404,26 +408,30 @@ contract InterchainTokenFactory is IInterchainTokenFactory, ITokenManagerType, M bytes32 deploySalt = canonicalInterchainTokenDeploySalt(tokenAddress); string memory currentChain = ''; uint256 gasValue = 0; - _checkToken(tokenAddress); + // slither-disable-next-line unused-return + _getTokenMetadata(tokenAddress); tokenId = interchainTokenService.deployTokenManager(deploySalt, currentChain, TokenManagerType.LOCK_UNLOCK, params, gasValue); } - function _checkToken(address tokenAddress) internal view { + function _getTokenMetadata(address tokenAddress) internal view returns (string memory name, string memory symbol, uint8 decimals) { IERC20Named token = IERC20Named(tokenAddress); - // slither-disable-next-line unused-return - try token.name() {} catch { + try token.name() returns (string memory name_) { + name = name_; + } catch { revert NotToken(tokenAddress); } - // slither-disable-next-line unused-return - try token.symbol() {} catch { + try token.symbol() returns (string memory symbol_) { + symbol = symbol_; + } catch { revert NotToken(tokenAddress); } - // slither-disable-next-line unused-return - try token.decimals() {} catch { + try token.decimals() returns (uint8 decimals_) { + decimals = decimals_; + } catch { revert NotToken(tokenAddress); } } From eb4c928e1b7d360925cef8dde6702105897b0b1d Mon Sep 17 00:00:00 2001 From: Milap Sheth Date: Mon, 6 Jan 2025 10:41:39 -0500 Subject: [PATCH 16/16] cleanup comments --- contracts/InterchainTokenFactory.sol | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/contracts/InterchainTokenFactory.sol b/contracts/InterchainTokenFactory.sol index a532eb47..51c0e439 100644 --- a/contracts/InterchainTokenFactory.sol +++ b/contracts/InterchainTokenFactory.sol @@ -384,15 +384,13 @@ 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 - string memory name; - string memory symbol; - uint8 decimals; - (name, symbol, decimals) = _getTokenMetadata(interchainTokenService.registeredTokenAddress(expectedTokenId)); + address tokenAddress = interchainTokenService.registeredTokenAddress(expectedTokenId); // The local token must expose the name, symbol, and decimals metadata + (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); } @@ -408,12 +406,21 @@ contract InterchainTokenFactory is IInterchainTokenFactory, ITokenManagerType, M bytes32 deploySalt = canonicalInterchainTokenDeploySalt(tokenAddress); 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);