diff --git a/.github/workflows/test-sui.yaml b/.github/workflows/test-sui.yaml index 9bdef97f..4dd2cf67 100644 --- a/.github/workflows/test-sui.yaml +++ b/.github/workflows/test-sui.yaml @@ -169,6 +169,18 @@ jobs: run: node sui/gas-service.js collectGas --amount 0.1 ###### Command: Gateway ###### + - name: Gateway Pause + run: node sui/contract.js pause AxelarGateway -y + + - name: Gateway Unpause + run: node sui/contract.js unpause AxelarGateway -y + + - name: Gateway Pause All + run: node sui/contract.js pause AxelarGateway --functions all -y + + - name: Gateway Unpause + run: node sui/contract.js unpause AxelarGateway -y + - name: Gateway Approve run: node sui/gateway.js approve --proof wallet ethereum 0x32034b47cb29d162d9d803cc405356f4ac0ec07fe847ace431385fe8acf3e6e5-2 0x4F4495243837681061C4743b74B3eEdf548D56A5 0x6ce0d81b412abca2770eddb1549c9fcff721889c3aab1203dc93866db22ecc4b 0x56570de287d73cd1cb6092bb8fdee6173974955fdef345ae579ee9f475ea7432 @@ -190,8 +202,20 @@ jobs: node sui/gateway.js approve --proof wallet ethereum 0x32034b47cb29d162d9d803cc405356f4ac0ec07fe847ace431385fe8acf3e6e5-3 0x4F4495243837681061C4743b74B3eEdf548D56A5 $channel_id 0x56570de287d73cd1cb6092bb8fdee6173974955fdef345ae579ee9f475ea7432 node sui/gmp.js execute ethereum 0x32034b47cb29d162d9d803cc405356f4ac0ec07fe847ace431385fe8acf3e6e5-3 0x4F4495243837681061C4743b74B3eEdf548D56A5 0x1234 - ###### Command: ITS Example ###### + ###### Command: ITS ###### + - name: ITS Pause + run: node sui/contract.js pause InterchainTokenService -y + - name: ITS Unpause + run: node sui/contract.js unpause InterchainTokenService -y + + - name: ITS Pause + run: node sui/contract.js pause InterchainTokenService --functions all -y + + - name: ITS Unpause + run: node sui/contract.js unpause InterchainTokenService -y + + ###### Command: ITS Example ###### - name: Prepare ITS Example Parameters run: | echo "sourceChain=Ethereum" >> $GITHUB_ENV diff --git a/sui/contract.js b/sui/contract.js new file mode 100644 index 00000000..e11153a0 --- /dev/null +++ b/sui/contract.js @@ -0,0 +1,260 @@ +const { Command, Option } = require('commander'); +const { TxBuilder } = require('@axelar-network/axelar-cgp-sui'); +const { loadConfig, saveConfig, getChainConfig, printInfo } = require('../common/utils'); +const { + addBaseOptions, + addOptionsToCommands, + getWallet, + printWalletInfo, + broadcastFromTxBuilder, + getAllowedFunctions, +} = require('./utils'); + +const SPECIAL_PAUSE_FUNCTION_TAGS = { + ALL: 'all', // All EVM chains that have InterchainTokenService deployed + DEFAULT: 'default', +}; + +const SPECIAL_UNPAUSE_FUNCTION_TAGS = { + DISALLOWED: 'disallowed', // All EVM chains that have InterchainTokenService deployed + DEFAULT: 'default', +}; + +const CONTRACT_INFO = { + AxelarGateway: { + singletonName: 'Gateway', + moduleName: 'gateway', + defaultFunctions: { + versions: [0, 0], + functionNames: ['approve_messages', 'rotate_signers'], + }, + }, + InterchainTokenService: { + singletonName: 'InterchainTokenService', + moduleName: 'interchain_token_service', + defaultFunctions: { + versions: [0, 0, 0, 0, 0, 0, 0, 0, 0], + functionNames: [ + 'register_coin', + 'deploy_remote_interchain_token', + 'send_interchain_transfer', + 'receive_interchain_transfer', + 'receive_interchain_transfer_with_data', + 'receive_deploy_interchain_token', + 'mint_as_distributor', + 'mint_to_as_distributor', + 'burn_as_distributor', + ], + }, + }, +}; + +function getVariablesForPackage(chain, packageName) { + const contractConfig = chain.contracts[packageName]; + const info = CONTRACT_INFO[packageName]; + const defaultFunctions = info.defaultFunctions; + const version = Math.max(...Object.keys(contractConfig.versions).map((version) => Number(version))); + defaultFunctions.versions.fill(version); + return { + packageId: contractConfig.address, + singletonId: contractConfig.objects[info.singletonName], + versionedId: contractConfig.objects[info.singletonName + 'v0'], + ownerCapId: contractConfig.objects.OwnerCap, + moduleName: info.moduleName, + defaultFunctions: info.defaultFunctions, + contract: contractConfig, + }; +} + +async function allowFunctions(keypair, client, packageId, moduleName, singletonId, ownerCapId, versions, functionNames, options) { + if (versions.length !== functionNames.length) throw new Error('Versions and Function Names must have a matching length'); + + const builder = new TxBuilder(client); + + for (const i in versions) { + await builder.moveCall({ + target: `${packageId}::${moduleName}::allow_function`, + arguments: [singletonId, ownerCapId, versions[i], functionNames[i]], + }); + } + + await broadcastFromTxBuilder(builder, keypair, 'Allow Functions', options); +} + +async function disallowFunctions(keypair, client, packageId, moduleName, singletonId, ownerCapId, versions, functionNames, options) { + if (versions.length !== functionNames.length) throw new Error('Versions and Function Names must have a matching length'); + + const builder = new TxBuilder(client); + + for (const i in versions) { + await builder.moveCall({ + target: `${packageId}::${moduleName}::disallow_function`, + arguments: [singletonId, ownerCapId, versions[i], functionNames[i]], + }); + } + + await broadcastFromTxBuilder(builder, keypair, 'Disallow Functions', options); +} + +async function pause(keypair, client, chain, args, options) { + const [packageName] = args; + const functions = options.functions; + + const { packageId, singletonId, versionedId, ownerCapId, moduleName, defaultFunctions, contract } = getVariablesForPackage( + chain, + packageName, + ); + + let versionsArg = []; + let allowedFunctionsArg = []; + + if (functions === SPECIAL_PAUSE_FUNCTION_TAGS.ALL) { + const allowedFunctionsArray = await getAllowedFunctions(client, versionedId); + + for (const version in allowedFunctionsArray) { + if (options.version !== 'all' && options.version !== version) { + continue; + } + + let allowedFunctions = allowedFunctionsArray[version]; + + // Do not dissalow `allow_function` because that locks the gateway forever. + if (Number(version) === allowedFunctionsArray.length - 1) { + allowedFunctions = allowedFunctions.filter( + (allowedFunction) => allowedFunction !== 'allow_function' && allowedFunction !== 'disallow_function', + ); + } + + printInfo(`Functions that will be disallowed for version ${version}`, allowedFunctions); + + versionsArg = versionsArg.concat(new Array(allowedFunctions.length).fill(Number(version))); + allowedFunctionsArg = allowedFunctionsArg.concat(allowedFunctions); + } + } else if (functions === SPECIAL_PAUSE_FUNCTION_TAGS.DEFAULT) { + versionsArg = defaultFunctions.versions; + allowedFunctionsArg = defaultFunctions.functionNames; + } else if (options.version !== 'all') { + allowedFunctionsArg = functions.split(','); + versionsArg = allowedFunctionsArg.map(() => Number(options.version)); + } else { + throw new Error('Need to specify a version if providing specific functions.'); + } + + if (!contract.disallowedFunctions) { + contract.disallowedFunctions = { + versions: [], + functionNames: [], + }; + } + + contract.disallowedFunctions.versions = contract.disallowedFunctions.versions.concat(versionsArg); + contract.disallowedFunctions.functionNames = contract.disallowedFunctions.functionNames.concat(allowedFunctionsArg); + + return await disallowFunctions( + keypair, + client, + packageId, + moduleName, + singletonId, + ownerCapId, + versionsArg, + allowedFunctionsArg, + options, + ); +} + +async function unpause(keypair, client, chain, args, options) { + const [packageName] = args; + const functions = options.functions; + const { packageId, singletonId, ownerCapId, moduleName, defaultFunctions, contract } = getVariablesForPackage(chain, packageName); + + let versionsArg = []; + let allowedFunctionsArg = []; + + if (functions === SPECIAL_UNPAUSE_FUNCTION_TAGS.DISALLOWED) { + versionsArg = contract.disallowedFunctions.versions.slice(); + allowedFunctionsArg = contract.disallowedFunctions.functionNames.slice(); + } else if (functions === SPECIAL_UNPAUSE_FUNCTION_TAGS.DEFAULT) { + versionsArg = defaultFunctions.versions; + allowedFunctionsArg = defaultFunctions.functionNames; + } else if (options.version !== 'all') { + allowedFunctionsArg = functions.split(','); + versionsArg = allowedFunctionsArg.map(() => Number(options.version)); + } else { + throw new Error('Need to specify a version if providing specific functions.'); + } + + if (contract.disallowedFunctions) { + for (let i = contract.disallowedFunctions.versions.length - 1; i >= 0; i--) { + const version = contract.disallowedFunctions.versions[i]; + const functionName = contract.disallowedFunctions.functionNames[i]; + + for (let j = 0; j < versionsArg.length; j++) { + if (version === versionsArg[j] && functionName === allowedFunctionsArg[j]) { + contract.disallowedFunctions.versions.splice(i, 1); + contract.disallowedFunctions.functionNames.splice(i, 1); + break; + } + } + } + } + + return await allowFunctions(keypair, client, packageId, moduleName, singletonId, ownerCapId, versionsArg, allowedFunctionsArg, options); +} + +async function processCommand(command, chain, args, options) { + const [keypair, client] = getWallet(chain, options); + + await printWalletInfo(keypair, client, chain, options); + + await command(keypair, client, chain, args, options); +} + +async function mainProcessor(command, options, args, processor) { + const config = loadConfig(options.env); + const chain = getChainConfig(config, options.chainName); + await processor(command, chain, args, options); + saveConfig(config, options.env); +} + +if (require.main === module) { + const program = new Command(); + program.name('Pause').description('SUI Pause scripts'); + + const pauseProgram = new Command() + .name('pause') + .description('Pause') + .command('pause ') + .addOption( + new Option( + '--functions ', + 'The functions to allow. Use use "default" for the default functions, "all" for all functions except the most recent "allow_function" and a comma separated list for custom pausing.', + ).default('default'), + ) + .addOption(new Option('--version, ', 'The version to pause. Use all to pause all versions').default('all')) + .action((packageName, options) => { + mainProcessor(pause, options, [packageName], processCommand); + }); + + const unpauseProgram = new Command() + .name('unpause') + .description('Unpause') + .command('unpause ') + .addOption( + new Option( + '--functions, ', + 'The functions to pause. Use "disallowed" for previously disallowed functions, "default" for the default functions and a comma separated list for custom pausing.', + ).default('disallowed'), + ) + .addOption(new Option('--version, ', 'The version to pause. Use all to pause all versions').default('all')) + .action((packageName, options) => { + mainProcessor(unpause, options, [packageName], processCommand); + }); + + program.addCommand(pauseProgram); + program.addCommand(unpauseProgram); + + addOptionsToCommands(program, addBaseOptions, { offline: true }); + + program.parse(); +} diff --git a/sui/gateway.js b/sui/gateway.js index 97f64a16..c0095606 100644 --- a/sui/gateway.js +++ b/sui/gateway.js @@ -282,68 +282,6 @@ async function rotate(keypair, client, config, chain, contractConfig, args, opti }; } -async function allowFunctions(keypair, client, config, chain, contractConfig, args, options) { - const packageId = contractConfig.address; - - const [versionsArg, functionNamesArg] = args; - - const versions = versionsArg.split(','); - const functionNames = functionNamesArg.split(','); - - if (versions.length !== functionNames.length) throw new Error('Versions and Function Names must have a matching length'); - - const tx = new Transaction(); - console.log(contractConfig.objects); - - for (const i in versions) { - tx.moveCall({ - target: `${packageId}::gateway::allow_function`, - arguments: [ - tx.object(contractConfig.objects.Gateway), - tx.object(contractConfig.objects.OwnerCap), - tx.pure.u64(versions[i]), - tx.pure.string(functionNames[i]), - ], - }); - } - - return { - tx, - message: 'Allow Functions', - }; -} - -async function disallowFunctions(keypair, client, config, chain, contractConfig, args, options) { - const packageId = contractConfig.address; - - const [versionsArg, functionNamesArg] = args; - - const versions = versionsArg.split(','); - const functionNames = functionNamesArg.split(','); - - if (versions.length !== functionNames.length) throw new Error('Versions and Function Names must have a matching length'); - - const tx = new Transaction(); - console.log(contractConfig.objects); - - for (const i in versions) { - tx.moveCall({ - target: `${packageId}::gateway::disallow_function`, - arguments: [ - tx.object(contractConfig.objects.Gateway), - tx.object(contractConfig.objects.OwnerCap), - tx.pure.u64(versions[i]), - tx.pure.string(functionNames[i]), - ], - }); - } - - return { - tx, - message: 'Disallow Functions', - }; -} - async function checkVersionControl(version, options) { const config = loadConfig(options.env); @@ -582,20 +520,6 @@ if (require.main === module) { mainProcessor(callContract, [destinationChain, destinationAddress, payload], options); }); - program - .command('allow-functions ') - .description('Allow certain funcitons on the gateway') - .action((versions, functionNames, options) => { - mainProcessor(allowFunctions, [versions, functionNames], options); - }); - - program - .command('disallow-functions ') - .description('Allow certain funcitons on the gateway') - .action((versions, functionNames, options) => { - mainProcessor(disallowFunctions, [versions, functionNames], options); - }); - program .command('check-version-control ') .description('Check if version control works on a certain version') diff --git a/sui/its-example.js b/sui/its-example.js index 89097030..1cfc3fa7 100644 --- a/sui/its-example.js +++ b/sui/its-example.js @@ -247,18 +247,18 @@ async function printReceiveDeploymentInfo(contracts, args, options) { const tokenDistributor = options.distributor; // InterchainTokenService transfer payload from Ethereum to Sui - let payload = defaultAbiCoder.encode( + const itsMessage = defaultAbiCoder.encode( ['uint256', 'uint256', 'bytes', 'bytes', 'uint256', 'bytes'], [messageType, tokenId, byteName, byteSymbol, tokenDecimals, tokenDistributor], ); - payload = defaultAbiCoder.encode(['uint256', 'string', 'bytes'], [ITSMessageType.ReceiveFromItsHub, sourceChain, payload]); + const hubMessage = defaultAbiCoder.encode(['uint256', 'string', 'bytes'], [ITSMessageType.ReceiveFromItsHub, sourceChain, itsMessage]); printInfo( JSON.stringify( { - payload, + payload: hubMessage, tokenId, - payloadHash: keccak256(payload), + payloadHash: keccak256(hubMessage), }, null, 2, @@ -276,18 +276,18 @@ async function printReceiveTransferInfo(contracts, args, options) { const itsBytes = options.itsBytes; const channelId = options.channelId || Example.objects.ItsChannelId; - let payload = defaultAbiCoder.encode( + const itsMessage = defaultAbiCoder.encode( ['uint256', 'uint256', 'bytes', 'bytes', 'uint256', 'bytes'], [ITSMessageType.InterchainTokenTransfer, tokenId, sourceAddress, channelId, unitAmount, itsBytes], ); - payload = defaultAbiCoder.encode(['uint256', 'string', 'bytes'], [ITSMessageType.ReceiveFromItsHub, sourceChain, payload]); + const hubMessage = defaultAbiCoder.encode(['uint256', 'string', 'bytes'], [ITSMessageType.ReceiveFromItsHub, sourceChain, itsMessage]); printInfo( JSON.stringify( { - payload, + payload: hubMessage, tokenId, - payloadHash: keccak256(payload), + payloadHash: keccak256(hubMessage), }, null, 2, diff --git a/sui/utils/utils.js b/sui/utils/utils.js index cc11ce29..9ea7ade4 100644 --- a/sui/utils/utils.js +++ b/sui/utils/utils.js @@ -346,6 +346,17 @@ const isAllowed = async (client, keypair, chain, exec, options) => { return true; }; +const getAllowedFunctions = async (client, versionedObjectId) => { + const response = await client.getObject({ + id: versionedObjectId, + options: { + showContent: true, + }, + }); + const allowedFunctionsArray = response.data.content.fields.value.fields.version_control.fields.allowed_functions; + return allowedFunctionsArray.map((allowedFunctions) => allowedFunctions.fields.contents); +}; + module.exports = { suiCoinId, getAmplifierSigners, @@ -367,10 +378,11 @@ module.exports = { getBagContentId, moveDir, getTransactionList, - checkTrustedAddresses, parseDiscoveryInfo, parseGatewayInfo, + checkTrustedAddresses, getStructs, saveGeneratedTx, isAllowed, + getAllowedFunctions, };