diff --git a/contracts/interchain-token-service/InterchainTokenService.sol b/contracts/interchain-token-service/InterchainTokenService.sol index 971fb5b9..9cd9109d 100644 --- a/contracts/interchain-token-service/InterchainTokenService.sol +++ b/contracts/interchain-token-service/InterchainTokenService.sol @@ -848,7 +848,7 @@ contract InterchainTokenService is function _decodeMetadata(bytes memory metadata) internal pure returns (uint32 version, bytes memory data) { data = new bytes(metadata.length - 4); assembly { - version := shr(224, mload(data)) + version := shr(224, mload(add(metadata, 32))) } if (data.length == 0) return (version, data); uint256 n = (data.length - 1) / 32; diff --git a/contracts/test/MockAxelarGateway.sol b/contracts/test/MockAxelarGateway.sol index a7f7b399..e57f6b77 100644 --- a/contracts/test/MockAxelarGateway.sol +++ b/contracts/test/MockAxelarGateway.sol @@ -77,6 +77,10 @@ contract MockAxelarGateway is IMockAxelarGateway { _addressStorage[_getTokenAddressKey(symbol)] = tokenAddress; } + function setCommandExecuted(bytes32 commandId, bool executed) external { + _setCommandExecuted(commandId, executed); + } + /********************\ |* Pure Key Getters *| \********************/ diff --git a/scripts/deploy.js b/scripts/deploy.js index 8c3c7a65..5553b23d 100644 --- a/scripts/deploy.js +++ b/scripts/deploy.js @@ -6,7 +6,7 @@ const { create3DeployContract, getCreate3Address } = require('@axelar-network/ax async function deployContract(wallet, contractName, args = []) { const factory = await ethers.getContractFactory(contractName, wallet); - const contract = await factory.deploy(...args); + const contract = await factory.deploy(...args).then((d) => d.deployed()); return contract; } @@ -103,6 +103,7 @@ module.exports = { deployContract, deployRemoteAddressValidator, deployMockGateway, + deployTokenManagerImplementations, deployGasService, deployInterchainTokenService, deployAll, diff --git a/test/RemoteAddressValidator.js b/test/RemoteAddressValidator.js index c787d5c6..d257a31b 100644 --- a/test/RemoteAddressValidator.js +++ b/test/RemoteAddressValidator.js @@ -8,6 +8,7 @@ const { } = ethers; const { expect } = chai; const { deployRemoteAddressValidator, deployContract } = require('../scripts/deploy'); +const { expectRevert } = require('./utils'); describe('RemoteAddressValidator', () => { let ownerWallet, otherWallet, remoteAddressValidator, interchainTokenServiceAddress; @@ -15,6 +16,7 @@ describe('RemoteAddressValidator', () => { const otherRemoteAddress = 'any string as an address'; const otherChain = 'Chain Name'; const chainName = 'Chain Name'; + before(async () => { const wallets = await ethers.getSigners(); ownerWallet = wallets[0]; @@ -25,16 +27,23 @@ describe('RemoteAddressValidator', () => { it('Should revert on RemoteAddressValidator deployment with invalid chain name', async () => { const remoteAddressValidatorFactory = await ethers.getContractFactory('RemoteAddressValidator'); - await expect(remoteAddressValidatorFactory.deploy('')).to.be.revertedWithCustomError(remoteAddressValidator, 'ZeroStringLength'); + await expectRevert( + (gasOptions) => remoteAddressValidatorFactory.deploy('', gasOptions), + remoteAddressValidator, + 'ZeroStringLength', + ); }); it('Should revert on RemoteAddressValidator deployment with length mismatch between chains and trusted addresses arrays', async () => { const remoteAddressValidatorImpl = await deployContract(ownerWallet, 'RemoteAddressValidator', [chainName]); const remoteAddressValidatorProxyFactory = await ethers.getContractFactory('RemoteAddressValidatorProxy'); const params = defaultAbiCoder.encode(['string[]', 'string[]'], [['Chain A'], []]); - await expect( - remoteAddressValidatorProxyFactory.deploy(remoteAddressValidatorImpl.address, ownerWallet.address, params), - ).to.be.revertedWithCustomError(remoteAddressValidator, 'SetupFailed'); + await expectRevert( + (gasOptions) => + remoteAddressValidatorProxyFactory.deploy(remoteAddressValidatorImpl.address, ownerWallet.address, params, gasOptions), + remoteAddressValidator, + 'SetupFailed', + ); }); it('Should revert when querrying the remote address for unregistered chains', async () => { @@ -50,9 +59,11 @@ describe('RemoteAddressValidator', () => { }); it('Should not be able to add a custom remote address as not the owner', async () => { - await expect( - remoteAddressValidator.connect(otherWallet).addTrustedAddress(otherChain, otherRemoteAddress), - ).to.be.revertedWithCustomError(remoteAddressValidator, 'NotOwner'); + await expectRevert( + (gasOptions) => remoteAddressValidator.connect(otherWallet).addTrustedAddress(otherChain, otherRemoteAddress, gasOptions), + remoteAddressValidator, + 'NotOwner', + ); }); it('Should be able to add a custom remote address as the owner', async () => { @@ -63,14 +74,16 @@ describe('RemoteAddressValidator', () => { }); it('Should revert on adding a custom remote address with an empty chain name', async () => { - await expect(remoteAddressValidator.addTrustedAddress('', otherRemoteAddress)).to.be.revertedWithCustomError( + await expectRevert( + (gasOptions) => remoteAddressValidator.addTrustedAddress('', otherRemoteAddress, gasOptions), remoteAddressValidator, 'ZeroStringLength', ); }); it('Should revert on adding a custom remote address with an invalid remote address', async () => { - await expect(remoteAddressValidator.addTrustedAddress(otherChain, '')).to.be.revertedWithCustomError( + await expectRevert( + (gasOptions) => remoteAddressValidator.addTrustedAddress(otherChain, '', gasOptions), remoteAddressValidator, 'ZeroStringLength', ); @@ -81,7 +94,8 @@ describe('RemoteAddressValidator', () => { }); it('Should not be able to remove a custom remote address as not the owner', async () => { - await expect(remoteAddressValidator.connect(otherWallet).removeTrustedAddress(otherChain)).to.be.revertedWithCustomError( + await expectRevert( + (gasOptions) => remoteAddressValidator.connect(otherWallet).removeTrustedAddress(otherChain, gasOptions), remoteAddressValidator, 'NotOwner', ); @@ -98,7 +112,8 @@ describe('RemoteAddressValidator', () => { }); it('Should revert on removing a custom remote address with an empty chain name', async () => { - await expect(remoteAddressValidator.removeTrustedAddress('')).to.be.revertedWithCustomError( + await expectRevert( + (gasOptions) => remoteAddressValidator.removeTrustedAddress('', gasOptions), remoteAddressValidator, 'ZeroStringLength', ); diff --git a/test/tokenService.js b/test/tokenService.js index 90db2482..0c1c1735 100644 --- a/test/tokenService.js +++ b/test/tokenService.js @@ -5,19 +5,28 @@ const { expect } = chai; require('dotenv').config(); const { ethers } = require('hardhat'); const { AddressZero, MaxUint256 } = ethers.constants; -const { defaultAbiCoder, solidityPack, keccak256 } = ethers.utils; +const { defaultAbiCoder, solidityPack, keccak256, arrayify } = ethers.utils; const { Contract, Wallet } = ethers; - const TokenManager = require('../artifacts/contracts/token-manager/TokenManager.sol/TokenManager.json'); const Token = require('../artifacts/contracts/interfaces/IStandardizedToken.sol/IStandardizedToken.json'); - -const { approveContractCall, getRandomBytes32 } = require('../scripts/utils'); -const { deployAll, deployContract } = require('../scripts/deploy'); +const { getCreate3Address } = require('@axelar-network/axelar-gmp-sdk-solidity'); +const { approveContractCall } = require('../scripts/utils'); +const { getRandomBytes32, expectRevert } = require('./utils'); +const { + deployAll, + deployContract, + deployMockGateway, + deployGasService, + deployInterchainTokenService, + deployRemoteAddressValidator, + deployTokenManagerImplementations, +} = require('../scripts/deploy'); const SELECTOR_SEND_TOKEN = 1; const SELECTOR_SEND_TOKEN_WITH_DATA = 2; const SELECTOR_DEPLOY_TOKEN_MANAGER = 3; const SELECTOR_DEPLOY_AND_REGISTER_STANDARDIZED_TOKEN = 4; +const INVALID_SELECTOR = 5; const MINT_BURN = 0; const MINT_BURN_FROM = 1; @@ -164,6 +173,208 @@ describe('Interchain Token Service', () => { [service, gateway, gasService] = await deployAll(wallet, 'Test', [sourceChain, destinationChain]); }); + describe('Interchain Token Service Deployment', () => { + let create3Deployer; + let gateway; + let gasService; + let tokenManagerDeployer; + let standardizedToken; + let standardizedTokenDeployer; + let interchainTokenServiceAddress; + let remoteAddressValidator; + let tokenManagerImplementations; + + const chainName = 'Test'; + const deploymentKey = 'interchainTokenService'; + + before(async () => { + create3Deployer = await deployContract(wallet, 'Create3Deployer'); + gateway = await deployMockGateway(wallet); + gasService = await deployGasService(wallet); + tokenManagerDeployer = await deployContract(wallet, 'TokenManagerDeployer', []); + standardizedToken = await deployContract(wallet, 'StandardizedToken'); + standardizedTokenDeployer = await deployContract(wallet, 'StandardizedTokenDeployer', [standardizedToken.address]); + interchainTokenServiceAddress = await getCreate3Address(create3Deployer.address, wallet, deploymentKey); + remoteAddressValidator = await deployRemoteAddressValidator(wallet, interchainTokenServiceAddress, chainName); + tokenManagerImplementations = await deployTokenManagerImplementations(wallet, interchainTokenServiceAddress); + }); + + it('Should revert on invalid remote address validator', async () => { + await expectRevert( + (gasOptions) => + deployInterchainTokenService( + wallet, + create3Deployer.address, + tokenManagerDeployer.address, + standardizedTokenDeployer.address, + gateway.address, + gasService.address, + AddressZero, + tokenManagerImplementations.map((impl) => impl.address), + deploymentKey, + gasOptions, + ), + service, + 'ZeroAddress', + ); + }); + + it('Should revert on invalid gas service', async () => { + await expectRevert( + (gasOptions) => + deployInterchainTokenService( + wallet, + create3Deployer.address, + tokenManagerDeployer.address, + standardizedTokenDeployer.address, + gateway.address, + AddressZero, + remoteAddressValidator.address, + tokenManagerImplementations.map((impl) => impl.address), + deploymentKey, + gasOptions, + ), + service, + 'ZeroAddress', + ); + }); + + it('Should revert on invalid token manager deployer', async () => { + await expectRevert( + (gasOptions) => + deployInterchainTokenService( + wallet, + create3Deployer.address, + AddressZero, + standardizedTokenDeployer.address, + gateway.address, + gasService.address, + remoteAddressValidator.address, + tokenManagerImplementations.map((impl) => impl.address), + deploymentKey, + gasOptions, + ), + service, + 'ZeroAddress', + ); + }); + + it('Should revert on invalid standardized token deployer', async () => { + await expectRevert( + (gasOptions) => + deployInterchainTokenService( + wallet, + create3Deployer.address, + tokenManagerDeployer.address, + AddressZero, + gateway.address, + gasService.address, + remoteAddressValidator.address, + tokenManagerImplementations.map((impl) => impl.address), + deploymentKey, + gasOptions, + ), + service, + 'ZeroAddress', + ); + }); + + it('Should revert on invalid token manager implementation length', async () => { + tokenManagerImplementations.push(wallet); + + await expectRevert( + (gasOptions) => + deployInterchainTokenService( + wallet, + create3Deployer.address, + tokenManagerDeployer.address, + standardizedTokenDeployer.address, + gateway.address, + gasService.address, + remoteAddressValidator.address, + tokenManagerImplementations.map((impl) => impl.address), + deploymentKey, + gasOptions, + ), + service, + 'LengthMismatch', + ); + + tokenManagerImplementations.pop(); + }); + + it('Should return all token manager implementations', async () => { + const service = await deployInterchainTokenService( + wallet, + create3Deployer.address, + tokenManagerDeployer.address, + standardizedTokenDeployer.address, + gateway.address, + gasService.address, + remoteAddressValidator.address, + tokenManagerImplementations.map((impl) => impl.address), + deploymentKey, + ); + + const length = tokenManagerImplementations.length; + let implementation; + + for (let i = 0; i < length; i++) { + implementation = await service.getImplementation(i); + expect(implementation).to.eq(tokenManagerImplementations[i].address); + } + + await expectRevert((gasOptions) => service.getImplementation(length, gasOptions), service, 'InvalidImplementation'); + }); + + it('Should revert on invalid token manager implementation', async () => { + const toRemove = tokenManagerImplementations.pop(); + + await expectRevert( + (gasOptions) => + deployInterchainTokenService( + wallet, + create3Deployer.address, + tokenManagerDeployer.address, + standardizedTokenDeployer.address, + gateway.address, + gasService.address, + remoteAddressValidator.address, + [...tokenManagerImplementations.map((impl) => impl.address), AddressZero], + deploymentKey, + gasOptions, + ), + service, + 'ZeroAddress', + ); + + tokenManagerImplementations.push(toRemove); + }); + + it('Should revert on duplicate token manager type', async () => { + const length = tokenManagerImplementations.length; + tokenManagerImplementations[length - 1] = tokenManagerImplementations[length - 2]; + + await expectRevert( + (gasOptions) => + deployInterchainTokenService( + wallet, + create3Deployer.address, + tokenManagerDeployer.address, + standardizedTokenDeployer.address, + gateway.address, + gasService.address, + remoteAddressValidator.address, + tokenManagerImplementations.map((impl) => impl.address), + deploymentKey, + gasOptions, + ), + service, + 'InvalidTokenManagerImplementation', + ); + }); + }); + describe('Register Canonical Token', () => { let token; const tokenName = 'Token Name'; @@ -181,6 +392,18 @@ describe('Interchain Token Service', () => { await txPaused.wait(); }); + it('Should revert on pausing if not the owner', async () => { + await expectRevert((gasOptions) => service.connect(liquidityPool).setPaused(true, gasOptions), service, 'NotOwner'); + }); + + it('Should revert on get token manager if token manager does not exist', async () => { + await expectRevert( + (gasOptions) => service.getValidTokenManagerAddress(tokenId, gasOptions), + service, + 'TokenManagerDoesNotExist', + ); + }); + it('Should register a canonical token', async () => { const params = defaultAbiCoder.encode(['bytes', 'address'], ['0x', token.address]); await expect(service.registerCanonicalToken(token.address)) @@ -199,7 +422,8 @@ describe('Interchain Token Service', () => { .to.emit(service, 'TokenManagerDeployed') .withArgs(tokenId, LOCK_UNLOCK, params); - await expect(service.registerCanonicalToken(token.address)).to.be.revertedWithCustomError( + await expectRevert( + (gasOptions) => service.registerCanonicalToken(token.address, gasOptions), service, 'TokenManagerDeploymentFailed', ); @@ -207,13 +431,13 @@ describe('Interchain Token Service', () => { it('Should revert when trying to register a gateway token', async () => { await (await gateway.setTokenAddress(tokenSymbol, token.address)).wait(); - await expect(service.registerCanonicalToken(token.address)).to.be.revertedWithCustomError(service, 'GatewayToken'); + await expectRevert((gasOptions) => service.registerCanonicalToken(token.address, gasOptions), service, 'GatewayToken'); }); it('Should revert when registering a canonical token if paused', async () => { let tx = await service.setPaused(true); await tx.wait(); - await expect(service.registerCanonicalToken(token.address)).to.be.revertedWithCustomError(service, 'Paused'); + await expectRevert((gasOptions) => service.registerCanonicalToken(token.address, gasOptions), service, 'Paused'); tx = await service.setPaused(false); await tx.wait(); }); @@ -229,9 +453,11 @@ describe('Interchain Token Service', () => { beforeEach(async () => { token = await deployContract(wallet, 'InterchainTokenTest', [tokenName, tokenSymbol, tokenDecimals, service.address]); - await (await service.registerCanonicalToken(token.address)).wait(); + await service.registerCanonicalToken(token.address).then((tx) => tx.wait()); + tokenId = await service.getCanonicalTokenId(token.address); - await (await token.setTokenManager(await service.getTokenManagerAddress(tokenId))).wait(); + await token.setTokenManager(await service.getTokenManagerAddress(tokenId)).then((tx) => tx.wait()); + const tokenManagerAddress = await service.getValidTokenManagerAddress(tokenId); expect(tokenManagerAddress).to.not.equal(AddressZero); @@ -254,8 +480,6 @@ describe('Interchain Token Service', () => { .withArgs(service.address, destinationChain, service.address.toLowerCase(), keccak256(payload), payload); }); - // it('Should revert if token manager for given token has not be deployed', async () => {}); - it('Should revert if token manager for given tokenID is not a canonical token manager', async () => { const tokenName = 'Standardized Token'; const tokenSymbol = 'ST'; @@ -275,9 +499,11 @@ describe('Interchain Token Service', () => { expect(tokenManagerAddress).to.not.equal(AddressZero); const gasValue = 1e6; - await expect( - service.deployRemoteCanonicalToken(tokenId, destinationChain, gasValue, { value: gasValue }), - ).to.be.revertedWithCustomError(service, 'NotCanonicalTokenManager'); + await expectRevert( + (gasOptions) => service.deployRemoteCanonicalToken(tokenId, destinationChain, gasValue, { ...gasOptions, value: gasValue }), + service, + 'NotCanonicalTokenManager', + ); }); it('Should revert on remote standardized token deployment if paused', async () => { @@ -287,9 +513,12 @@ describe('Interchain Token Service', () => { const salt = getRandomBytes32(); const tokenId = await service.getCustomTokenId(wallet.address, salt); const gasValue = 1e6; - await expect( - service.deployRemoteCanonicalToken(tokenId, destinationChain, gasValue, { value: gasValue }), - ).to.be.revertedWithCustomError(service, 'Paused'); + + await expectRevert( + (gasOptions) => service.deployRemoteCanonicalToken(tokenId, destinationChain, gasValue, { ...gasOptions, value: gasValue }), + service, + 'Paused', + ); tx = await service.setPaused(false); await tx.wait(); @@ -325,6 +554,31 @@ describe('Interchain Token Service', () => { expect(await tokenManager.operator()).to.equal(wallet.address); }); + it('Should revert when registering a standardized token when service is paused', async () => { + const salt = getRandomBytes32(); + + txPaused = await service.setPaused(true); + await txPaused.wait(); + + await expectRevert( + (gasOptions) => + service.deployAndRegisterStandardizedToken( + salt, + tokenName, + tokenSymbol, + tokenDecimals, + mintAmount, + wallet.address, + gasOptions, + ), + service, + 'Paused', + ); + + txPaused = await service.setPaused(false); + await txPaused.wait(); + }); + it('Should revert when registering a standardized token as a lock/unlock for a second time', async () => { const salt = getRandomBytes32(); const tokenId = await service.getCustomTokenId(wallet.address, salt); @@ -342,9 +596,20 @@ describe('Interchain Token Service', () => { expect(await tokenManager.operator()).to.equal(wallet.address); // Register the same token again - await expect( - service.deployAndRegisterStandardizedToken(salt, tokenName, tokenSymbol, tokenDecimals, mintAmount, wallet.address), - ).to.be.revertedWithCustomError(service, 'StandardizedTokenDeploymentFailed'); + await expectRevert( + (gasOptions) => + service.deployAndRegisterStandardizedToken( + salt, + tokenName, + tokenSymbol, + tokenDecimals, + mintAmount, + wallet.address, + gasOptions, + ), + service, + 'StandardizedTokenDeploymentFailed', + ); }); }); @@ -419,21 +684,24 @@ describe('Interchain Token Service', () => { let tx = await service.setPaused(true); await tx.wait(); - await expect( - service.deployAndRegisterRemoteStandardizedToken( - salt, - tokenName, - tokenSymbol, - tokenDecimals, - distributor, - mintTo, - mintAmount, - operator, - destinationChain, - gasValue, - { value: gasValue }, - ), - ).to.be.revertedWithCustomError(service, 'Paused'); + await expectRevert( + (gasOptions) => + service.deployAndRegisterRemoteStandardizedToken( + salt, + tokenName, + tokenSymbol, + tokenDecimals, + distributor, + mintTo, + mintAmount, + operator, + destinationChain, + gasValue, + { ...gasOptions, value: gasValue }, + ), + service, + 'Paused', + ); tx = await service.setPaused(false); await tx.wait(); @@ -566,6 +834,44 @@ describe('Interchain Token Service', () => { expect(await tokenManager.tokenAddress()).to.equal(tokenAddress); expect(await tokenManager.operator()).to.equal(service.address); }); + + it('Should be able to receive a remote standardized token depoloyment with a mint/burn token manager with non-empty mintTo bytes', async () => { + const tokenId = getRandomBytes32(); + const tokenManagerAddress = await service.getTokenManagerAddress(tokenId); + const distributor = '0x'; + const mintTo = arrayify(tokenManagerAddress); + const mintAmount = 1234; + const operator = '0x'; + const tokenAddress = await service.getStandardizedTokenAddress(tokenId); + const params = defaultAbiCoder.encode(['bytes', 'address'], [service.address, tokenAddress]); + const payload = defaultAbiCoder.encode( + ['uint256', 'bytes32', 'string', 'string', 'uint8', 'bytes', 'bytes', 'uint256', 'bytes'], + [ + SELECTOR_DEPLOY_AND_REGISTER_STANDARDIZED_TOKEN, + tokenId, + tokenName, + tokenSymbol, + tokenDecimals, + distributor, + mintTo, + mintAmount, + operator, + ], + ); + const commandId = await approveContractCall(gateway, sourceChain, sourceAddress, service.address, payload); + const token = new Contract(tokenAddress, Token.abi, wallet); + + await expect(service.execute(commandId, sourceChain, sourceAddress, payload)) + .to.emit(service, 'StandardizedTokenDeployed') + .withArgs(tokenId, tokenManagerAddress, tokenName, tokenSymbol, tokenDecimals, mintAmount, tokenManagerAddress) + .and.to.emit(token, 'Transfer') + .withArgs(AddressZero, tokenManagerAddress, mintAmount) + .and.to.emit(service, 'TokenManagerDeployed') + .withArgs(tokenId, MINT_BURN, params); + const tokenManager = new Contract(tokenManagerAddress, TokenManager.abi, wallet); + expect(await tokenManager.tokenAddress()).to.equal(tokenAddress); + expect(await tokenManager.operator()).to.equal(service.address); + }); }); describe('Custom Token Manager Deployment', () => { @@ -670,7 +976,8 @@ describe('Interchain Token Service', () => { const tx = service.deployCustomTokenManager(salt, LOCK_UNLOCK, params); await expect(tx).to.emit(service, 'TokenManagerDeployed').withArgs(tokenId, LOCK_UNLOCK, params); - await expect(service.deployCustomTokenManager(salt, LOCK_UNLOCK, params)).to.be.revertedWithCustomError( + await expectRevert( + (gasOptions) => service.deployCustomTokenManager(salt, LOCK_UNLOCK, params, gasOptions), service, 'TokenManagerDeploymentFailed', ); @@ -689,8 +996,8 @@ describe('Interchain Token Service', () => { const token = await deployContract(wallet, 'InterchainTokenTest', [tokenName, tokenSymbol, tokenDecimals, tokenManagerAddress]); const params = defaultAbiCoder.encode(['bytes', 'address'], [wallet.address, token.address]); - const tx = service.deployCustomTokenManager(salt, LOCK_UNLOCK, params); - await expect(tx).to.be.revertedWithCustomError(service, 'Paused'); + await expectRevert((gasOptions) => service.deployCustomTokenManager(salt, LOCK_UNLOCK, params, gasOptions), service, 'Paused'); + tx2 = await service.setPaused(false); await tx2.wait(); }); @@ -733,9 +1040,15 @@ describe('Interchain Token Service', () => { const params = '0x1234'; const type = LOCK_UNLOCK; - await expect( - service.deployRemoteCustomTokenManager(salt, destinationChain, type, params, gasValue, { value: gasValue }), - ).to.be.revertedWithCustomError(service, 'Paused'); + await expectRevert( + (gasOptions) => + service.deployRemoteCustomTokenManager(salt, destinationChain, type, params, gasValue, { + ...gasOptions, + value: gasValue, + }), + service, + 'Paused', + ); tx = await service.setPaused(false); await tx.wait(); }); @@ -778,9 +1091,15 @@ describe('Interchain Token Service', () => { const params = '0x1234'; const type = LOCK_UNLOCK; - await expect( - service.deployRemoteCustomTokenManager(salt, destinationChain, type, params, gasValue, { value: gasValue }), - ).to.be.revertedWithCustomError(service, 'Paused'); + await expectRevert( + (gasOptions) => + service.deployRemoteCustomTokenManager(salt, destinationChain, type, params, gasValue, { + ...gasOptions, + value: gasValue, + }), + service, + 'Paused', + ); tx = await service.setPaused(false); await tx.wait(); }); @@ -897,12 +1216,113 @@ describe('Interchain Token Service', () => { .withArgs(tokenId, destinationChain, destAddress, sendAmount); }); } + + it(`Should revert on initiate interchain token transfer when service is paused`, async () => { + const [, tokenManager] = await deployFunctions.lockUnlock(`Test Token lockUnlock`, 'TT', 12, amount); + + let txPaused = await service.setPaused(true); + await txPaused.wait(); + + await expectRevert( + (gasOptions) => + tokenManager.interchainTransfer(destinationChain, destAddress, amount, '0x', { ...gasOptions, value: gasValue }), + service, + 'Paused', + ); + + txPaused = await service.setPaused(false); + await txPaused.wait(); + }); + + it(`Should revert on transmit send token when not called by token manager`, async () => { + const [, tokenManager, tokenId] = await deployFunctions.lockUnlock(`Test Token lockUnlock`, 'TT', 12, amount); + + await expectRevert( + (gasOptions) => + service.transmitSendToken(tokenId, tokenManager.address, destinationChain, destAddress, amount, '0x', { + ...gasOptions, + value: gasValue, + }), + service, + 'NotTokenManager', + ); + }); + }); + + describe('Execute checks', () => { + const sourceChain = 'source chain'; + let sourceAddress; + const amount = 1234; + let destAddress; + + before(async () => { + sourceAddress = service.address.toLowerCase(); + destAddress = wallet.address; + }); + + it('Should revert on execute if remote address validation fails', async () => { + const [token, tokenManager, tokenId] = await deployFunctions.lockUnlock(`Test Token Lock Unlock`, 'TT', 12, amount); + (await await token.transfer(tokenManager.address, amount)).wait(); + + const payload = defaultAbiCoder.encode( + ['uint256', 'bytes32', 'bytes', 'uint256'], + [SELECTOR_SEND_TOKEN, tokenId, destAddress, amount], + ); + const commandId = await approveContractCall(gateway, sourceChain, wallet.address, service.address, payload); + + await expectRevert( + (gasOptions) => service.execute(commandId, sourceChain, wallet.address, payload, gasOptions), + service, + 'NotRemoteService', + ); + }); + + it('Should revert on execute if the service is paused', async () => { + const [token, tokenManager, tokenId] = await deployFunctions.lockUnlock(`Test Token Lock Unlock`, 'TT', 12, amount); + (await await token.transfer(tokenManager.address, amount)).wait(); + + const payload = defaultAbiCoder.encode( + ['uint256', 'bytes32', 'bytes', 'uint256'], + [SELECTOR_SEND_TOKEN, tokenId, destAddress, amount], + ); + const commandId = await approveContractCall(gateway, sourceChain, sourceAddress, service.address, payload); + + let txPaused = await service.setPaused(true); + await txPaused.wait(); + + await expectRevert( + (gasOptions) => service.execute(commandId, sourceChain, sourceAddress, payload, gasOptions), + service, + 'Paused', + ); + + txPaused = await service.setPaused(false); + await txPaused.wait(); + }); + + it('Should revert on execute with invalid selector', async () => { + const [token, tokenManager, tokenId] = await deployFunctions.lockUnlock(`Test Token Lock Unlock`, 'TT', 12, amount); + (await await token.transfer(tokenManager.address, amount)).wait(); + + const payload = defaultAbiCoder.encode( + ['uint256', 'bytes32', 'bytes', 'uint256'], + [INVALID_SELECTOR, tokenId, destAddress, amount], + ); + const commandId = await approveContractCall(gateway, sourceChain, sourceAddress, service.address, payload); + + await expectRevert( + (gasOptions) => service.execute(commandId, sourceChain, sourceAddress, payload, gasOptions), + service, + 'SelectorUnknown', + ); + }); }); describe('Receive Remote Tokens', () => { let sourceAddress; const amount = 1234; let destAddress; + before(async () => { sourceAddress = service.address.toLowerCase(); destAddress = wallet.address; @@ -1015,6 +1435,75 @@ describe('Interchain Token Service', () => { .withArgs(tokenId, destinationChain, destAddress, sendAmount, sourceAddress, data); }); } + + for (const type of ['lockUnlock', 'mintBurn', 'lockUnlockFee', 'liquidityPool']) { + it(`Should be able to initiate an interchain token transfer via the interchainTransfer function on the service [${type}]`, async () => { + const [token, tokenManager, tokenId] = await deployFunctions[type](`Test Token ${type}`, 'TT', 12, amount); + const sendAmount = type === 'lockUnlockFee' ? amount - 10 : amount; + const metadata = '0x00000000'; + const payload = defaultAbiCoder.encode( + ['uint256', 'bytes32', 'bytes', 'uint256', 'bytes', 'bytes'], + [SELECTOR_SEND_TOKEN_WITH_DATA, tokenId, destAddress, sendAmount, sourceAddress, '0x'], + ); + const payloadHash = keccak256(payload); + + let transferToAddress = AddressZero; + + if (type === 'lockUnlock' || type === 'lockUnlockFee') { + transferToAddress = tokenManager.address; + } else if (type === 'liquidityPool') { + transferToAddress = liquidityPool.address; + } + + await expect(service.interchainTransfer(tokenId, destinationChain, destAddress, amount, metadata)) + .and.to.emit(token, 'Transfer') + .withArgs(wallet.address, transferToAddress, amount) + .and.to.emit(gateway, 'ContractCall') + .withArgs(service.address, destinationChain, service.address.toLowerCase(), payloadHash, payload) + .to.emit(service, 'TokenSentWithData') + .withArgs(tokenId, destinationChain, destAddress, sendAmount, sourceAddress, '0x'); + }); + } + + for (const type of ['lockUnlock', 'mintBurn', 'lockUnlockFee', 'liquidityPool']) { + it(`Should be able to initiate an interchain token transfer via the sendTokenWithData function on the service [${type}]`, async () => { + const [token, tokenManager, tokenId] = await deployFunctions[type](`Test Token ${type}`, 'TT', 12, amount); + const sendAmount = type === 'lockUnlockFee' ? amount - 10 : amount; + const payload = defaultAbiCoder.encode( + ['uint256', 'bytes32', 'bytes', 'uint256', 'bytes', 'bytes'], + [SELECTOR_SEND_TOKEN_WITH_DATA, tokenId, destAddress, sendAmount, sourceAddress, data], + ); + const payloadHash = keccak256(payload); + + let transferToAddress = AddressZero; + + if (type === 'lockUnlock' || type === 'lockUnlockFee') { + transferToAddress = tokenManager.address; + } else if (type === 'liquidityPool') { + transferToAddress = liquidityPool.address; + } + + await expect(service.sendTokenWithData(tokenId, destinationChain, destAddress, amount, data)) + .and.to.emit(token, 'Transfer') + .withArgs(wallet.address, transferToAddress, amount) + .and.to.emit(gateway, 'ContractCall') + .withArgs(service.address, destinationChain, service.address.toLowerCase(), payloadHash, payload) + .to.emit(service, 'TokenSentWithData') + .withArgs(tokenId, destinationChain, destAddress, sendAmount, sourceAddress, data); + }); + } + + it(`Should revert on interchainTransfer function with invalid metadata version`, async () => { + const [, , tokenId] = await deployFunctions.lockUnlock(`Test Token lockUnlock`, 'TT', 12, amount); + + const metadata = '0x00000001'; + + await expectRevert( + (gasOptions) => service.interchainTransfer(tokenId, destinationChain, destAddress, amount, metadata, gasOptions), + service, + 'InvalidMetadataVersion', + ); + }); }); describe('Receive Remote Tokens with Data', () => { @@ -1262,10 +1751,49 @@ describe('Interchain Token Service', () => { let sourceAddress; const amount = 1234; const destAddress = new Wallet(getRandomBytes32()).address; + before(async () => { sourceAddress = service.address.toLowerCase(); }); + it('Should revert if command is already executed by gateway', async () => { + const [token, tokenManager, tokenId] = await deployFunctions.lockUnlock(`Test Token Lock Unlock`, 'TT', 12, 2 * amount); + await (await token.transfer(tokenManager.address, amount)).wait(); + await (await token.approve(service.address, amount)).wait(); + + const payload = defaultAbiCoder.encode( + ['uint256', 'bytes32', 'bytes', 'uint256'], + [SELECTOR_SEND_TOKEN, tokenId, destAddress, amount], + ); + + const commandId = await approveContractCall(gateway, sourceChain, sourceAddress, service.address, payload); + await gateway.setCommandExecuted(commandId, true).then((tx) => tx.wait()); + + await expectRevert( + (gasOptions) => service.expressReceiveToken(payload, commandId, sourceChain, gasOptions), + service, + 'AlreadyExecuted', + ); + }); + + it('Should revert with invalid selector', async () => { + const [token, tokenManager, tokenId] = await deployFunctions.lockUnlock(`Test Token Lock Unlock`, 'TT', 12, 2 * amount); + await (await token.transfer(tokenManager.address, amount)).wait(); + await (await token.approve(service.address, amount)).wait(); + + const payload = defaultAbiCoder.encode( + ['uint256', 'bytes32', 'bytes', 'uint256'], + [SELECTOR_DEPLOY_TOKEN_MANAGER, tokenId, destAddress, amount], + ); + const commandId = await approveContractCall(gateway, sourceChain, sourceAddress, service.address, payload); + + await expectRevert( + (gasOptions) => service.expressReceiveToken(payload, commandId, sourceChain, gasOptions), + service, + 'InvalidExpressSelector', + ); + }); + it('Should be able to receive lock/unlock token', async () => { const [token, tokenManager, tokenId] = await deployFunctions.lockUnlock(`Test Token Lock Unlock`, 'TT', 12, 2 * amount); await (await token.transfer(tokenManager.address, amount)).wait(); @@ -1456,6 +1984,7 @@ describe('Interchain Token Service', () => { const sendAmount = 1234; const flowLimit = (sendAmount * 3) / 2; const mintAmount = flowLimit * 3; + beforeEach(async () => { [, tokenManager, tokenId] = await deployFunctions.mintBurn(`Test Token Lock Unlock`, 'TT', 12, mintAmount); await (await tokenManager.setFlowLimit(flowLimit)).wait(); @@ -1465,12 +1994,23 @@ describe('Interchain Token Service', () => { // LMK of any fixes to this that do not involve writing a new contract to facilitate a multicall. it('Should be able to send token only if it does not trigger the mint limit', async () => { await (await tokenManager.interchainTransfer(destinationChain, destinationAddress, sendAmount, '0x')).wait(); - await expect( - tokenManager.interchainTransfer(destinationChain, destinationAddress, sendAmount, '0x'), - ).to.be.revertedWithCustomError(tokenManager, 'FlowLimitExceeded'); + await expectRevert( + (gasOptions) => tokenManager.interchainTransfer(destinationChain, destinationAddress, sendAmount, '0x', gasOptions), + tokenManager, + 'FlowLimitExceeded', + ); }); it('Should be able to receive token only if it does not trigger the mint limit', async () => { + const tokenFlowLimit = await service.getFlowLimit(tokenId); + expect(tokenFlowLimit).to.eq(flowLimit); + + let flowIn = await service.getFlowInAmount(tokenId); + let flowOut = await service.getFlowOutAmount(tokenId); + + expect(flowIn).to.eq(0); + expect(flowOut).to.eq(0); + async function receiveToken(sendAmount) { const payload = defaultAbiCoder.encode( ['uint256', 'bytes32', 'bytes', 'uint256'], @@ -1483,7 +2023,47 @@ describe('Interchain Token Service', () => { await (await receiveToken(sendAmount)).wait(); - await expect(receiveToken(sendAmount)).to.be.revertedWithCustomError(tokenManager, 'FlowLimitExceeded'); + flowIn = await service.getFlowInAmount(tokenId); + flowOut = await service.getFlowOutAmount(tokenId); + + expect(flowIn).to.eq(sendAmount); + expect(flowOut).to.eq(0); + + await expectRevert((gasOptions) => receiveToken(sendAmount, gasOptions), tokenManager, 'FlowLimitExceeded'); + }); + + it('Should be able to set flow limits for each token manager', async () => { + const tokenIds = []; + const flowLimits = new Array(4).fill(flowLimit); + const tokenManagers = []; + + for (const type of ['lockUnlock', 'mintBurn', 'lockUnlockFee', 'liquidityPool']) { + const [, tokenManager, tokenId] = await deployFunctions[type](`Test Token ${type}`, 'TT', 12, mintAmount); + tokenIds.push(tokenId); + tokenManagers.push(tokenManager); + + await tokenManager.transferOperatorship(service.address).then((tx) => tx.wait()); + } + + await expectRevert( + (gasOptions) => service.connect(liquidityPool).setFlowLimits(tokenIds, flowLimits, gasOptions), + service, + 'NotOperator', + ); + + await expect(service.setFlowLimits(tokenIds, flowLimits)) + .to.emit(tokenManagers[0], 'FlowLimitSet') + .withArgs(flowLimit) + .to.emit(tokenManagers[1], 'FlowLimitSet') + .withArgs(flowLimit) + .to.emit(tokenManagers[2], 'FlowLimitSet') + .withArgs(flowLimit) + .to.emit(tokenManagers[3], 'FlowLimitSet') + .withArgs(flowLimit); + + flowLimits.pop(); + + await expectRevert((gasOptions) => service.setFlowLimits(tokenIds, flowLimits, gasOptions), service, 'LengthMismatch'); }); }); }); diff --git a/test/tokenServiceFullFlow.js b/test/tokenServiceFullFlow.js index 2170c04c..05028287 100644 --- a/test/tokenServiceFullFlow.js +++ b/test/tokenServiceFullFlow.js @@ -12,11 +12,10 @@ const IStandardizedToken = require('../artifacts/contracts/interfaces/IStandardi const ITokenManager = require('../artifacts/contracts/interfaces/ITokenManager.sol/ITokenManager.json'); const ITokenManagerMintBurn = require('../artifacts/contracts/interfaces/ITokenManagerMintBurn.sol/ITokenManagerMintBurn.json'); -const { getRandomBytes32 } = require('../scripts/utils'); +const { getRandomBytes32, expectRevert } = require('./utils'); const { deployAll, deployContract } = require('../scripts/deploy'); const SELECTOR_SEND_TOKEN = 1; -// const SELECTOR_SEND_TOKEN_WITH_DATA = 2; const SELECTOR_DEPLOY_TOKEN_MANAGER = 3; const SELECTOR_DEPLOY_AND_REGISTER_STANDARDIZED_TOKEN = 4; @@ -125,8 +124,8 @@ describe('Interchain Token Service Full Flow', () => { await expect(token.transferDistributorship(newAddress)).to.emit(token, 'DistributorshipTransferred').withArgs(newAddress); - await expect(token.mint(newAddress, amount)).to.be.revertedWithCustomError(token, 'NotDistributor'); - await expect(token.burn(newAddress, amount)).to.be.revertedWithCustomError(token, 'NotDistributor'); + await expectRevert((gasOptions) => token.mint(newAddress, amount, gasOptions), token, 'NotDistributor'); + await expectRevert((gasOptions) => token.burn(newAddress, amount, gasOptions), token, 'NotDistributor'); }); }); @@ -233,8 +232,8 @@ describe('Interchain Token Service Full Flow', () => { await expect(token.transferDistributorship(newAddress)).to.emit(token, 'DistributorshipTransferred').withArgs(newAddress); - await expect(token.mint(newAddress, amount)).to.be.revertedWithCustomError(token, 'NotDistributor'); - await expect(token.burn(newAddress, amount)).to.be.revertedWithCustomError(token, 'NotDistributor'); + await expectRevert((gasOptions) => token.mint(newAddress, amount, gasOptions), token, 'NotDistributor'); + await expectRevert((gasOptions) => token.burn(newAddress, amount, gasOptions), token, 'NotDistributor'); }); }); @@ -309,8 +308,8 @@ describe('Interchain Token Service Full Flow', () => { .to.emit(token, 'DistributorshipTransferred') .withArgs(tokenManager.address); - await expect(token.mint(newAddress, amount)).to.be.revertedWithCustomError(token, 'NotDistributor'); - await expect(token.burn(newAddress, amount)).to.be.revertedWithCustomError(token, 'NotDistributor'); + await expectRevert((gasOptions) => token.mint(newAddress, amount, gasOptions), token, 'NotDistributor'); + await expectRevert((gasOptions) => token.burn(newAddress, amount, gasOptions), token, 'NotDistributor'); }); // In order to be able to receive tokens the distributorship should be changed on other chains as well.