diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 059073fa..651d4eeb 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -113,3 +113,9 @@ jobs: - name: Upgrade ITS using create2 run: node evm/deploy-its.js -s "ITS v1.0.0" -f "ITS v1.0.0 Factory" -m create2 -u -y + + - name: InterchainTokenFactory deploy interchain token on current chain + run: node evm/interchainTokenFactory.js --action deployInterchainToken --name "test" --symbol "TST" --decimals 18 --distributor 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 --salt "salt" -y + + - name: InterchainTokenService deploy interchain token on current chain + run: node evm/its.js --action deployInterchainToken --name "test" --symbol "TST" --decimals 18 --distributor 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 --destinationChain '' --gasValue 0 --salt "salt" -y diff --git a/evm/cli-utils.js b/evm/cli-utils.js index 6ce17f32..36d7f243 100644 --- a/evm/cli-utils.js +++ b/evm/cli-utils.js @@ -36,7 +36,7 @@ const addExtendedOptions = (program, options = {}) => { program.addOption(new Option('-v, --verify', 'verify the deployed contract on the explorer').env('VERIFY')); if (options.artifactPath) { - program.addOption(new Option('-a, --artifactPath ', 'artifact path')); + program.addOption(new Option('--artifactPath ', 'artifact path')); } if (options.contractName) { diff --git a/evm/deploy-contract.js b/evm/deploy-contract.js index 27d44849..884330d4 100644 --- a/evm/deploy-contract.js +++ b/evm/deploy-contract.js @@ -15,7 +15,7 @@ const { printWarn, printError, copyObject, - isString, + isNonEmptyString, isNumber, isAddressArray, getBytecodeHash, @@ -45,14 +45,14 @@ async function getConstructorArgs(contractName, chain, wallet) { const governanceChain = contractConfig.governanceChain || 'Axelarnet'; contractConfig.governanceChain = governanceChain; - if (!isString(governanceChain)) { + if (!isNonEmptyString(governanceChain)) { throw new Error(`Missing AxelarServiceGovernance.governanceChain in the chain info.`); } const governanceAddress = contractConfig.governanceAddress || 'axelar10d07y265gmmuvt4z0w9aw880jnsr700j7v9daj'; contractConfig.governanceAddress = governanceAddress; - if (!isString(governanceAddress)) { + if (!isNonEmptyString(governanceAddress)) { throw new Error(`Missing AxelarServiceGovernance.governanceAddress in the chain info.`); } @@ -102,14 +102,14 @@ async function getConstructorArgs(contractName, chain, wallet) { const governanceChain = contractConfig.governanceChain || 'Axelarnet'; contractConfig.governanceChain = governanceChain; - if (!isString(governanceChain)) { + if (!isNonEmptyString(governanceChain)) { throw new Error(`Missing InterchainGovernance.governanceChain in the chain info.`); } const governanceAddress = contractConfig.governanceAddress || 'axelar10d07y265gmmuvt4z0w9aw880jnsr700j7v9daj'; contractConfig.governanceAddress = governanceAddress; - if (!isString(governanceAddress)) { + if (!isNonEmptyString(governanceAddress)) { throw new Error(`Missing InterchainGovernance.governanceAddress in the chain info.`); } diff --git a/evm/execute-contract.js b/evm/execute-contract.js index e8189096..5b477b8e 100644 --- a/evm/execute-contract.js +++ b/evm/execute-contract.js @@ -12,7 +12,7 @@ const { const readlineSync = require('readline-sync'); const { Command, Option } = require('commander'); -const { isNumber, isString, loadConfig, saveConfig, printObj, printLog, printError, getContractJSON } = require('./utils'); +const { isNumber, isNonEmptyString, loadConfig, saveConfig, printObj, printLog, printError, getContractJSON } = require('./utils'); const { addBaseOptions } = require('./cli-utils'); async function getCallData(action, targetContract, inputRecipient, inputAmount) { @@ -129,7 +129,7 @@ async function executeContract(options, chain, wallet) { throw new Error('Missing target address in the address info.'); } - if (!isString(action)) { + if (!isNonEmptyString(action)) { throw new Error('Missing method name from the user info.'); } @@ -181,7 +181,7 @@ async function main(options) { const provider = getDefaultProvider(rpc); const privateKey = options.privateKey; - if (!isString(privateKey)) { + if (!isNonEmptyString(privateKey)) { throw new Error('Private Key value is not provided in the info file'); } diff --git a/evm/interchainTokenFactory.js b/evm/interchainTokenFactory.js new file mode 100644 index 00000000..ffdc1e33 --- /dev/null +++ b/evm/interchainTokenFactory.js @@ -0,0 +1,323 @@ +'use strict'; + +require('dotenv').config(); + +const { ethers } = require('hardhat'); +const { + getDefaultProvider, + utils: { hexZeroPad }, + Contract, +} = ethers; +const { Command, Option } = require('commander'); +const { printInfo, prompt, mainProcessor, validateParameters, getContractJSON } = require('./utils'); +const { getWallet } = require('./sign-utils'); +const { addExtendedOptions } = require('./cli-utils'); +const { getDeploymentSalt, handleTx } = require('./its'); +const IInterchainTokenFactory = getContractJSON('IInterchainTokenFactory'); +const IInterchainTokenService = getContractJSON('IInterchainTokenService'); +const IERC20 = getContractJSON('IERC20'); + +async function processCommand(config, chain, options) { + const { privateKey, address, action, yes } = options; + + const contracts = chain.contracts; + const contractName = 'InterchainTokenFactory'; + const contractConfig = contracts.InterchainTokenService; + + const interchainTokenFactoryAddress = address || contracts.InterchainTokenService?.interchainTokenFactory; + const interchainTokenServiceAddress = contracts.InterchainTokenService?.address; + + validateParameters({ isValidAddress: { interchainTokenFactoryAddress, interchainTokenServiceAddress } }); + + const rpc = chain.rpc; + const provider = getDefaultProvider(rpc); + + printInfo('Chain', chain.name); + + const wallet = await getWallet(privateKey, provider, options); + + printInfo('Contract name', contractName); + printInfo('Contract address', interchainTokenFactoryAddress); + + const interchainTokenFactory = new Contract(interchainTokenFactoryAddress, IInterchainTokenFactory.abi, wallet); + const interchainTokenService = new Contract(interchainTokenServiceAddress, IInterchainTokenService.abi, wallet); + + const gasOptions = contractConfig?.gasOptions || chain?.gasOptions || {}; + printInfo('Gas options', JSON.stringify(gasOptions, null, 2)); + + printInfo('Action', action); + + if (prompt(`Proceed with action ${action}`, yes)) { + return; + } + + switch (action) { + case 'contractId': { + const contractId = await interchainTokenFactory.contractId(); + printInfo('InterchainTokenFactory contract ID', contractId); + + break; + } + + case 'interchainTokenSalt': { + const { chainNameHash, deployer } = options; + + const deploymentSalt = getDeploymentSalt(options); + + validateParameters({ isValidAddress: { deployer }, isKeccak256Hash: { chainNameHash } }); + + const interchainTokenSalt = await interchainTokenFactory.interchainTokenSalt(chainNameHash, deployer, deploymentSalt); + printInfo(`interchainTokenSalt for deployer ${deployer} and deployment salt: ${deploymentSalt}`, interchainTokenSalt); + + break; + } + + case 'canonicalInterchainTokenSalt': { + const { chainNameHash, tokenAddress } = options; + + validateParameters({ isValidAddress: { tokenAddress }, isKeccak256Hash: { chainNameHash } }); + + const canonicalInterchainTokenSalt = await interchainTokenFactory.canonicalInterchainTokenSalt(chainNameHash, tokenAddress); + printInfo(`canonicalInterchainTokenSalt for token address: ${tokenAddress}`, canonicalInterchainTokenSalt); + + break; + } + + case 'interchainTokenId': { + const { deployer } = options; + + const deploymentSalt = getDeploymentSalt(options); + + validateParameters({ isValidAddress: { deployer } }); + + const interchainTokenId = await interchainTokenFactory.interchainTokenId(deployer, deploymentSalt); + printInfo(`InterchainTokenId for deployer ${deployer} and deployment salt: ${deploymentSalt}`, interchainTokenId); + + break; + } + + case 'canonicalInterchainTokenId': { + const { tokenAddress } = options; + + validateParameters({ isValidAddress: { tokenAddress } }); + + const canonicalInterchainTokenId = await interchainTokenFactory.canonicalInterchainTokenId(tokenAddress); + printInfo(`canonicalInterchainTokenId for token address: ${tokenAddress}`, canonicalInterchainTokenId); + + break; + } + + case 'interchainTokenAddress': { + const { deployer } = options; + + const deploymentSalt = getDeploymentSalt(options); + + validateParameters({ isValidAddress: { deployer } }); + + const interchainTokenAddress = await interchainTokenFactory.interchainTokenAddress(deployer, deploymentSalt); + printInfo(`interchainTokenAddress for deployer ${deployer} and deployment salt: ${deploymentSalt}`, interchainTokenAddress); + + break; + } + + case 'deployInterchainToken': { + const { name, symbol, decimals, mintAmount, distributor } = options; + + const deploymentSalt = getDeploymentSalt(options); + + validateParameters({ + isNonEmptyString: { name, symbol }, + isValidAddress: { distributor }, + isValidNumber: { decimals, mintAmount }, + }); + + const tx = await interchainTokenFactory.deployInterchainToken(deploymentSalt, name, symbol, decimals, mintAmount, distributor); + + await handleTx(tx, chain, interchainTokenService, options.action, 'TokenManagerDeployed', 'InterchainTokenDeploymentStarted'); + + break; + } + + case 'deployRemoteInterchainToken': { + const { originalChain, distributor, destinationChain, gasValue } = options; + + const deploymentSalt = getDeploymentSalt(options); + + validateParameters({ + isNonEmptyString: { originalChain, destinationChain }, + isValidBytesAddress: { distributor }, + isValidNumber: { gasValue }, + }); + + const tx = await interchainTokenFactory.deployRemoteInterchainToken( + originalChain, + deploymentSalt, + distributor, + destinationChain, + gasValue, + ); + + await handleTx(tx, chain, interchainTokenService, options.action, 'TokenManagerDeployed', 'InterchainTokenDeploymentStarted'); + + break; + } + + case 'registerCanonicalInterchainToken': { + const { tokenAddress } = options; + + validateParameters({ isValidAddress: { tokenAddress } }); + + const tx = await interchainTokenFactory.registerCanonicalInterchainToken(tokenAddress); + + await handleTx(tx, chain, interchainTokenService, options.action, 'TokenManagerDeployed', 'TokenManagerDeploymentStarted'); + + break; + } + + case 'deployRemoteCanonicalInterchainToken': { + const { originalChain, tokenAddress, destinationChain, gasValue } = options; + + validateParameters({ + isValidAddress: { tokenAddress }, + isNonEmptyString: { originalChain, destinationChain }, + isValidNumber: { gasValue }, + }); + + const tx = await interchainTokenFactory.deployRemoteCanonicalInterchainToken( + originalChain, + tokenAddress, + destinationChain, + gasValue, + ); + + await handleTx(tx, chain, interchainTokenService, options.action, 'TokenManagerDeployed', 'InterchainTokenDeploymentStarted'); + + break; + } + + case 'interchainTransfer': { + const { tokenId, destinationChain, destinationAddress, amount, gasValue } = options; + + const tokenIdBytes32 = hexZeroPad(tokenId.startsWith('0x') ? tokenId : '0x' + tokenId, 32); + + validateParameters({ + isValidTokenId: { tokenId }, + isString: { destinationChain }, + isValidCalldata: { destinationAddress }, + isValidNumber: { amount, gasValue }, + }); + + const tx = await interchainTokenFactory.interchainTransfer( + tokenIdBytes32, + destinationChain, + destinationAddress, + amount, + gasValue, + ); + + if (destinationChain === '') { + const tokenAddress = await interchainTokenService.interchainTokenAddress(tokenIdBytes32); + const token = new Contract(tokenAddress, IERC20.abi, wallet); + + await handleTx(tx, chain, token, options.action, 'Transfer'); + } else { + await handleTx(tx, chain, interchainTokenFactory, options.action, 'InterchainTransferWithData'); + } + + break; + } + + case 'tokenTransferFrom': { + const { tokenId, amount } = options; + + const tokenIdBytes32 = hexZeroPad(tokenId.startsWith('0x') ? tokenId : '0x' + tokenId, 32); + + validateParameters({ isValidTokenId: { tokenId }, isValidNumber: { amount } }); + + const tokenAddress = await interchainTokenService.interchainTokenAddress(tokenIdBytes32); + const token = new Contract(tokenAddress, IERC20.abi, wallet); + + const tx = await interchainTokenFactory.tokenTransferFrom(tokenIdBytes32, amount); + + await handleTx(tx, chain, token, options.action, 'Transfer'); + + break; + } + + case 'tokenApprove': { + const { tokenId, amount } = options; + + const tokenIdBytes32 = hexZeroPad(tokenId.startsWith('0x') ? tokenId : '0x' + tokenId, 32); + + validateParameters({ isValidTokenId: { tokenId }, isValidNumber: { amount } }); + + const tokenAddress = await interchainTokenService.interchainTokenAddress(tokenIdBytes32); + const token = new Contract(tokenAddress, IERC20.abi, wallet); + + const tx = await interchainTokenFactory.tokenApprove(tokenIdBytes32, amount); + + await handleTx(tx, chain, token, options.action, 'Approval'); + + break; + } + + default: { + throw new Error(`Unknown action ${action}`); + } + } +} + +async function main(options) { + await mainProcessor(options, processCommand); +} + +if (require.main === module) { + const program = new Command(); + + program.name('InterchainTokenFactory').description('Script to perform interchain token factory commands'); + + addExtendedOptions(program, { address: true, salt: true }); + + program.addOption( + new Option('--action ', 'interchain token factory action') + .choices([ + 'contractId', + 'interchainTokenSalt', + 'canonicalInterchainTokenSalt', + 'interchainTokenId', + 'canonicalInterchainTokenId', + 'interchainTokenAddress', + 'deployInterchainToken', + 'deployRemoteInterchainToken', + 'registerCanonicalInterchainToken', + 'deployRemoteCanonicalInterchainToken', + 'interchainTransfer', + 'tokenTransferFrom', + 'tokenApprove', + ]) + .makeOptionMandatory(true), + ); + + program.addOption(new Option('--tokenId ', 'ID of the token')); + program.addOption(new Option('--sender ', 'TokenManager deployer address')); + program.addOption(new Option('--chainNameHash ', 'chain name hash')); + program.addOption(new Option('--deployer ', 'deployer address')); + program.addOption(new Option('--tokenAddress ', 'token address')); + program.addOption(new Option('--name ', 'token name')); + program.addOption(new Option('--symbol ', 'token symbol')); + program.addOption(new Option('--decimals ', 'token decimals')); + program.addOption(new Option('--distributor ', 'token distributor')); + program.addOption(new Option('--mintAmount ', 'mint amount').default(0)); + program.addOption(new Option('--originalChain ', 'original chain')); + program.addOption(new Option('--destinationChain ', 'destination chain')); + program.addOption(new Option('--destinationAddress ', 'destination address')); + program.addOption(new Option('--gasValue ', 'gas value')); + program.addOption(new Option('--amount ', 'token amount')); + program.addOption(new Option('--rawSalt ', 'raw deployment salt').env('RAW_SALT')); + + program.action((options) => { + main(options); + }); + + program.parse(); +} diff --git a/evm/its.js b/evm/its.js new file mode 100644 index 00000000..a2b703af --- /dev/null +++ b/evm/its.js @@ -0,0 +1,500 @@ +'use strict'; + +require('dotenv').config(); + +const { ethers } = require('hardhat'); +const { + getDefaultProvider, + utils: { hexZeroPad }, + Contract, +} = ethers; +const { Command, Option } = require('commander'); +const { + printInfo, + prompt, + printWarn, + printWalletInfo, + wasEventEmitted, + mainProcessor, + validateParameters, + getContractJSON, + isValidTokenId, +} = require('./utils'); +const { getWallet } = require('./sign-utils'); +const IInterchainTokenService = getContractJSON('IInterchainTokenService'); +const { addExtendedOptions } = require('./cli-utils'); +const { getSaltFromKey } = require('@axelar-network/axelar-gmp-sdk-solidity/scripts/utils'); +const tokenManagerImplementations = { + MINT_BURN: 0, + MINT_BURN_FROM: 1, + LOCK_UNLOCK: 2, + LOCK_UNLOCK_FEE: 3, +}; + +function getDeploymentSalt(options) { + const { rawSalt, salt } = options; + + if (rawSalt) { + validateParameters({ isKeccak256Hash: { rawSalt } }); + return rawSalt; + } + + validateParameters({ isString: { salt } }); + return getSaltFromKey(salt); +} + +async function handleTx(tx, chain, contract, action, firstEvent, secondEvent) { + printInfo(`${action} tx`, tx.hash); + + const receipt = await tx.wait(chain.confirmations); + + const eventEmitted = + (firstEvent ? wasEventEmitted(receipt, contract, firstEvent) : true) || + (secondEvent ? wasEventEmitted(receipt, contract, secondEvent) : false); + + if (!eventEmitted) { + printWarn('Event not emitted in receipt.'); + } +} + +async function processCommand(config, chain, options) { + const { privateKey, address, action, yes } = options; + + const contracts = chain.contracts; + const contractName = 'InterchainTokenService'; + const contractConfig = contracts.InterchainTokenService; + + const interchainTokenServiceAddress = address || contracts.InterchainTokenService?.address; + + validateParameters({ isValidAddress: { interchainTokenServiceAddress } }); + + const rpc = chain.rpc; + const provider = getDefaultProvider(rpc); + + printInfo('Chain', chain.name); + + const wallet = await getWallet(privateKey, provider, options); + const { address: walletAddress } = await printWalletInfo(wallet, options); + + printInfo('Contract name', contractName); + printInfo('Contract address', interchainTokenServiceAddress); + + const interchainTokenService = new Contract(interchainTokenServiceAddress, IInterchainTokenService.abi, wallet); + + const gasOptions = contractConfig?.gasOptions || chain?.gasOptions || {}; + printInfo('Gas options', JSON.stringify(gasOptions, null, 2)); + + printInfo('Action', action); + + if (prompt(`Proceed with action ${action}`, yes)) { + return; + } + + const tokenId = options.tokenId; + + switch (action) { + case 'contractId': { + const contractId = await interchainTokenService.contractId(); + printInfo('InterchainTokenService contract ID', contractId); + + break; + } + + case 'tokenManagerAddress': { + validateParameters({ isValidTokenId: { tokenId } }); + + const tokenIdBytes32 = hexZeroPad(tokenId.startsWith('0x') ? tokenId : '0x' + tokenId, 32); + + const tokenManagerAddress = await interchainTokenService.tokenManagerAddress(tokenIdBytes32); + printInfo(`TokenManager address for tokenId: ${tokenId}`, tokenManagerAddress); + + try { + await interchainTokenService.validTokenManagerAddress(tokenIdBytes32); + printInfo(`TokenManager for tokenId: ${tokenId} exists at address:`, tokenManagerAddress); + } catch (error) { + printInfo(`TokenManager for tokenId: ${tokenId} does not yet exist.`); + } + + break; + } + + case 'interchainTokenAddress': { + validateParameters({ isValidTokenId: { tokenId } }); + + const tokenIdBytes32 = hexZeroPad(tokenId.startsWith('0x') ? tokenId : '0x' + tokenId, 32); + + const interchainTokenAddress = await interchainTokenService.interchainTokenAddress(tokenIdBytes32); + printInfo(`InterchainToken address for tokenId: ${tokenId}`, interchainTokenAddress); + + break; + } + + case 'interchainTokenId': { + const { sender } = options; + + const deploymentSalt = getDeploymentSalt(options); + + validateParameters({ isValidAddress: { sender } }); + + const interchainTokenId = await interchainTokenService.interchainTokenId(sender, deploymentSalt); + printInfo(`InterchainTokenId for sender ${sender} and deployment salt: ${deploymentSalt}`, interchainTokenId); + + break; + } + + case 'tokenManagerImplementation': { + const type = options.type; + + const tokenManagerImplementation = await interchainTokenService.tokenManagerImplementation(tokenManagerImplementations[type]); + printInfo(`${type} TokenManager implementation address`, tokenManagerImplementation); + + break; + } + + case 'flowLimit': { + validateParameters({ isValidTokenId: { tokenId } }); + + const tokenIdBytes32 = hexZeroPad(tokenId.startsWith('0x') ? tokenId : '0x' + tokenId, 32); + + const flowLimit = await interchainTokenService.flowLimit(tokenIdBytes32); + printInfo(`Flow limit for TokenManager with tokenId ${tokenId}`, flowLimit); + + break; + } + + case 'flowOutAmount': { + validateParameters({ isValidTokenId: { tokenId } }); + + const tokenIdBytes32 = hexZeroPad(tokenId.startsWith('0x') ? tokenId : '0x' + tokenId, 32); + + const flowOutAmount = await interchainTokenService.flowOutAmount(tokenIdBytes32); + printInfo(`Flow out amount for TokenManager with tokenId ${tokenId}`, flowOutAmount); + + break; + } + + case 'flowInAmount': { + validateParameters({ isValidTokenId: { tokenId } }); + + const tokenIdBytes32 = hexZeroPad(tokenId.startsWith('0x') ? tokenId : '0x' + tokenId, 32); + + const flowInAmount = await interchainTokenService.flowInAmount(tokenIdBytes32); + printInfo(`Flow out amount for TokenManager with tokenId ${tokenId}`, flowInAmount); + + break; + } + + case 'deployTokenManager': { + const { destinationChain, type, params, gasValue } = options; + + const deploymentSalt = getDeploymentSalt(options); + + validateParameters({ + isString: { destinationChain }, + isValidCalldata: { params }, + isValidNumber: { gasValue }, + }); + + const tx = await interchainTokenService.deployTokenManager( + deploymentSalt, + destinationChain, + tokenManagerImplementations[type], + params, + gasValue, + ); + + await handleTx(tx, chain, interchainTokenService, options.action, 'TokenManagerDeployed', 'TokenManagerDeploymentStarted'); + + break; + } + + case 'deployInterchainToken': { + const { destinationChain, name, symbol, decimals, distributor, gasValue } = options; + + const deploymentSalt = getDeploymentSalt(options); + + validateParameters({ + isNonEmptyString: { name, symbol }, + isString: { destinationChain }, + isValidBytesAddress: { distributor }, + isValidNumber: { decimals, gasValue }, + }); + + const tx = await interchainTokenService.deployInterchainToken( + deploymentSalt, + destinationChain, + name, + symbol, + decimals, + distributor, + gasValue, + ); + + await handleTx(tx, chain, interchainTokenService, options.action, 'TokenManagerDeployed', 'InterchainTokenDeploymentStarted'); + + break; + } + + case 'contractCallValue': { + const { sourceChain, sourceAddress, payload } = options; + + validateParameters({ isNonEmptyString: { sourceChain, sourceAddress } }); + + const isTrustedAddress = await interchainTokenService.isTrustedAddress(sourceChain, sourceAddress); + + if (!isTrustedAddress) { + throw new Error('Invalid remote service.'); + } + + validateParameters({ isValidCalldata: { payload } }); + + const [tokenAddress, tokenAmount] = await interchainTokenService.contractCallValue(sourceChain, sourceAddress, payload); + printInfo(`Amount of tokens with address ${tokenAddress} that the call is worth:`, tokenAmount); + + break; + } + + case 'expressExecute': { + const { commandID, sourceChain, sourceAddress, payload } = options; + + validateParameters({ + isKeccak256Hash: { commandID }, + isNonEmptyString: { sourceChain, sourceAddress }, + isValidCalldata: { payload }, + }); + + const tx = await interchainTokenService.expressExecute(commandID, sourceChain, sourceAddress, payload); + + await handleTx(tx, chain, interchainTokenService, options.action, 'ExpressExecuted'); + + break; + } + + case 'interchainTransfer': { + const { destinationChain, destinationAddress, amount, metadata } = options; + + validateParameters({ + isValidTokenId: { tokenId }, + isNonEmptyString: { destinationChain, destinationAddress }, + isValidNumber: { amount }, + isValidCalldata: { metadata }, + }); + + const tokenIdBytes32 = hexZeroPad(tokenId.startsWith('0x') ? tokenId : '0x' + tokenId, 32); + + const tx = await interchainTokenService.interchainTransfer( + tokenIdBytes32, + destinationChain, + destinationAddress, + amount, + metadata, + ); + + await handleTx(tx, chain, interchainTokenService, options.action, 'InterchainTransfer', 'InterchainTransferWithData'); + + break; + } + + case 'callContractWithInterchainToken': { + const { destinationChain, destinationAddress, amount, data } = options; + + validateParameters({ + isValidTokenId: { tokenId }, + isNonEmptyString: { destinationChain, destinationAddress }, + isValidNumber: { amount }, + isValidCalldata: { data }, + }); + + const tokenIdBytes32 = hexZeroPad(tokenId.startsWith('0x') ? tokenId : '0x' + tokenId, 32); + + const tx = await interchainTokenService.callContractWithInterchainToken( + tokenIdBytes32, + destinationChain, + destinationAddress, + amount, + data, + ); + + await handleTx(tx, chain, interchainTokenService, options.action, 'InterchainTransfer', 'InterchainTransferWithData'); + + break; + } + + case 'setFlowLimits': { + const { tokenIds, flowLimits } = options; + const tokenIdsBytes32 = []; + + for (const tokenId of tokenIds) { + if (!isValidTokenId(tokenId)) { + throw new Error(`Invalid tokenId value: ${tokenId}`); + } + + const tokenIdBytes32 = hexZeroPad(tokenId.startsWith('0x') ? tokenId : '0x' + tokenId, 32); + tokenIdsBytes32.push(tokenIdBytes32); + } + + validateParameters({ isNumberArray: { flowLimits } }); + + const tx = await interchainTokenService.setFlowLimits(tokenIdsBytes32, flowLimits); + + await handleTx(tx, chain, interchainTokenService, options.action, 'FlowLimitSet'); + + break; + } + + case 'setTrustedAddress': { + const owner = await interchainTokenService.owner(); + + if (owner.toLowerCase() !== walletAddress.toLowerCase()) { + throw new Error(`${action} can only be performed by contract owner: ${owner}`); + } + + const { trustedChain, trustedAddress } = options; + + validateParameters({ isNonEmptyString: { trustedChain, trustedAddress } }); + + const tx = await interchainTokenService.setTrustedAddress(trustedChain, trustedAddress); + + await handleTx(tx, chain, interchainTokenService, options.action, 'TrustedAddressSet'); + + break; + } + + case 'removeTrustedAddress': { + const owner = await interchainTokenService.owner(); + + if (owner.toLowerCase() !== walletAddress.toLowerCase()) { + throw new Error(`${action} can only be performed by contract owner: ${owner}`); + } + + const trustedChain = options.trustedChain; + + validateParameters({ isNonEmptyString: { trustedChain } }); + + const tx = await interchainTokenService.removeTrustedAddress(trustedChain); + + await handleTx(tx, chain, interchainTokenService, options.action, 'TrustedAddressRemoved'); + + break; + } + + case 'setPauseStatus': { + const owner = await interchainTokenService.owner(); + + if (owner.toLowerCase() !== walletAddress.toLowerCase()) { + throw new Error(`${action} can only be performed by contract owner: ${owner}`); + } + + const pauseStatus = options.pauseStatus; + + const tx = await interchainTokenService.setPauseStatus(pauseStatus); + + await handleTx(tx, chain, interchainTokenService, options.action, 'Paused', 'Unpaused'); + + break; + } + + case 'execute': { + const { commandID, sourceChain, sourceAddress, payload } = options; + + validateParameters({ isKeccak256Hash: { commandID }, isNonEmptyString: { sourceChain, sourceAddress } }); + + const isTrustedAddress = await interchainTokenService.isTrustedAddress(sourceChain, sourceAddress); + + if (!isTrustedAddress) { + throw new Error('Invalid remote service.'); + } + + validateParameters({ isValidCalldata: { payload } }); + + const tx = await interchainTokenService.execute(commandID, sourceChain, sourceAddress, payload); + + await handleTx(tx, chain, interchainTokenService, options.action); + + break; + } + + default: { + throw new Error(`Unknown action ${action}`); + } + } +} + +async function main(options) { + await mainProcessor(options, processCommand); +} + +if (require.main === module) { + const program = new Command(); + + program.name('ITS').description('Script to perform ITS commands'); + + addExtendedOptions(program, { address: true, salt: true }); + + program.addOption( + new Option('--action ', 'ITS action') + .choices([ + 'contractId', + 'tokenManagerAddress', + 'tokenAddress', + 'interchainTokenAddress', + 'interchainTokenId', + 'tokenManagerImplementation', + 'flowLimit', + 'flowOutAmount', + 'flowInAmount', + 'deployTokenManager', + 'deployInterchainToken', + 'contractCallValue', + 'expressExecute', + 'interchainTransfer', + 'callContractWithInterchainToken', + 'setFlowLimits', + 'setTrustedAddress', + 'removeTrustedAddress', + 'setPauseStatus', + 'execute', + ]) + .makeOptionMandatory(true), + ); + + program.addOption(new Option('--commandID ', 'execute command ID')); + program.addOption(new Option('--tokenId ', 'ID of the token')); + program.addOption(new Option('--sender ', 'TokenManager deployer address')); + program.addOption( + new Option('--type ', 'TokenManager implementation type').choices([ + 'MINT_BURN', + 'MINT_BURN_FROM', + 'LOCK_UNLOCK', + 'LOCK_UNLOCK_FEE', + ]), + ); + program.addOption(new Option('--destinationChain ', 'destination chain')); + program.addOption(new Option('--destinationAddress ', 'destination address')); + program.addOption(new Option('--params ', 'params for TokenManager deployment')); + program.addOption(new Option('--gasValue ', 'gas value')); + program.addOption(new Option('--name ', 'token name')); + program.addOption(new Option('--symbol ', 'token symbol')); + program.addOption(new Option('--decimals ', 'token decimals')); + program.addOption(new Option('--distributor ', 'token distributor')); + program.addOption(new Option('--sourceChain ', 'source chain')); + program.addOption(new Option('--sourceAddress ', 'source address')); + program.addOption(new Option('--payload ', 'payload')); + program.addOption(new Option('--amount ', 'token amount')); + program.addOption(new Option('--metadata ', 'token transfer metadata')); + program.addOption(new Option('--data ', 'token transfer data')); + program.addOption(new Option('--tokenIds ', 'tokenId array')); + program.addOption(new Option('--flowLimits ', 'flow limit array')); + program.addOption(new Option('--trustedChain ', 'chain name for trusted addresses')); + program.addOption(new Option('--trustedAddress ', 'trusted address')); + program.addOption(new Option('--pauseStatus ', 'pause status').choices(['true', 'false'])); + program.addOption(new Option('--rawSalt ', 'raw deployment salt').env('RAW_SALT')); + + program.action((options) => { + main(options); + }); + + program.parse(); +} + +module.exports = { getDeploymentSalt, handleTx }; diff --git a/evm/multisig.js b/evm/multisig.js index d6118800..dbdf5a70 100644 --- a/evm/multisig.js +++ b/evm/multisig.js @@ -16,7 +16,7 @@ const { isNumber, isValidCalldata, printWarn, - isStringArray, + isNonEmptyStringArray, isNumberArray, isValidAddress, mainProcessor, @@ -151,7 +151,7 @@ async function processCommand(_, chain, options) { const symbolsArray = JSON.parse(symbols); const limitsArray = JSON.parse(limits); - if (!isStringArray(symbolsArray)) { + if (!isNonEmptyStringArray(symbolsArray)) { throw new Error(`Invalid token symbols: ${symbols})}`); } diff --git a/evm/utils.js b/evm/utils.js index fd9d75bf..9bc48067 100644 --- a/evm/utils.js +++ b/evm/utils.js @@ -191,10 +191,14 @@ const httpGet = (url) => { }); }; -const isString = (arg) => { +const isNonEmptyString = (arg) => { return typeof arg === 'string' && arg !== ''; }; +const isString = (arg) => { + return typeof arg === 'string'; +}; + const isNumber = (arg) => { return Number.isInteger(arg); }; @@ -221,7 +225,7 @@ const isNumberArray = (arr) => { return true; }; -const isStringArray = (arr) => { +const isNonEmptyStringArray = (arr) => { if (!Array.isArray(arr)) { return false; } @@ -293,6 +297,110 @@ function isValidCalldata(input) { return hexPattern.test(input.slice(2)); } +function isValidBytesAddress(input) { + const addressRegex = /^0x[a-fA-F0-9]{40}$/; + return addressRegex.test(input); +} + +const isContract = async (address, provider) => { + const code = await provider.getCode(address); + return code && code !== '0x'; +}; + +function isValidAddress(address, allowZeroAddress) { + if (!allowZeroAddress && address === AddressZero) { + return false; + } + + return isAddress(address); +} + +/** + * Validate if the input string matches the time format YYYY-MM-DDTHH:mm:ss + * + * @param {string} timeString - The input time string. + * @return {boolean} - Returns true if the format matches, false otherwise. + */ +function isValidTimeFormat(timeString) { + const regex = /^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|1\d|2\d|3[01])T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d$/; + + if (timeString === '0') { + return true; + } + + return regex.test(timeString); +} + +// Validate if the input privateKey is correct +function isValidPrivateKey(privateKey) { + // Check if it's a valid hexadecimal string + if (!privateKey?.startsWith('0x')) { + privateKey = '0x' + privateKey; + } + + if (!isHexString(privateKey) || privateKey.length !== 66) { + return false; + } + + return true; +} + +function isValidTokenId(input) { + if (!input?.startsWith('0x')) { + return false; + } + + const hexPattern = /^[0-9a-fA-F]+$/; + + if (!hexPattern.test(input.slice(2))) { + return false; + } + + const minValue = BigInt('0x00'); + const maxValue = BigInt('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF'); + const numericValue = BigInt(input); + + return numericValue >= minValue && numericValue <= maxValue; +} + +const validationFunctions = { + isNonEmptyString, + isNumber, + isValidNumber, + isValidDecimal, + isNumberArray, + isString, + isNonEmptyStringArray, + isAddressArray, + isKeccak256Hash, + isValidCalldata, + isValidBytesAddress, + isValidTimeFormat, + isContract, + isValidAddress, + isValidPrivateKey, + isValidTokenId, +}; + +function validateParameters(parameters) { + for (const [validatorFunctionString, paramsObj] of Object.entries(parameters)) { + const validatorFunction = validationFunctions[validatorFunctionString]; + + if (typeof validatorFunction !== 'function') { + throw new Error(`Validator function ${validatorFunction} is not defined`); + } + + for (const paramKey of Object.keys(paramsObj)) { + const paramValue = paramsObj[paramKey]; + const isValid = validatorFunction(paramValue); + + if (!isValid) { + throw new Error(`Input validation failed for ${validatorFunctionString} with parameter ${paramKey}: ${paramValue}`); + } + } + } +} + /** * Parses the input string into an array of arguments, recognizing and converting * to the following types: boolean, number, array, and string. @@ -334,7 +442,7 @@ const parseArgs = (args) => { async function getBytecodeHash(contractObject, chain = '', provider = null) { let bytecode; - if (isString(contractObject)) { + if (isNonEmptyString(contractObject)) { if (provider === null) { throw new Error('Provider must be provided for chain'); } @@ -408,7 +516,7 @@ const getDeployedAddress = async (deployer, deployMethod, options = {}) => { const deployerContract = options.deployerContract; - if (!isString(deployerContract)) { + if (!isNonEmptyString(deployerContract)) { throw new Error('Deployer contract address was not provided'); } @@ -431,7 +539,7 @@ const getDeployedAddress = async (deployer, deployMethod, options = {}) => { case 'create3': { const deployerContract = options.deployerContract; - if (!isString(deployerContract)) { + if (!isNonEmptyString(deployerContract)) { throw new Error('Deployer contract address was not provided'); } @@ -568,11 +676,11 @@ const deployContract = async ( } case 'create2': { - if (!isString(deployOptions.deployerContract)) { + if (!isNonEmptyString(deployOptions.deployerContract)) { throw new Error('Deployer contract address was not provided'); } - if (!isString(deployOptions.salt)) { + if (!isNonEmptyString(deployOptions.salt)) { throw new Error('Salt was not provided'); } @@ -591,11 +699,11 @@ const deployContract = async ( } case 'create3': { - if (!isString(deployOptions.deployerContract)) { + if (!isNonEmptyString(deployOptions.deployerContract)) { throw new Error('Deployer contract address was not provided'); } - if (!isString(deployOptions.salt)) { + if (!isNonEmptyString(deployOptions.salt)) { throw new Error('Salt was not provided'); } @@ -619,36 +727,6 @@ const deployContract = async ( } }; -/** - * Validate if the input string matches the time format YYYY-MM-DDTHH:mm:ss - * - * @param {string} timeString - The input time string. - * @return {boolean} - Returns true if the format matches, false otherwise. - */ -function isValidTimeFormat(timeString) { - const regex = /^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|1\d|2\d|3[01])T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d$/; - - if (timeString === '0') { - return true; - } - - return regex.test(timeString); -} - -// Validate if the input privateKey is correct -function isValidPrivateKey(privateKey) { - // Check if it's a valid hexadecimal string - if (!privateKey.startsWith('0x')) { - privateKey = '0x' + privateKey; - } - - if (!isHexString(privateKey) || privateKey.length !== 66) { - return false; - } - - return true; -} - const dateToEta = (utcTimeString) => { if (utcTimeString === '0') { return 0; @@ -687,19 +765,6 @@ function wasEventEmitted(receipt, contract, eventName) { return receipt.logs.some((log) => log.topics[0] === event.topics[0]); } -const isContract = async (address, provider) => { - const code = await provider.getCode(address); - return code && code !== '0x'; -}; - -function isValidAddress(address, allowZeroAddress) { - if (!allowZeroAddress && address === AddressZero) { - return false; - } - - return isAddress(address); -} - function copyObject(obj) { return JSON.parse(JSON.stringify(obj)); } @@ -872,14 +937,17 @@ module.exports = { predictAddressCreate, getDeployedAddress, isString, + isNonEmptyString, isNumber, isValidNumber, isValidDecimal, isNumberArray, - isStringArray, + isNonEmptyStringArray, isAddressArray, isKeccak256Hash, isValidCalldata, + isValidBytesAddress, + validateParameters, parseArgs, getProxy, getEVMBatch, @@ -897,6 +965,7 @@ module.exports = { isContract, isValidAddress, isValidPrivateKey, + isValidTokenId, verifyContract, prompt, mainProcessor,