diff --git a/contracts/InterchainTokenFactory.sol b/contracts/InterchainTokenFactory.sol index 12248b91..e65339f3 100644 --- a/contracts/InterchainTokenFactory.sol +++ b/contracts/InterchainTokenFactory.sol @@ -27,6 +27,7 @@ contract InterchainTokenFactory is IInterchainTokenFactory, ITokenManagerType, M bytes32 internal constant PREFIX_CANONICAL_TOKEN_SALT = keccak256('canonical-token-salt'); bytes32 internal constant PREFIX_INTERCHAIN_TOKEN_SALT = keccak256('interchain-token-salt'); bytes32 internal constant PREFIX_DEPLOY_APPROVAL = keccak256('deploy-approval'); + bytes32 internal constant PREFIX_CUSTOM_TOKEN_SALT = keccak256('custom-token-salt'); address private constant TOKEN_FACTORY_DEPLOYER = address(0); IInterchainTokenService public immutable interchainTokenService; @@ -490,6 +491,53 @@ contract InterchainTokenFactory is IInterchainTokenFactory, ITokenManagerType, M tokenId = deployRemoteCanonicalInterchainToken(originalTokenAddress, destinationChain, gasValue); } + function linkedTokenDeploySalt(address deployer, bytes32 salt) public view returns (bytes32 deploySalt) { + deploySalt = keccak256(abi.encode(PREFIX_CUSTOM_TOKEN_SALT, chainNameHash, deployer, salt)); + } + + function linkedTokenId(address deployer, bytes32 salt) external view returns (bytes32 tokenId) { + bytes32 deploySalt = linkedTokenDeploySalt(deployer, salt); + tokenId = _interchainTokenId(deploySalt); + } + + function registerCustomToken( + bytes32 salt, + address tokenAddress, + TokenManagerType tokenManagerType, + address operator, + uint256 gasValue + ) external payable returns (bytes32 tokenId) { + bytes32 deploySalt = linkedTokenDeploySalt(msg.sender, salt); + bytes memory operatorBytes = ''; + string memory currentChain = ''; + if (operator != address(0)) { + operatorBytes = operator.toBytes(); + } + + tokenId = interchainTokenService.linkToken(deploySalt, currentChain, tokenAddress.toBytes(), tokenManagerType, operatorBytes, 0); + + interchainTokenService.registerTokenMetadata{ value: gasValue }(tokenAddress, gasValue); + } + + function linkToken( + bytes32 salt, + string calldata destinationChain, + bytes calldata destinationTokenAddress, + TokenManagerType tokenManagerType, + bytes calldata linkParams, + uint256 gasValue + ) external payable returns (bytes32 tokenId) { + bytes32 deploySalt = linkedTokenDeploySalt(msg.sender, salt); + tokenId = interchainTokenService.linkToken( + deploySalt, + destinationChain, + destinationTokenAddress, + tokenManagerType, + linkParams, + gasValue + ); + } + /********************\ |* Pure Key Getters *| \********************/ diff --git a/test/InterchainTokenFactory.js b/test/InterchainTokenFactory.js index 1bc83ca2..5f61d2a4 100644 --- a/test/InterchainTokenFactory.js +++ b/test/InterchainTokenFactory.js @@ -10,7 +10,7 @@ const { utils: { defaultAbiCoder, keccak256, toUtf8Bytes, arrayify }, } = ethers; const { deployAll, deployContract } = require('../scripts/deploy'); -const { getRandomBytes32, expectRevert } = require('./utils'); +const { getRandomBytes32, expectRevert, gasReporter } = require('./utils'); const { MESSAGE_TYPE_DEPLOY_INTERCHAIN_TOKEN, NATIVE_INTERCHAIN_TOKEN, @@ -18,9 +18,14 @@ const { MINTER_ROLE, OPERATOR_ROLE, FLOW_LIMITER_ROLE, + MINT_BURN, + MINT_BURN_FROM, + LOCK_UNLOCK_FEE_ON_TRANSFER, } = require('./constants'); const { getBytecodeHash } = require('@axelar-network/axelar-chains-config'); +const reportGas = gasReporter('Interchain Token Factory'); + describe('InterchainTokenFactory', () => { let wallet, otherWallet; let service, gateway, gasService, tokenFactory; @@ -617,5 +622,254 @@ describe('InterchainTokenFactory', () => { .and.to.emit(gateway, 'ContractCall') .withArgs(service.address, destinationChain, service.address, keccak256(payload), payload); }); + + describe('Custom Token Manager Deployment', () => { + const tokenName = 'Token Name'; + const tokenSymbol = 'TN'; + const tokenDecimals = 13; + let token, salt, tokenId; + let tokenManagerProxy; + let factorySalt; + + before(async () => { + salt = getRandomBytes32(); + tokenId = await tokenFactory.linkedTokenId(wallet.address, salt); + token = await deployContract(wallet, 'TestInterchainTokenStandard', [ + tokenName, + tokenSymbol, + tokenDecimals, + service.address, + tokenId, + ]); + factorySalt = await tokenFactory.linkedTokenDeploySalt(wallet.address, salt); + }); + + 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', + ); + }); + + 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), + service, + 'CannotDeploy', + [NATIVE_INTERCHAIN_TOKEN], + ); + }); + + it('Should revert on deploying a remote token manager with interchain token manager type', async () => { + await expectRevert( + (gasOptions) => + tokenFactory.linkToken( + salt, + destinationChain, + token.address, + NATIVE_INTERCHAIN_TOKEN, + wallet.address, + 0, + gasOptions, + ), + service, + 'CannotDeploy', + [NATIVE_INTERCHAIN_TOKEN], + ); + }); + + 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), + 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( + tokenFactory.linkToken(salt, '', token.address, LOCK_UNLOCK, wallet.address, 0), + 'Call deployTokenManager on source chain', + ), + ) + .to.emit(service, 'InterchainTokenIdClaimed') + .withArgs(tokenId, AddressZero, factorySalt) + .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) => tokenFactory.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 tokenFactory.linkedTokenId(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 = tokenFactory.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 tokenFactory.linkedTokenId(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 = tokenFactory.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 tokenFactory.linkedTokenId(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 = tokenFactory.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); + + await expectRevert( + (gasOptions) => tokenFactory.linkToken(salt, '', token.address, LOCK_UNLOCK, wallet.address, 0, gasOptions), + service, + 'Pause', + ); + + await service.setPauseStatus(false).then((tx) => tx.wait); + }); + }); }); });