From 796c27a82f7921b9c8222dc7be933529e8baaec3 Mon Sep 17 00:00:00 2001 From: Foivos Date: Wed, 15 Jan 2025 18:25:11 +0200 Subject: [PATCH] feat(its): add a registerCustomToken function in ITS and add support for it in factory (#322) Co-authored-by: Milap Sheth --- contracts/InterchainTokenFactory.sol | 14 +- contracts/InterchainTokenService.sol | 109 ++-- .../interfaces/IInterchainTokenService.sol | 14 + scripts/deploy.js | 1 + test/AddressDerivation.js | 16 - test/InterchainTokenFactory.js | 206 ++++++- test/InterchainTokenService.js | 534 +++++------------- test/InterchainTokenServiceFullFlow.js | 24 +- test/InterchainTokenServiceUpgradeFlow.js | 24 +- 9 files changed, 442 insertions(+), 500 deletions(-) diff --git a/contracts/InterchainTokenFactory.sol b/contracts/InterchainTokenFactory.sol index 535b9f25..19ec1e13 100644 --- a/contracts/InterchainTokenFactory.sol +++ b/contracts/InterchainTokenFactory.sol @@ -403,23 +403,14 @@ contract InterchainTokenFactory is IInterchainTokenFactory, Multicall, Upgradabl */ function registerCanonicalInterchainToken(address tokenAddress) external payable returns (bytes32 tokenId) { bytes32 deploySalt = canonicalInterchainTokenDeploySalt(tokenAddress); - string memory currentChain = ''; // No custom operator is set for canonical token registration bytes memory linkParams = ''; - uint256 gasValue = 0; // Ensure that the ERC20 token has metadata before registering it // slither-disable-next-line unused-return _getTokenMetadata(tokenAddress); - tokenId = interchainTokenService.linkToken( - deploySalt, - currentChain, - tokenAddress.toBytes(), - TokenManagerType.LOCK_UNLOCK, - linkParams, - gasValue - ); + tokenId = interchainTokenService.registerCustomToken(deploySalt, tokenAddress, TokenManagerType.LOCK_UNLOCK, linkParams); } /** @@ -530,13 +521,12 @@ contract InterchainTokenFactory is IInterchainTokenFactory, Multicall, Upgradabl uint256 gasValue ) external payable returns (bytes32 tokenId) { bytes32 deploySalt = linkedTokenDeploySalt(msg.sender, salt); - string memory currentChain = ''; bytes memory linkParams = ''; if (operator != address(0)) { linkParams = operator.toBytes(); } - tokenId = interchainTokenService.linkToken(deploySalt, currentChain, tokenAddress.toBytes(), tokenManagerType, linkParams, 0); + tokenId = interchainTokenService.registerCustomToken(deploySalt, tokenAddress, tokenManagerType, linkParams); interchainTokenService.registerTokenMetadata{ value: gasValue }(tokenAddress, gasValue); } diff --git a/contracts/InterchainTokenService.sol b/contracts/InterchainTokenService.sol index d6ad8d39..d950b3bc 100644 --- a/contracts/InterchainTokenService.sol +++ b/contracts/InterchainTokenService.sol @@ -175,6 +175,15 @@ contract InterchainTokenService is _; } + /** + * @notice This modifier is used to ensure that only a the token factory can call a function. + */ + modifier onlyTokenFactory() { + if (msg.sender != interchainTokenFactory) revert NotInterchainTokenFactory(msg.sender); + + _; + } + /*****\ GETTERS \*****/ @@ -279,6 +288,30 @@ contract InterchainTokenService is ); } + /** + * @notice Only to be used by the InterchainTokenFactory to register custom tokens to this chain. Then link token can be used to register those tokens to other chains. + * @param salt A unique salt to derive tokenId from. + * @param tokenManagerType The type of the token manager to use for the token registration. + * @param linkParams The operator for the token. + */ + function registerCustomToken( + bytes32 salt, + address tokenAddress, + TokenManagerType tokenManagerType, + bytes calldata linkParams + ) external payable whenNotPaused onlyTokenFactory returns (bytes32 tokenId) { + // Custom token managers can't be deployed with native interchain token type, which is reserved for interchain tokens + if (tokenManagerType == TokenManagerType.NATIVE_INTERCHAIN_TOKEN) revert CannotDeploy(tokenManagerType); + + address deployer = TOKEN_FACTORY_DEPLOYER; + + tokenId = interchainTokenId(deployer, salt); + + emit InterchainTokenIdClaimed(tokenId, deployer, salt); + + _deployTokenManager(tokenId, tokenManagerType, tokenAddress, linkParams); + } + /** * @notice If `destinationChain` is an empty string, this function will register the token address on the current chain. * Otherwise, it will link the token address on the destination chain with the token corresponding to the tokenId on the current chain. @@ -305,27 +338,38 @@ contract InterchainTokenService is // Custom token managers can't be deployed with native interchain token type, which is reserved for interchain tokens if (tokenManagerType == TokenManagerType.NATIVE_INTERCHAIN_TOKEN) revert CannotDeploy(tokenManagerType); + // Cannot deploy to this chain using linkToken anymore + if (bytes(destinationChain).length == 0) { + revert NotSupported(); + } + + // Cannot deploy to this chain using linkToken anymore + if (chainNameHash == keccak256(bytes(destinationChain))) revert CannotDeployRemotelyToSelf(); + address deployer = msg.sender; if (deployer == interchainTokenFactory) { deployer = TOKEN_FACTORY_DEPLOYER; - } else if (bytes(destinationChain).length == 0) { - // TODO: Only support linking new tokens via ITS factory, to include chain name in token id derivation - // Custom token usage needs to be moved to ITS factory tests - // revert NotSupported(); } tokenId = interchainTokenId(deployer, salt); emit InterchainTokenIdClaimed(tokenId, deployer, salt); - if (bytes(destinationChain).length == 0) { - _deployTokenManager(tokenId, tokenManagerType, destinationTokenAddress.toAddress(), linkParams); - } else { - if (chainNameHash == keccak256(bytes(destinationChain))) revert CannotDeployRemotelyToSelf(); + bytes memory sourceTokenAddress = registeredTokenAddress(tokenId).toBytes(); - _linkToken(tokenId, destinationChain, destinationTokenAddress, tokenManagerType, linkParams, gasValue); - } + emit LinkTokenStarted(tokenId, destinationChain, sourceTokenAddress, destinationTokenAddress, tokenManagerType, linkParams); + + bytes memory payload = abi.encode( + MESSAGE_TYPE_LINK_TOKEN, + tokenId, + tokenManagerType, + sourceTokenAddress, + destinationTokenAddress, + linkParams + ); + + _routeMessage(destinationChain, payload, IGatewayCaller.MetadataVersion.CONTRACT_CALL, gasValue); } /** @@ -352,15 +396,8 @@ contract InterchainTokenService is uint8 decimals, bytes memory minter, uint256 gasValue - ) external payable whenNotPaused returns (bytes32 tokenId) { - address deployer = msg.sender; - - if (deployer == interchainTokenFactory) { - deployer = TOKEN_FACTORY_DEPLOYER; - } else if (trustedAddressHash(chainName()) == ITS_HUB_ROUTING_IDENTIFIER_HASH) { - // Currently, deployments directly on ITS contract (instead of ITS Factory) are restricted for ITS contracts deployed on Amplifier, i.e registered with the Hub - revert NotSupported(); - } + ) external payable whenNotPaused onlyTokenFactory returns (bytes32 tokenId) { + address deployer = TOKEN_FACTORY_DEPLOYER; tokenId = interchainTokenId(deployer, salt); @@ -863,40 +900,6 @@ contract InterchainTokenService is return (messageType, originalSourceChain, payload); } - /** - * @notice Deploys a token manager on a destination chain. - * @param tokenId The ID of the token. - * @param destinationChain The chain where the token manager will be deployed. - * @param destinationTokenAddress The address of the token on the destination chain. - * @param tokenManagerType The type of token manager to be deployed. - * @param params Additional parameters for the token linking. - * @param gasValue The amount of gas to be paid for the transaction. - */ - function _linkToken( - bytes32 tokenId, - string calldata destinationChain, - bytes calldata destinationTokenAddress, - TokenManagerType tokenManagerType, - bytes calldata params, - uint256 gasValue - ) internal { - // slither-disable-next-line unused-return - bytes memory sourceTokenAddress = registeredTokenAddress(tokenId).toBytes(); - - emit LinkTokenStarted(tokenId, destinationChain, sourceTokenAddress, destinationTokenAddress, tokenManagerType, params); - - bytes memory payload = abi.encode( - MESSAGE_TYPE_LINK_TOKEN, - tokenId, - tokenManagerType, - sourceTokenAddress, - destinationTokenAddress, - params - ); - - _routeMessage(destinationChain, payload, IGatewayCaller.MetadataVersion.CONTRACT_CALL, gasValue); - } - /** * @notice Deploys an interchain token on a destination chain. * @param tokenId The ID of the token. diff --git a/contracts/interfaces/IInterchainTokenService.sol b/contracts/interfaces/IInterchainTokenService.sol index 0eaaec1e..916c69c1 100644 --- a/contracts/interfaces/IInterchainTokenService.sol +++ b/contracts/interfaces/IInterchainTokenService.sol @@ -55,6 +55,7 @@ interface IInterchainTokenService is error EmptyDestinationAddress(); error EmptyTokenAddress(); error NotSupported(); + error NotInterchainTokenFactory(address sender); event InterchainTransfer( bytes32 indexed tokenId, @@ -181,6 +182,19 @@ interface IInterchainTokenService is */ function registerTokenMetadata(address tokenAddress, uint256 gasValue) external payable; + /** + * @notice Only to be used by the InterchainTokenFactory to register custom tokens to this chain. Then link token can be used to register those tokens to other chains. + * @param salt A unique salt to derive tokenId from. + * @param tokenManagerType The type of the token manager to use for the token registration. + * @param linkParams The operator for the token. + */ + function registerCustomToken( + bytes32 salt, + address tokenAddress, + TokenManagerType tokenManagerType, + bytes calldata linkParams + ) external payable returns (bytes32 tokenId); + /** * @notice If `destinationChain` is an empty string, this function will register the token address on the current chain. * Otherwise, it will link the token address on the destination chain with the token corresponding to the tokenId on the current chain. diff --git a/scripts/deploy.js b/scripts/deploy.js index aa29904c..7cb1623f 100644 --- a/scripts/deploy.js +++ b/scripts/deploy.js @@ -142,5 +142,6 @@ module.exports = { deployMockGateway, deployGasService, deployInterchainTokenService, + deployInterchainTokenFactory, deployAll, }; diff --git a/test/AddressDerivation.js b/test/AddressDerivation.js index c4d64b13..d0a169d8 100644 --- a/test/AddressDerivation.js +++ b/test/AddressDerivation.js @@ -47,22 +47,6 @@ if (isHardhat) { }); describe('Interchain Token Service Deployments', () => { - it('Should derive the correct token address for interchain token deployment on source chain', async () => { - const salt = getSaltFromKey('deployInterchainToken'); - const tokenId = await service.interchainTokenId(wallet.address, salt); - - const expectedTokenAddress = '0x2b7c2c1f7297BB9a573Fb970D086F0d113722Ceb'; - const expectedTokenManagerAddress = '0x1248d7831b5B231147bfbDb5e2b29B0110EeC9C8'; - - const params = defaultAbiCoder.encode(['bytes', 'address'], [wallet.address, expectedTokenAddress]); - - await expect(service.deployInterchainToken(salt, '', tokenName, tokenSymbol, tokenDecimals, wallet.address, 0)) - .to.emit(service, 'InterchainTokenDeployed') - .withArgs(tokenId, expectedTokenAddress, wallet.address, tokenName, tokenSymbol, tokenDecimals) - .to.emit(service, 'TokenManagerDeployed') - .withArgs(tokenId, expectedTokenManagerAddress, NATIVE_INTERCHAIN_TOKEN, params); - }); - it('Should derive the correct token address for remote interchain token deployment', async () => { const salt = getSaltFromKey('deployRemoteInterchainToken'); const tokenId = await service.interchainTokenId(wallet.address, salt); diff --git a/test/InterchainTokenFactory.js b/test/InterchainTokenFactory.js index db16746f..a27c5325 100644 --- a/test/InterchainTokenFactory.js +++ b/test/InterchainTokenFactory.js @@ -13,6 +13,7 @@ const { deployAll, deployContract } = require('../scripts/deploy'); const { getRandomBytes32, expectRevert, gasReporter } = require('./utils'); const { MESSAGE_TYPE_DEPLOY_INTERCHAIN_TOKEN, + MESSAGE_TYPE_LINK_TOKEN, NATIVE_INTERCHAIN_TOKEN, LOCK_UNLOCK, MINTER_ROLE, @@ -40,6 +41,8 @@ describe('InterchainTokenFactory', () => { before(async () => { [wallet, otherWallet] = await ethers.getSigners(); ({ service, gateway, gasService, tokenFactory } = await deployAll(wallet, chainName, [destinationChain])); + + await service.setTrustedAddress(ITS_HUB_CHAIN_NAME, ITS_HUB_ADDRESS).then((tx) => tx.wait); }); describe('Token Factory Deployment', async () => { @@ -625,6 +628,74 @@ describe('InterchainTokenFactory', () => { .withArgs(service.address, destinationChain, service.address, keccak256(payload), payload); }); + it('Should revert when deploying a remote interchain token to self', async () => { + const tokenName = 'Token Name'; + const tokenSymbol = 'TN'; + const tokenDecimals = 13; + const salt = getRandomBytes32(); + + await tokenFactory.deployInterchainToken(salt, tokenName, tokenSymbol, tokenDecimals, 0, wallet.address).then((tx) => tx.wait); + + await expectRevert( + (gasOptions) => + tokenFactory['deployRemoteInterchainToken(bytes32,address,string,uint256)']( + salt, + wallet.address, + chainName, + 0, + gasOptions, + ), + service, + 'CannotDeployRemotelyToSelf', + ); + }); + + it('Should revert on remote interchain token deployment with invalid token symbol', async () => { + const salt = getRandomBytes32(); + const tokenName = 'name'; + const tokenDecimals = 9; + + await expectRevert( + (gasOptions) => tokenFactory.deployInterchainToken(salt, tokenName, '', tokenDecimals, 0, minter, gasOptions), + service, + 'EmptyTokenSymbol', + ); + }); + + it('Should revert on remote interchain token deployment if destination chain is not trusted', async () => { + const tokenName = 'Token Name'; + const tokenSymbol = 'TN'; + const tokenDecimals = 13; + const salt = getRandomBytes32(); + + await tokenFactory.deployInterchainToken(salt, tokenName, tokenSymbol, tokenDecimals, 0, wallet.address).then((tx) => tx.wait); + + await expectRevert( + (gasOptions) => + tokenFactory['deployRemoteInterchainToken(bytes32,address,string,uint256)']( + salt, + wallet.address, + 'untrusted chain', + 0, + gasOptions, + ), + service, + 'UntrustedChain', + ); + }); + + it('Should revert on remote interchain token deployment with invalid token name', async () => { + const salt = getRandomBytes32(); + const tokenSymbol = 'symbol'; + const tokenDecimals = 9; + + await expectRevert( + (gasOptions) => tokenFactory.deployInterchainToken(salt, '', tokenSymbol, tokenDecimals, 0, minter, gasOptions), + service, + 'EmptyTokenName', + ); + }); + describe('Custom Token Manager Deployment', () => { const tokenName = 'Token Name'; const tokenSymbol = 'TN'; @@ -647,20 +718,13 @@ describe('InterchainTokenFactory', () => { }); it('Should revert on deploying an invalid token manager', async () => { - await expectRevert((gasOptions) => tokenFactory.linkToken(salt, '', token.address, 6, wallet.address, 0, gasOptions)); - }); - - it('Should revert on deploying a local token manager with invalid params', async () => { - await expectRevert( - (gasOptions) => tokenFactory.linkToken(salt, '', token.address, NATIVE_INTERCHAIN_TOKEN, '0x', 0, gasOptions), - service, - 'CannotDeploy', - ); + await expectRevert((gasOptions) => tokenFactory.registerCustomToken(salt, token.address, 6, wallet.address, 0, gasOptions)); }); it('Should revert on deploying a local token manager with interchain token manager type', async () => { await expectRevert( - (gasOptions) => tokenFactory.linkToken(salt, '', token.address, NATIVE_INTERCHAIN_TOKEN, wallet.address, 0, gasOptions), + (gasOptions) => + tokenFactory.registerCustomToken(salt, token.address, NATIVE_INTERCHAIN_TOKEN, wallet.address, 0, gasOptions), service, 'CannotDeploy', [NATIVE_INTERCHAIN_TOKEN], @@ -687,7 +751,7 @@ describe('InterchainTokenFactory', () => { it('Should revert on deploying a token manager if token handler post deploy fails', async () => { await expectRevert( - (gasOptions) => tokenFactory.linkToken(salt, '', AddressZero, LOCK_UNLOCK, wallet.address, 0, gasOptions), + (gasOptions) => tokenFactory.registerCustomToken(salt, AddressZero, LOCK_UNLOCK, wallet.address, 0, gasOptions), service, 'PostDeployFailed', ); @@ -701,8 +765,6 @@ describe('InterchainTokenFactory', () => { const gasValue = 1; const params = defaultAbiCoder.encode(['bytes', 'address'], ['0x', token.address]); - await service.setTrustedAddress(ITS_HUB_CHAIN_NAME, ITS_HUB_ADDRESS).then((tx) => tx.wait); - await expect(tokenFactory.registerCustomToken(salt, token.address, LOCK_UNLOCK, AddressZero, gasValue, { value: gasValue })) .to.emit(service, 'InterchainTokenIdClaimed') .withArgs(tokenId, AddressZero, deploySalt) @@ -729,14 +791,14 @@ describe('InterchainTokenFactory', () => { .withArgs(tokenId, tokenManagerAddress, LOCK_UNLOCK, params); }); - it('Should link token with lock_unlock type', async () => { + it('Should register a token with lock_unlock type', async () => { const tokenManagerAddress = await service.tokenManagerAddress(tokenId); const params = defaultAbiCoder.encode(['bytes', 'address'], [wallet.address, token.address]); const gasValue = 1; await expect( reportGas( - tokenFactory.linkToken(salt, '', token.address, LOCK_UNLOCK, wallet.address, gasValue, { value: gasValue }), + tokenFactory.registerCustomToken(salt, token.address, LOCK_UNLOCK, wallet.address, gasValue, { value: gasValue }), 'Call deployTokenManager on source chain', ), ) @@ -766,7 +828,7 @@ describe('InterchainTokenFactory', () => { it('Should revert when linking a token twice', async () => { const revertData = keccak256(toUtf8Bytes('AlreadyDeployed()')).substring(0, 10); await expectRevert( - (gasOptions) => tokenFactory.linkToken(salt, '', token.address, LOCK_UNLOCK, wallet.address, 0, gasOptions), + (gasOptions) => tokenFactory.registerCustomToken(salt, token.address, LOCK_UNLOCK, wallet.address, 0, gasOptions), service, 'TokenManagerDeploymentFailed', [revertData], @@ -789,7 +851,7 @@ describe('InterchainTokenFactory', () => { ); }); - it('Should link token with mint_burn type', async () => { + it('Should register a token with mint_burn type', async () => { const salt = getRandomBytes32(); const tokenId = await tokenFactory.linkedTokenId(wallet.address, salt); const tokenManagerAddress = await service.tokenManagerAddress(tokenId); @@ -802,7 +864,7 @@ describe('InterchainTokenFactory', () => { ]); const params = defaultAbiCoder.encode(['bytes', 'address'], [wallet.address, token.address]); - const tx = tokenFactory.linkToken(salt, '', token.address, MINT_BURN, wallet.address, 0); + const tx = tokenFactory.registerCustomToken(salt, token.address, MINT_BURN, wallet.address, 0); const expectedTokenManagerAddress = await service.tokenManagerAddress(tokenId); await expect(tx).to.emit(service, 'TokenManagerDeployed').withArgs(tokenId, expectedTokenManagerAddress, MINT_BURN, params); @@ -824,7 +886,7 @@ describe('InterchainTokenFactory', () => { expect(tokenAddressFromProxy).to.eq(token.address); }); - it('Should link token with mint_burn_from type', async () => { + it('Should register a token with mint_burn_from type', async () => { const salt = getRandomBytes32(); const tokenId = await tokenFactory.linkedTokenId(wallet.address, salt); const tokenManagerAddress = await service.tokenManagerAddress(tokenId); @@ -837,7 +899,7 @@ describe('InterchainTokenFactory', () => { ]); const params = defaultAbiCoder.encode(['bytes', 'address'], [wallet.address, token.address]); - const tx = tokenFactory.linkToken(salt, '', token.address, MINT_BURN_FROM, wallet.address, 0); + const tx = tokenFactory.registerCustomToken(salt, token.address, MINT_BURN_FROM, wallet.address, 0); const expectedTokenManagerAddress = await service.tokenManagerAddress(tokenId); await expect(tx) .to.emit(service, 'TokenManagerDeployed') @@ -861,7 +923,7 @@ describe('InterchainTokenFactory', () => { expect(tokenAddressFromProxy).to.eq(token.address); }); - it('Should link token with lock_unlock_with_fee type', async () => { + it('Should register a token with lock_unlock_with_fee type', async () => { const salt = getRandomBytes32(); const tokenId = await tokenFactory.linkedTokenId(wallet.address, salt); const tokenManagerAddress = await service.tokenManagerAddress(tokenId); @@ -874,7 +936,7 @@ describe('InterchainTokenFactory', () => { ]); const params = defaultAbiCoder.encode(['bytes', 'address'], [wallet.address, token.address]); - const tx = tokenFactory.linkToken(salt, '', token.address, LOCK_UNLOCK_FEE_ON_TRANSFER, wallet.address, 0); + const tx = tokenFactory.registerCustomToken(salt, token.address, LOCK_UNLOCK_FEE_ON_TRANSFER, wallet.address, 0); const expectedTokenManagerAddress = await service.tokenManagerAddress(tokenId); await expect(tx) .to.emit(service, 'TokenManagerDeployed') @@ -898,7 +960,7 @@ describe('InterchainTokenFactory', () => { expect(tokenAddressFromProxy).to.eq(token.address); }); - it('Should revert on linking a token if ITS is paused', async () => { + it('Should revert on registering a token if ITS is paused', async () => { await service.setPauseStatus(true).then((tx) => tx.wait); await expectRevert( @@ -910,5 +972,103 @@ describe('InterchainTokenFactory', () => { await service.setPauseStatus(false).then((tx) => tx.wait); }); }); + + describe('Initialize remote custom token manager deployment', () => { + let token, tokenId, salt; + const tokenManagerType = LOCK_UNLOCK; + const operator = AddressZero; + const gasValue = 5678; + + async function deployAndRegisterToken() { + salt = getRandomBytes32(); + + token = await deployContract(wallet, 'TestInterchainTokenStandard', [ + name, + symbol, + decimals, + service.address, + getRandomBytes32(), + ]); + + tokenId = await tokenFactory.linkedTokenId(wallet.address, salt); + await tokenFactory + .registerCustomToken(salt, token.address, tokenManagerType, operator, gasValue, { value: gasValue }) + .then((tx) => tx.wait); + await token.setTokenId(tokenId).then((tx) => tx.wait); + } + + it('Should initialize a remote custom token manager deployment', async () => { + await deployAndRegisterToken(); + + const remoteTokenAddress = '0x1234'; + const minter = '0x5789'; + const type = LOCK_UNLOCK; + const payload = defaultAbiCoder.encode( + ['uint256', 'bytes32', 'uint256', 'bytes', 'bytes', 'bytes'], + [MESSAGE_TYPE_LINK_TOKEN, tokenId, type, token.address, remoteTokenAddress, minter], + ); + + const tokenManager = await getContractAt('TokenManager', await service.deployedTokenManager(tokenId), wallet); + expect(await tokenManager.isOperator(AddressZero)).to.be.true; + expect(await tokenManager.isOperator(service.address)).to.be.true; + expect(await tokenManager.isFlowLimiter(AddressZero)).to.be.true; + expect(await tokenManager.isFlowLimiter(service.address)).to.be.true; + + await expect( + reportGas( + tokenFactory.linkToken(salt, destinationChain, remoteTokenAddress, type, minter, gasValue, { value: gasValue }), + 'Send deployTokenManager to remote chain', + ), + ) + .to.emit(service, 'InterchainTokenIdClaimed') + .withArgs(tokenId, AddressZero, await tokenFactory.linkedTokenDeploySalt(wallet.address, salt)) + .to.emit(service, 'LinkTokenStarted') + .withArgs( + tokenId, + destinationChain, + token.address.toLowerCase(), + remoteTokenAddress.toLowerCase(), + type, + minter.toLowerCase(), + ) + .and.to.emit(gasService, 'NativeGasPaidForContractCall') + .withArgs(service.address, destinationChain, service.address, keccak256(payload), gasValue, wallet.address) + .and.to.emit(gateway, 'ContractCall') + .withArgs(service.address, destinationChain, service.address, keccak256(payload), payload); + }); + + it('Should revert on a remote custom token manager deployment if the token manager does does not exist', async () => { + const salt = getRandomBytes32(); + const tokenId = await service.interchainTokenId(wallet.address, salt); + const tokenAddress = '0x1234'; + const minter = '0x5678'; + const type = LOCK_UNLOCK; + + await expect( + tokenFactory.linkToken(salt, destinationChain, tokenAddress, type, minter, gasValue, { value: gasValue }), + ).to.be.revertedWithCustomError(service, 'TokenManagerDoesNotExist', [tokenId]); + }); + + it('Should revert on remote custom token manager deployment if paused', async () => { + await service.setPauseStatus(true).then((tx) => tx.wait); + + const salt = getRandomBytes32(); + const tokenAddress = '0x1234'; + const minter = '0x5678'; + const type = LOCK_UNLOCK; + + await expectRevert( + (gasOptions) => + tokenFactory.linkToken(salt, destinationChain, tokenAddress, type, minter, gasValue, { + ...gasOptions, + value: gasValue, + }), + service, + 'Pause', + ); + + await service.setPauseStatus(false).then((tx) => tx.wait); + }); + }); }); }); diff --git a/test/InterchainTokenService.js b/test/InterchainTokenService.js index 7a310873..19ee9633 100644 --- a/test/InterchainTokenService.js +++ b/test/InterchainTokenService.js @@ -60,7 +60,10 @@ describe('Interchain Token Service', () => { async function deployNewLockUnlock(service, tokenName, tokenSymbol, tokenDecimals, mintAmount = 0, skipApprove = false) { const salt = getRandomBytes32(); const tokenId = await service.interchainTokenId(wallet.address, salt); - const tokenManager = await getContractAt('TokenManager', await service.tokenManagerAddress(tokenId), wallet); + const tokenManagerAddress = await service.tokenManagerAddress(tokenId); + const tokenManagerType = LOCK_UNLOCK; + const sourceTokenAddress = '0x1234'; + const minter = wallet.address; const token = await deployContract(wallet, 'TestInterchainTokenStandard', [ tokenName, @@ -70,14 +73,29 @@ describe('Interchain Token Service', () => { tokenId, ]); - await service.linkToken(salt, '', token.address, LOCK_UNLOCK, wallet.address, 0).then((tx) => tx.wait); + const sourceAddress = service.address; + const params = defaultAbiCoder.encode(['bytes', 'address'], [wallet.address, token.address]); + const payload = defaultAbiCoder.encode( + ['uint256', 'bytes32', 'uint256', 'bytes', 'bytes', 'bytes'], + [MESSAGE_TYPE_LINK_TOKEN, tokenId, tokenManagerType, sourceTokenAddress, token.address, minter], + ); + const commandId = await approveContractCall(gateway, sourceChain, sourceAddress, service.address, payload); + const expectedTokenManagerAddress = await service.tokenManagerAddress(tokenId); + + await expect(reportGas(service.execute(commandId, sourceChain, sourceAddress, payload), 'Receive GMP DEPLOY_TOKEN_MANAGER')) + .to.emit(service, 'TokenManagerDeployed') + .withArgs(tokenId, expectedTokenManagerAddress, tokenManagerType, params); + + const tokenManager = await getContractAt('TokenManager', tokenManagerAddress, wallet); + expect(await tokenManager.tokenAddress()).to.equal(token.address); + expect(await tokenManager.hasRole(wallet.address, OPERATOR_ROLE)).to.be.true; if (mintAmount > 0) { await token.mint(wallet.address, mintAmount).then((tx) => tx.wait); if (!skipApprove) await token.approve(service.address, mintAmount).then((tx) => tx.wait); } - return [token, tokenManager, tokenId]; + return [token, tokenManager, tokenId, salt]; } async function deployNewLockUnlockFee( @@ -91,7 +109,10 @@ describe('Interchain Token Service', () => { ) { const salt = getRandomBytes32(); const tokenId = await service.interchainTokenId(wallet.address, salt); - const tokenManager = await getContractAt('TokenManager', await service.tokenManagerAddress(tokenId), wallet); + const tokenManagerAddress = await service.tokenManagerAddress(tokenId); + const tokenManagerType = LOCK_UNLOCK_FEE_ON_TRANSFER; + const sourceTokenAddress = '0x1234'; + const minter = wallet.address; let token; @@ -121,7 +142,22 @@ describe('Interchain Token Service', () => { ]); } - await service.linkToken(salt, '', token.address, LOCK_UNLOCK_FEE_ON_TRANSFER, wallet.address, 0).then((tx) => tx.wait); + const sourceAddress = service.address; + const params = defaultAbiCoder.encode(['bytes', 'address'], [wallet.address, token.address]); + const payload = defaultAbiCoder.encode( + ['uint256', 'bytes32', 'uint256', 'bytes', 'bytes', 'bytes'], + [MESSAGE_TYPE_LINK_TOKEN, tokenId, tokenManagerType, sourceTokenAddress, token.address, minter], + ); + const commandId = await approveContractCall(gateway, sourceChain, sourceAddress, service.address, payload); + const expectedTokenManagerAddress = await service.tokenManagerAddress(tokenId); + + await expect(reportGas(service.execute(commandId, sourceChain, sourceAddress, payload), 'Receive GMP DEPLOY_TOKEN_MANAGER')) + .to.emit(service, 'TokenManagerDeployed') + .withArgs(tokenId, expectedTokenManagerAddress, tokenManagerType, params); + + const tokenManager = await getContractAt('TokenManager', tokenManagerAddress, wallet); + expect(await tokenManager.tokenAddress()).to.equal(token.address); + expect(await tokenManager.hasRole(wallet.address, OPERATOR_ROLE)).to.be.true; if (mintAmount > 0) { await token.mint(wallet.address, mintAmount).then((tx) => tx.wait); @@ -138,6 +174,11 @@ describe('Interchain Token Service', () => { async function deployNewMintBurn(service, tokenName, tokenSymbol, tokenDecimals, mintAmount = 0) { const salt = getRandomBytes32(); const tokenId = await service.interchainTokenId(wallet.address, salt); + const tokenManagerAddress = await service.tokenManagerAddress(tokenId); + const tokenManagerType = type; + const sourceTokenAddress = '0x1234'; + const minter = wallet.address; + const token = await deployContract(wallet, 'TestInterchainTokenStandard', [ tokenName, tokenSymbol, @@ -146,7 +187,7 @@ describe('Interchain Token Service', () => { tokenId, ]); - const tokenManager = await getContractAt('TokenManager', await service.tokenManagerAddress(tokenId), wallet); + const tokenManager = await getContractAt('TokenManager', tokenManagerAddress, wallet); if (mintAmount > 0) { await token.mint(wallet.address, mintAmount).then((tx) => tx.wait); @@ -154,15 +195,64 @@ describe('Interchain Token Service', () => { await token.transferMintership(tokenManager.address).then((tx) => tx.wait); - await service.linkToken(salt, '', token.address, type, wallet.address, 0).then((tx) => tx.wait); + const sourceAddress = service.address; + const params = defaultAbiCoder.encode(['bytes', 'address'], [wallet.address, token.address]); + const payload = defaultAbiCoder.encode( + ['uint256', 'bytes32', 'uint256', 'bytes', 'bytes', 'bytes'], + [MESSAGE_TYPE_LINK_TOKEN, tokenId, tokenManagerType, sourceTokenAddress, token.address, minter], + ); + const commandId = await approveContractCall(gateway, sourceChain, sourceAddress, service.address, payload); + const expectedTokenManagerAddress = await service.tokenManagerAddress(tokenId); + + await expect(reportGas(service.execute(commandId, sourceChain, sourceAddress, payload), 'Receive GMP DEPLOY_TOKEN_MANAGER')) + .to.emit(service, 'TokenManagerDeployed') + .withArgs(tokenId, expectedTokenManagerAddress, tokenManagerType, params); + + expect(await tokenManager.tokenAddress()).to.equal(token.address); return [token, tokenManager, tokenId]; }; + async function deployNewInterchainToken(service, tokenName, tokenSymbol, tokenDecimals, mintAmount = 0, skipApprove = false) { + const salt = getRandomBytes32(); + const tokenId = await service.interchainTokenId(wallet.address, salt); + const sourceAddress = service.address; + + const tokenManagerAddress = await service.tokenManagerAddress(tokenId); + const minter = wallet.address; + const operator = '0x'; + const tokenAddress = await service.interchainTokenAddress(tokenId); + const params = defaultAbiCoder.encode(['bytes', 'address'], [minter, tokenAddress]); + const payload = defaultAbiCoder.encode( + ['uint256', 'bytes32', 'string', 'string', 'uint8', 'bytes', 'bytes'], + [MESSAGE_TYPE_DEPLOY_INTERCHAIN_TOKEN, tokenId, tokenName, tokenSymbol, tokenDecimals, minter, operator], + ); + const commandId = await approveContractCall(gateway, sourceChain, sourceAddress, service.address, payload); + + await expect(service.execute(commandId, sourceChain, sourceAddress, payload)) + .to.emit(service, 'InterchainTokenDeployed') + .withArgs(tokenId, tokenAddress, wallet.address, tokenName, tokenSymbol, tokenDecimals) + .and.to.emit(service, 'TokenManagerDeployed') + .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; + + const token = await getContractAt('IInterchainToken', tokenAddress, wallet); + + if (mintAmount > 0) { + await token.mint(wallet.address, mintAmount).then((tx) => tx.wait); + if (!skipApprove) await token.approve(service.address, mintAmount).then((tx) => tx.wait); + } + + return [token, tokenManager, tokenId, salt]; + } + deployFunctions.lockUnlock = deployNewLockUnlock; deployFunctions.lockUnlockFee = deployNewLockUnlockFee; deployFunctions.mintBurn = makeDeployNewMintBurn(MINT_BURN); deployFunctions.mintBurnFrom = makeDeployNewMintBurn(MINT_BURN_FROM); + deployFunctions.interchainToken = deployNewInterchainToken; before(async () => { const wallets = await ethers.getSigners(); @@ -513,7 +603,7 @@ describe('Interchain Token Service', () => { await deployContract(wallet, 'TokenManagerProxy', [service.address, LOCK_UNLOCK, tokenId, validParams]); }); - it('Should revert when deploying a remote interchain token to self', async () => { + it('Should revert when deploying a remote interchain token to self as not the factory', async () => { const tokenName = 'Token Name'; const tokenSymbol = 'TN'; const tokenDecimals = 13; @@ -521,18 +611,9 @@ describe('Interchain Token Service', () => { await expectRevert( (gasOptions) => - serviceTest.deployInterchainToken( - salt, - chainName, - tokenName, - tokenSymbol, - tokenDecimals, - wallet.address, - 0, - gasOptions, - ), + serviceTest.deployInterchainToken(salt, '', tokenName, tokenSymbol, tokenDecimals, wallet.address, 0, gasOptions), serviceTest, - 'CannotDeployRemotelyToSelf', + 'NotInterchainTokenFactory', ); }); @@ -599,180 +680,37 @@ describe('Interchain Token Service', () => { }); }); - describe('Deploy and Register Interchain Token', () => { - const tokenName = 'Token Name'; - const tokenSymbol = 'TN'; - const tokenDecimals = 13; - const salt = getRandomBytes32(); - let tokenManager; - - it('Should register an interchain token', async () => { - const tokenId = await service.interchainTokenId(wallet.address, salt); - const tokenAddress = await service.interchainTokenAddress(tokenId); - const params = defaultAbiCoder.encode(['bytes', 'address'], [wallet.address, tokenAddress]); - const expectedTokenManagerAddress = await service.tokenManagerAddress(tokenId); - - await expect( - reportGas( - service.deployInterchainToken(salt, '', tokenName, tokenSymbol, tokenDecimals, wallet.address, 0), - 'Call deployInterchainToken on source chain', - ), - ) - .to.emit(service, 'InterchainTokenIdClaimed') - .withArgs(tokenId, wallet.address, salt) - .to.emit(service, 'InterchainTokenDeployed') - .withArgs(tokenId, tokenAddress, wallet.address, tokenName, tokenSymbol, tokenDecimals) - .to.emit(service, 'TokenManagerDeployed') - .withArgs(tokenId, expectedTokenManagerAddress, NATIVE_INTERCHAIN_TOKEN, params); - - const tokenManagerAddress = await service.deployedTokenManager(tokenId); - expect(tokenManagerAddress).to.not.equal(AddressZero); - - tokenManager = await getContractAt('TokenManager', tokenManagerAddress, wallet); - expect(await tokenManager.isOperator(wallet.address)).to.be.true; - expect(await tokenManager.isOperator(service.address)).to.be.true; - expect(await tokenManager.isFlowLimiter(wallet.address)).to.be.true; - expect(await tokenManager.isFlowLimiter(service.address)).to.be.true; - - const token = await getContractAt('InterchainToken', tokenAddress, wallet); - expect(await token.isMinter(wallet.address)).to.be.true; - expect(await token.isMinter(service.address)).to.be.true; - }); - - it('Should revert when registering an interchain token as a lock/unlock for a second time', async () => { - expect(await tokenManager.hasRole(wallet.address, OPERATOR_ROLE)).to.be.true; - - // Register the same token again - const revertData = keccak256(toUtf8Bytes('AlreadyDeployed()')).substring(0, 10); - await expectRevert( - (gasOptions) => - service.deployInterchainToken(salt, '', tokenName, tokenSymbol, tokenDecimals, wallet.address, 0, gasOptions), - service, - 'InterchainTokenDeploymentFailed', - [revertData], - ); - }); - - it('Should revert when registering an interchain token when service is paused', async () => { - await service.setPauseStatus(true).then((tx) => tx.wait); - - await expectRevert( - (gasOptions) => - service.deployInterchainToken(salt, '', tokenName, tokenSymbol, tokenDecimals, wallet.address, 0, gasOptions), - service, - 'Pause', - ); - - await service.setPauseStatus(false).then((tx) => tx.wait); - }); - - it('Should revert when registering an interchain token with empty token name', async () => { - expect(await tokenManager.hasRole(wallet.address, OPERATOR_ROLE)).to.be.true; - - await expectRevert( - (gasOptions) => service.deployInterchainToken(salt, '', '', tokenSymbol, tokenDecimals, wallet.address, 0, gasOptions), - service, - 'EmptyTokenName', - ); - }); - - it('Should revert when registering an interchain token with empty token symbol', async () => { - expect(await tokenManager.hasRole(wallet.address, OPERATOR_ROLE)).to.be.true; - - await expectRevert( - (gasOptions) => service.deployInterchainToken(salt, '', tokenName, '', tokenDecimals, wallet.address, 0, gasOptions), - service, - 'EmptyTokenSymbol', - ); - }); - - it('Should revert when deploying an interchain token on chain native to ITS hub', async () => { - await expect(service.setTrustedAddress(chainName, ITS_HUB_ROUTING_IDENTIFIER)) - .to.emit(service, 'TrustedAddressSet') - .withArgs(chainName, ITS_HUB_ROUTING_IDENTIFIER); - - await expectRevert( - (gasOptions) => - service.deployInterchainToken(salt, '', tokenName, tokenSymbol, tokenDecimals, wallet.address, 0, gasOptions), - service, - 'NotSupported', - ); - - await expect(service.removeTrustedAddress(chainName)).to.emit(service, 'TrustedAddressRemoved').withArgs(chainName); - }); - }); - describe('Deploy and Register remote Interchain Token', () => { const tokenName = 'Token Name'; const tokenSymbol = 'TN'; const tokenDecimals = 13; - const minter = '0x12345678'; - let salt; + const minter = '0x1234'; + let salt, tokenId, sourceAddress; before(async () => { salt = getRandomBytes32(); - await service.linkToken(salt, '', testToken.address, LOCK_UNLOCK, '0x', 0).then((tx) => tx.wait); - }); + tokenId = await service.interchainTokenId(wallet.address, salt); + sourceAddress = service.address; - it('Should initialize a remote interchain token deployment', async () => { - const tokenId = await service.interchainTokenId(wallet.address, salt); + const tokenManagerAddress = await service.tokenManagerAddress(tokenId); + const minter = '0x'; + const operator = '0x'; + 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], - ); - - await expect( - reportGas( - service.deployInterchainToken(salt, destinationChain, tokenName, tokenSymbol, tokenDecimals, minter, gasValue, { - value: gasValue, - }), - 'Send deployInterchainToken to remote chain', - ), - ) - .to.emit(service, 'InterchainTokenIdClaimed') - .withArgs(tokenId, wallet.address, salt) - .to.emit(service, 'InterchainTokenDeploymentStarted') - .withArgs(tokenId, tokenName, tokenSymbol, tokenDecimals, minter, destinationChain) - .and.to.emit(gasService, 'NativeGasPaidForContractCall') - .withArgs(service.address, destinationChain, service.address, keccak256(payload), gasValue, wallet.address) - .and.to.emit(gateway, 'ContractCall') - .withArgs(service.address, destinationChain, service.address, keccak256(payload), payload); - }); - - it('Should revert on remote interchain token deployment if destination chain is not trusted', async () => { - await expectRevert( - (gasOptions) => - service.deployInterchainToken(salt, 'untrusted chain', tokenName, tokenSymbol, tokenDecimals, minter, gasValue, { - ...gasOptions, - value: gasValue, - }), - service, - 'UntrustedChain', - ); - }); - - it('Should revert on remote interchain token deployment with invalid token name', async () => { - await expectRevert( - (gasOptions) => - service.deployInterchainToken(salt, destinationChain, '', tokenSymbol, tokenDecimals, minter, gasValue, { - ...gasOptions, - value: gasValue, - }), - service, - 'EmptyTokenName', + ['uint256', 'bytes32', 'string', 'string', 'uint8', 'bytes', 'bytes'], + [MESSAGE_TYPE_DEPLOY_INTERCHAIN_TOKEN, tokenId, tokenName, tokenSymbol, tokenDecimals, minter, operator], ); - }); + const commandId = await approveContractCall(gateway, sourceChain, sourceAddress, service.address, payload); - it('Should revert on remote interchain token deployment with invalid token symbol', async () => { - await expectRevert( - (gasOptions) => - service.deployInterchainToken(salt, destinationChain, tokenName, '', tokenDecimals, minter, gasValue, { - ...gasOptions, - value: gasValue, - }), - service, - 'EmptyTokenSymbol', - ); + await expect(service.execute(commandId, sourceChain, sourceAddress, payload)) + .to.emit(service, 'InterchainTokenDeployed') + .withArgs(tokenId, tokenAddress, AddressZero, tokenName, tokenSymbol, tokenDecimals) + .and.to.emit(service, 'TokenManagerDeployed') + .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; }); it('Should revert on remote interchain token deployment if paused', async () => { @@ -880,7 +818,6 @@ describe('Interchain Token Service', () => { const tokenSymbol = 'TN'; const tokenDecimals = 13; let token, salt, tokenId; - let tokenManagerProxy; before(async () => { salt = getRandomBytes32(); @@ -894,6 +831,15 @@ describe('Interchain Token Service', () => { ]); }); + it('Should revert when calling registerCustomToken as not the factory', async () => { + await expectRevert( + (gasOptions) => service.registerCustomToken(salt, AddressZero, LOCK_UNLOCK, '0x', gasOptions), + service, + 'NotInterchainTokenFactory', + [wallet.address], + ); + }); + it('Should revert on deploying an invalid token manager', async () => { await expectRevert((gasOptions) => service.linkToken(salt, '', token.address, 6, wallet.address, 0, gasOptions)); }); @@ -915,6 +861,14 @@ describe('Interchain Token Service', () => { ); }); + it('Should revert on deploying a local token manager with invalid params', async () => { + await expectRevert( + (gasOptions) => service.linkToken(salt, '', token.address, LOCK_UNLOCK, '0x12', 0, gasOptions), + service, + 'NotSupported', + ); + }); + it('Should revert on linking a token with empty token address', async () => { await expectRevert( (gasOptions) => service.linkToken(salt, '', '0x', MINT_BURN, wallet.address, 0, gasOptions), @@ -933,182 +887,6 @@ describe('Interchain Token Service', () => { ); }); - it('Should revert on deploying a token manager if token handler post deploy fails', async () => { - await expectRevert( - (gasOptions) => service.linkToken(salt, '', AddressZero, LOCK_UNLOCK, wallet.address, 0, gasOptions), - service, - 'PostDeployFailed', - ); - }); - - it('Should deploy a lock_unlock token manager', async () => { - const tokenManagerAddress = await service.tokenManagerAddress(tokenId); - const params = defaultAbiCoder.encode(['bytes', 'address'], [wallet.address, token.address]); - - await expect( - reportGas( - service.linkToken(salt, '', token.address, LOCK_UNLOCK, wallet.address, 0), - 'Call deployTokenManager on source chain', - ), - ) - .to.emit(service, 'InterchainTokenIdClaimed') - .withArgs(tokenId, wallet.address, salt) - .to.emit(service, 'TokenManagerDeployed') - .withArgs(tokenId, tokenManagerAddress, LOCK_UNLOCK, params); - - expect(tokenManagerAddress).to.not.equal(AddressZero); - const tokenManager = await getContractAt('TokenManager', tokenManagerAddress, wallet); - - expect(await tokenManager.isOperator(wallet.address)).to.be.true; - expect(await tokenManager.isOperator(service.address)).to.be.true; - expect(await tokenManager.isFlowLimiter(wallet.address)).to.be.true; - expect(await tokenManager.isFlowLimiter(service.address)).to.be.true; - - const tokenAddress = await service.registeredTokenAddress(tokenId); - expect(tokenAddress).to.eq(token.address); - - tokenManagerProxy = await getContractAt('TokenManagerProxy', tokenManagerAddress, wallet); - - const [implementation, tokenAddressFromProxy] = await tokenManagerProxy.getImplementationTypeAndTokenAddress(); - expect(implementation).to.eq(LOCK_UNLOCK); - expect(tokenAddressFromProxy).to.eq(token.address); - }); - - it('Should revert when deploying a custom token manager twice', async () => { - const revertData = keccak256(toUtf8Bytes('AlreadyDeployed()')).substring(0, 10); - await expectRevert( - (gasOptions) => service.linkToken(salt, '', token.address, LOCK_UNLOCK, wallet.address, 0, gasOptions), - service, - 'TokenManagerDeploymentFailed', - [revertData], - ); - }); - - it('Should revert when calling unsupported functions directly on the token manager implementation', async () => { - const implementationAddress = await tokenManagerProxy.implementation(); - const implementationContract = await getContractAt('TokenManager', implementationAddress, wallet); - await expectRevert((gasOptions) => implementationContract.tokenAddress(gasOptions), implementationContract, 'NotSupported'); - await expectRevert( - (gasOptions) => implementationContract.interchainTokenId(gasOptions), - implementationContract, - 'NotSupported', - ); - await expectRevert( - (gasOptions) => implementationContract.implementationType(gasOptions), - implementationContract, - 'NotSupported', - ); - }); - - it('Should deploy a mint_burn token manager', async () => { - const salt = getRandomBytes32(); - const tokenId = await service.interchainTokenId(wallet.address, salt); - const tokenManagerAddress = await service.tokenManagerAddress(tokenId); - const token = await deployContract(wallet, 'TestInterchainTokenStandard', [ - tokenName, - tokenSymbol, - tokenDecimals, - service.address, - tokenId, - ]); - const params = defaultAbiCoder.encode(['bytes', 'address'], [wallet.address, token.address]); - - const tx = service.linkToken(salt, '', token.address, MINT_BURN, wallet.address, 0); - const expectedTokenManagerAddress = await service.tokenManagerAddress(tokenId); - await expect(tx).to.emit(service, 'TokenManagerDeployed').withArgs(tokenId, expectedTokenManagerAddress, MINT_BURN, params); - - expect(tokenManagerAddress).to.not.equal(AddressZero); - const tokenManager = await getContractAt('TokenManager', tokenManagerAddress, wallet); - - expect(await tokenManager.isOperator(wallet.address)).to.be.true; - expect(await tokenManager.isOperator(service.address)).to.be.true; - expect(await tokenManager.isFlowLimiter(wallet.address)).to.be.true; - expect(await tokenManager.isFlowLimiter(service.address)).to.be.true; - - const tokenAddress = await service.registeredTokenAddress(tokenId); - expect(tokenAddress).to.eq(token.address); - - const tokenManagerProxy = await getContractAt('TokenManagerProxy', tokenManagerAddress, wallet); - - const [implementation, tokenAddressFromProxy] = await tokenManagerProxy.getImplementationTypeAndTokenAddress(); - expect(implementation).to.eq(MINT_BURN); - expect(tokenAddressFromProxy).to.eq(token.address); - }); - - it('Should deploy a mint_burn_from token manager', async () => { - const salt = getRandomBytes32(); - const tokenId = await service.interchainTokenId(wallet.address, salt); - const tokenManagerAddress = await service.tokenManagerAddress(tokenId); - const token = await deployContract(wallet, 'TestInterchainTokenStandard', [ - tokenName, - tokenSymbol, - tokenDecimals, - service.address, - tokenId, - ]); - const params = defaultAbiCoder.encode(['bytes', 'address'], [wallet.address, token.address]); - - const tx = service.linkToken(salt, '', token.address, MINT_BURN_FROM, wallet.address, 0); - const expectedTokenManagerAddress = await service.tokenManagerAddress(tokenId); - await expect(tx) - .to.emit(service, 'TokenManagerDeployed') - .withArgs(tokenId, expectedTokenManagerAddress, MINT_BURN_FROM, params); - - expect(tokenManagerAddress).to.not.equal(AddressZero); - const tokenManager = await getContractAt('TokenManager', tokenManagerAddress, wallet); - - expect(await tokenManager.isOperator(wallet.address)).to.be.true; - expect(await tokenManager.isOperator(service.address)).to.be.true; - expect(await tokenManager.isFlowLimiter(wallet.address)).to.be.true; - expect(await tokenManager.isFlowLimiter(service.address)).to.be.true; - - const tokenAddress = await service.registeredTokenAddress(tokenId); - expect(tokenAddress).to.eq(token.address); - - const tokenManagerProxy = await getContractAt('TokenManagerProxy', tokenManagerAddress, wallet); - - const [implementation, tokenAddressFromProxy] = await tokenManagerProxy.getImplementationTypeAndTokenAddress(); - expect(implementation).to.eq(MINT_BURN_FROM); - expect(tokenAddressFromProxy).to.eq(token.address); - }); - - it('Should deploy a lock_unlock_with_fee token manager', async () => { - const salt = getRandomBytes32(); - const tokenId = await service.interchainTokenId(wallet.address, salt); - const tokenManagerAddress = await service.tokenManagerAddress(tokenId); - const token = await deployContract(wallet, 'TestFeeOnTransferToken', [ - tokenName, - tokenSymbol, - tokenDecimals, - service.address, - tokenId, - ]); - const params = defaultAbiCoder.encode(['bytes', 'address'], [wallet.address, token.address]); - - const tx = service.linkToken(salt, '', token.address, LOCK_UNLOCK_FEE_ON_TRANSFER, wallet.address, 0); - const expectedTokenManagerAddress = await service.tokenManagerAddress(tokenId); - await expect(tx) - .to.emit(service, 'TokenManagerDeployed') - .withArgs(tokenId, expectedTokenManagerAddress, LOCK_UNLOCK_FEE_ON_TRANSFER, params); - - expect(tokenManagerAddress).to.not.equal(AddressZero); - const tokenManager = await getContractAt('TokenManager', tokenManagerAddress, wallet); - - expect(await tokenManager.isOperator(wallet.address)).to.be.true; - expect(await tokenManager.isOperator(service.address)).to.be.true; - expect(await tokenManager.isFlowLimiter(wallet.address)).to.be.true; - expect(await tokenManager.isFlowLimiter(service.address)).to.be.true; - - const tokenAddress = await service.registeredTokenAddress(tokenId); - expect(tokenAddress).to.eq(token.address); - - const tokenManagerProxy = await getContractAt('TokenManagerProxy', tokenManagerAddress, wallet); - - const [implementation, tokenAddressFromProxy] = await tokenManagerProxy.getImplementationTypeAndTokenAddress(); - expect(implementation).to.eq(LOCK_UNLOCK_FEE_ON_TRANSFER); - expect(tokenAddressFromProxy).to.eq(token.address); - }); - it('Should revert when deploying a custom token manager if paused', async () => { await service.setPauseStatus(true).then((tx) => tx.wait); @@ -1123,13 +901,12 @@ describe('Interchain Token Service', () => { }); describe('Initialize remote custom token manager deployment', () => { + let salt, tokenId; + before(async () => { + [, , tokenId, salt] = await deployFunctions.lockUnlock(service, 'Name', 'symbol', 6); + }); it('Should initialize a remote custom token manager deployment', async () => { - const salt = getRandomBytes32(); - const tokenAddress = wallet.address; - - await service.linkToken(salt, '', tokenAddress, MINT_BURN, '0x', 0).then((tx) => tx.wait); - - const tokenId = await service.interchainTokenId(wallet.address, salt); + const tokenAddress = await service.registeredTokenAddress(tokenId); const remoteTokenAddress = '0x1234'; const minter = '0x5789'; const type = LOCK_UNLOCK; @@ -1138,12 +915,6 @@ describe('Interchain Token Service', () => { [MESSAGE_TYPE_LINK_TOKEN, tokenId, type, tokenAddress, remoteTokenAddress, minter], ); - const tokenManager = await getContractAt('TokenManager', await service.deployedTokenManager(tokenId), wallet); - expect(await tokenManager.isOperator(AddressZero)).to.be.true; - expect(await tokenManager.isOperator(service.address)).to.be.true; - expect(await tokenManager.isFlowLimiter(AddressZero)).to.be.true; - expect(await tokenManager.isFlowLimiter(service.address)).to.be.true; - await expect( reportGas( service.linkToken(salt, destinationChain, remoteTokenAddress, type, minter, gasValue, { value: gasValue }), @@ -2553,10 +2324,7 @@ describe('Interchain Token Service', () => { }); it('Should be able to receive native_interchain 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); + const [token, , tokenId] = await deployFunctions.interchainToken(service, 'Test Token Mint Burn', 'TT', 12); await (await token.mint(wallet.address, amount)).wait(); await (await token.approve(service.address, amount)).wait(); @@ -2732,10 +2500,7 @@ describe('Interchain Token Service', () => { }); it('Should be able to receive native_interchain 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); + const [token, , tokenId] = await deployFunctions.interchainToken(service, 'Test Token Mint Burn', 'TT', 12); await (await token.mint(wallet.address, amount)).wait(); await (await token.approve(service.address, amount)).wait(); @@ -3059,14 +2824,19 @@ describe('Interchain Token Service', () => { }); describe('Call contract value', () => { - const trustedAddress = 'Trusted address'; + let trustedAddress; + const untrustedAddress = 'untrusted address'; const amount = 100; + before(() => { + trustedAddress = service.address; + }); + it('Should revert on contractCallValue if not called by remote service', async () => { const payload = '0x'; await expectRevert( - (gasOptions) => service.contractCallValue(sourceChain, trustedAddress, payload, gasOptions), + (gasOptions) => service.contractCallValue(sourceChain, untrustedAddress, payload, gasOptions), service, 'NotRemoteService', ); @@ -3075,8 +2845,6 @@ describe('Interchain Token Service', () => { it('Should revert on contractCallValue if service is paused', async () => { const payload = '0x'; - await service.setTrustedAddress(sourceChain, trustedAddress).then((tx) => tx.wait); - await service.setPauseStatus(true).then((tx) => tx.wait); await expectRevert( diff --git a/test/InterchainTokenServiceFullFlow.js b/test/InterchainTokenServiceFullFlow.js index 3dbf4ac2..2f0181cd 100644 --- a/test/InterchainTokenServiceFullFlow.js +++ b/test/InterchainTokenServiceFullFlow.js @@ -43,6 +43,8 @@ describe('Interchain Token Service Full Flow', () => { const wallets = await ethers.getSigners(); wallet = wallets[0]; ({ service, gateway, gasService, tokenFactory } = await deployAll(wallet, chainName, otherChains)); + + await service.setTrustedAddress(ITS_HUB_CHAIN_NAME, ITS_HUB_ADDRESS).then((tx) => tx.wait); }); /** @@ -340,13 +342,14 @@ describe('Interchain Token Service Full Flow', () => { let token; const otherChains = ['chain 1', 'chain 2']; const gasValues = [1234, 5678]; + const registrationGasValue = 9012; const tokenCap = 1e9; - const salt = keccak256('0x697858'); + const salt = getRandomBytes32(); before(async () => { token = await deployContract(wallet, 'TestMintableBurnableERC20', [name, symbol, decimals]); - tokenId = await service.interchainTokenId(wallet.address, salt); + tokenId = await tokenFactory.linkedTokenId(wallet.address, salt); await token.mint(wallet.address, tokenCap).then((tx) => tx.wait); }); @@ -355,23 +358,30 @@ describe('Interchain Token Service Full Flow', () => { const tokenManagerImplementation = await getContractAt('TokenManager', tokenManagerImplementationAddress, wallet); const params = await tokenManagerImplementation.params(wallet.address, token.address); - let tx = await service.populateTransaction.linkToken(salt, '', token.address, MINT_BURN, wallet.address, 0); - + let tx = await tokenFactory.populateTransaction.registerCustomToken( + salt, + token.address, + MINT_BURN, + wallet.address, + registrationGasValue, + ); const calls = [tx.data]; - let value = 0; + let value = registrationGasValue; for (const i in otherChains) { // This should be replaced with the existing token address on each chain being linked const remoteTokenAddress = token.address; - tx = await service.populateTransaction.linkToken( + tx = await tokenFactory.populateTransaction.linkToken( salt, otherChains[i], remoteTokenAddress, MINT_BURN, wallet.address, gasValues[i], + { value: gasValues[i] }, ); + calls.push(tx.data); value += gasValues[i]; } @@ -382,7 +392,7 @@ describe('Interchain Token Service Full Flow', () => { ); const expectedTokenManagerAddress = await service.tokenManagerAddress(tokenId); - await expect(service.multicall(calls, { value })) + await expect(tokenFactory.multicall(calls, { value })) .to.emit(service, 'TokenManagerDeployed') .withArgs(tokenId, expectedTokenManagerAddress, MINT_BURN, params) .and.to.emit(service, 'LinkTokenStarted') diff --git a/test/InterchainTokenServiceUpgradeFlow.js b/test/InterchainTokenServiceUpgradeFlow.js index 69befd4e..80a9cd48 100644 --- a/test/InterchainTokenServiceUpgradeFlow.js +++ b/test/InterchainTokenServiceUpgradeFlow.js @@ -11,15 +11,21 @@ const { const { getCreate3Address } = require('@axelar-network/axelar-gmp-sdk-solidity'); const { approveContractCall } = require('../scripts/utils'); const { isHardhat, waitFor, getRandomBytes32, getPayloadAndProposalHash, getContractJSON } = require('./utils'); -const { deployContract, deployMockGateway, deployGasService, deployInterchainTokenService } = require('../scripts/deploy'); +const { + deployContract, + deployMockGateway, + deployGasService, + deployInterchainTokenService, + deployInterchainTokenFactory, +} = require('../scripts/deploy'); const { getBytecodeHash } = require('@axelar-network/axelar-chains-config'); const AxelarServiceGovernance = getContractJSON('AxelarServiceGovernance'); const Create3Deployer = getContractJSON('Create3Deployer'); -const { MINT_BURN } = require('./constants'); +const { MINT_BURN, ITS_HUB_CHAIN_NAME, ITS_HUB_ADDRESS } = require('./constants'); describe('Interchain Token Service Upgrade Flow', () => { let wallet, otherWallet, operator; - let service, gateway, gasService; + let service, gateway, gasService, tokenFactory; let tokenManagerDeployer, interchainTokenDeployer, tokenManager, tokenHandler, gatewayCaller; let interchainTokenFactoryAddress; @@ -40,7 +46,7 @@ describe('Interchain Token Service Upgrade Flow', () => { async function testDeployTokenManager() { const salt = getRandomBytes32(); - const tokenId = await service.interchainTokenId(wallet.address, salt); + const tokenId = await tokenFactory.linkedTokenId(wallet.address, salt); const tokenManager = await getContractAt('TokenManager', await service.tokenManagerAddress(tokenId), wallet); const token = await deployContract(wallet, 'TestInterchainTokenStandard', [ @@ -52,7 +58,7 @@ describe('Interchain Token Service Upgrade Flow', () => { ]); const params = defaultAbiCoder.encode(['bytes', 'address'], [wallet.address, token.address]); - await expect(service.linkToken(salt, '', token.address, MINT_BURN, wallet.address, 0)) + await expect(tokenFactory.registerCustomToken(salt, token.address, MINT_BURN, wallet.address, 0)) .to.emit(service, 'TokenManagerDeployed') .withArgs(tokenId, tokenManager.address, MINT_BURN, params); } @@ -101,8 +107,14 @@ describe('Interchain Token Service Upgrade Flow', () => { chainName, [], deploymentKey, - axelarServiceGovernance.address, + wallet.address, ); + + tokenFactory = await deployInterchainTokenFactory(wallet, create3Deployer.address, service.address, deploymentKey + 'Factory'); + + await service.setTrustedAddress(ITS_HUB_CHAIN_NAME, ITS_HUB_ADDRESS).then((tx) => tx.wait); + + await service.transferOwnership(axelarServiceGovernance.address).then((tx) => tx.wait); }); it('should upgrade Interchain Token Service through AxelarServiceGovernance timeLock proposal', async () => {