From b038fe03bcdf4c82618610991bb76f14fc4ed653 Mon Sep 17 00:00:00 2001 From: Foivos Date: Tue, 17 Dec 2024 15:09:24 +0200 Subject: [PATCH 1/4] feat(sui)!: allow and disallow functions on gateway (#474) Co-authored-by: Blockchain Guy --- sui/deploy-contract.js | 1 + sui/gateway.js | 76 +++++++++++++++++++++++++++++++++++++++++ sui/utils/sign-utils.js | 1 - sui/utils/utils.js | 22 ++++++++++++ 4 files changed, 99 insertions(+), 1 deletion(-) diff --git a/sui/deploy-contract.js b/sui/deploy-contract.js index 1e261577d..3fe8edcd3 100644 --- a/sui/deploy-contract.js +++ b/sui/deploy-contract.js @@ -227,6 +227,7 @@ async function postDeployAxelarGateway(published, keypair, client, config, chain Gateway: gateway, UpgradeCap: upgradeCap, Gatewayv0: gatewayv0, + OwnerCap: ownerCap, }, domainSeparator, operator, diff --git a/sui/gateway.js b/sui/gateway.js index f76ffe8fe..d70bbf8f4 100644 --- a/sui/gateway.js +++ b/sui/gateway.js @@ -275,6 +275,68 @@ 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: 'Allow Functions', + }; +} + async function mainProcessor(processor, args, options) { const config = loadConfig(options.env); @@ -346,6 +408,20 @@ 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); + }); + addOptionsToCommands(program, addBaseOptions, { offline: true }); program.parse(); diff --git a/sui/utils/sign-utils.js b/sui/utils/sign-utils.js index ca1768bdf..5d5880277 100644 --- a/sui/utils/sign-utils.js +++ b/sui/utils/sign-utils.js @@ -116,7 +116,6 @@ async function broadcast(client, keypair, tx, actionName) { showContent: true, }, }); - printInfo(actionName || 'Tx', receipt.digest); return receipt; diff --git a/sui/utils/utils.js b/sui/utils/utils.js index e8819b296..e75fab8ad 100644 --- a/sui/utils/utils.js +++ b/sui/utils/utils.js @@ -293,6 +293,27 @@ const saveGeneratedTx = async (tx, message, client, options) => { printInfo(`Unsigned transaction`, txFilePath); }; +const isAllowed = async (builder, sender = '0x0') => { + try { + await builder.devInspect(sender); + } catch (e) { + const errorMessage = e.cause.effects.status.error; + let regexp = /address: (.*?),/; + const packageId = `0x${regexp.exec(errorMessage)[1]}`; + + regexp = /Identifier\("(.*?)"\)/; + const module = regexp.exec(errorMessage)[1]; + + regexp = /Some\("(.*?)"\)/; + const functionName = regexp.exec(errorMessage)[1]; + + regexp = /Some\(".*?"\) \}, (.*?)\)/; + const errorCode = parseInt(regexp.exec(errorMessage)[1]); + console.log(packageId, module, functionName, errorCode); + console.log(errorMessage); + } +}; + module.exports = { suiCoinId, getAmplifierSigners, @@ -319,4 +340,5 @@ module.exports = { parseGatewayInfo, getStructs, saveGeneratedTx, + isAllowed, }; From d582782f63923b5bf46aa9f9cb9066548ce08acb Mon Sep 17 00:00:00 2001 From: Foivos Date: Tue, 17 Dec 2024 15:38:20 +0200 Subject: [PATCH 2/4] feat(sui)!: check which functions are allowed on the gateway. (#475) Co-authored-by: Blockchain Guy --- README.md | 1 + sui/deploy-contract.js | 9 +++ sui/gateway.js | 142 ++++++++++++++++++++++++++++++++++++- sui/utils/upgrade-utils.js | 5 +- sui/utils/utils.js | 50 +++++++++++-- 5 files changed, 198 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 17ba338b0..8d3ac6670 100644 --- a/README.md +++ b/README.md @@ -9,3 +9,4 @@ Install dependencies via - [Cosmwasm](./cosmwasm/README.md) - [Sui](./sui/README.md) - [Stellar](./stellar/README.md) + diff --git a/sui/deploy-contract.js b/sui/deploy-contract.js index 3fe8edcd3..53beaa5da 100644 --- a/sui/deploy-contract.js +++ b/sui/deploy-contract.js @@ -68,6 +68,7 @@ const PACKAGE_CONFIGS = { Operators: postDeployOperators, ITS: postDeployIts, Squid: postDeploySquid, + Utils: postDeployUtils, }, }; @@ -109,6 +110,13 @@ async function postDeployRelayerDiscovery(published, keypair, client, config, ch }; } +async function postDeployUtils(published, keypair, client, config, chain, options) { + const [upgradeCap] = getObjectIdsByObjectTypes(published.publishTxn, [`${suiPackageAddress}::package::UpgradeCap`]); + chain.contracts.Utils.objects = { + UpgradeCap: upgradeCap, + }; +} + async function postDeployGasService(published, keypair, client, config, chain, options) { const [gasCollectorCapObjectId, gasServiceObjectId, gasServicev0ObjectId] = getObjectIdsByObjectTypes(published.publishTxn, [ `${published.packageId}::gas_service::GasCollectorCap`, @@ -307,6 +315,7 @@ async function deploy(keypair, client, supportedContract, config, chain, options // Update chain configuration with deployed contract address chain.contracts[packageName] = { address: published.packageId, + versions: [published.packageId], }; chain.contracts[packageName].structs = await getStructs(client, published.packageId); diff --git a/sui/gateway.js b/sui/gateway.js index d70bbf8f4..8cd10a74d 100644 --- a/sui/gateway.js +++ b/sui/gateway.js @@ -2,7 +2,7 @@ const { Command, Option } = require('commander'); const { Transaction } = require('@mysten/sui/transactions'); const { bcs } = require('@mysten/sui/bcs'); const { ethers } = require('hardhat'); -const { bcsStructs } = require('@axelar-network/axelar-cgp-sui'); +const { bcsStructs, CLOCK_PACKAGE_ID } = require('@axelar-network/axelar-cgp-sui'); const { utils: { arrayify, keccak256, toUtf8Bytes }, constants: { HashZero }, @@ -19,8 +19,10 @@ const { broadcast, suiClockAddress, saveGeneratedTx, + isAllowed, } = require('./utils'); const secp256k1 = require('secp256k1'); +const chalk = require('chalk'); const COMMAND_TYPE_APPROVE_MESSAGES = 0; const COMMAND_TYPE_ROTATE_SIGNERS = 1; @@ -333,8 +335,135 @@ async function disallowFunctions(keypair, client, config, chain, contractConfig, return { tx, - message: 'Allow Functions', + message: 'Disallow Functions', + }; +} + +async function checkVersionControl(version, options) { + const config = loadConfig(options.env); + + const chain = getChainConfig(config, options.chainName); + const [keypair, client] = getWallet(chain, options); + await printWalletInfo(keypair, client, chain, options); + + if (!chain.contracts?.AxelarGateway) { + throw new Error('Axelar Gateway package not found.'); + } + + const contractConfig = chain.contracts.AxelarGateway; + const packageId = contractConfig.versions[version]; + + const functions = {}; + functions.approve_messages = (tx) => + tx.moveCall({ + target: `${packageId}::gateway::approve_messages`, + arguments: [tx.object(contractConfig.objects.Gateway), tx.pure.vector('u8', []), tx.pure.vector('u8', [])], + }); + functions.rotate_signers = (tx) => + tx.moveCall({ + target: `${packageId}::gateway::rotate_signers`, + arguments: [ + tx.object(contractConfig.objects.Gateway), + tx.object(CLOCK_PACKAGE_ID), + tx.pure.vector('u8', []), + tx.pure.vector('u8', []), + ], + }); + functions.is_message_approved = (tx) => + tx.moveCall({ + target: `${packageId}::gateway::is_message_approved`, + arguments: [ + tx.object(contractConfig.objects.Gateway), + tx.pure.string(''), + tx.pure.string(''), + tx.pure.string(''), + tx.pure.address('0x0'), + tx.moveCall({ + target: `${packageId}::bytes32::from_address`, + arguments: [tx.pure.address('0x0')], + }), + ], + }); + functions.is_message_executed = (tx) => + tx.moveCall({ + target: `${packageId}::gateway::is_message_executed`, + arguments: [tx.object(contractConfig.objects.Gateway), tx.pure.string(''), tx.pure.string('')], + }); + functions.take_approved_message = (tx) => + tx.moveCall({ + target: `${packageId}::gateway::take_approved_message`, + arguments: [ + tx.object(contractConfig.objects.Gateway), + tx.pure.string(''), + tx.pure.string(''), + tx.pure.string(''), + tx.pure.address('0x0'), + tx.pure.vector('u8', []), + ], + }); + + functions.send_message = (tx) => { + const channel = tx.moveCall({ + target: `${packageId}::channel::new`, + arguments: [], + }); + + const message = tx.moveCall({ + target: `${packageId}::gateway::prepare_message`, + arguments: [channel, tx.pure.string(''), tx.pure.string(''), tx.pure.vector('u8', [])], + }); + + tx.moveCall({ + target: `${packageId}::gateway::send_message`, + arguments: [tx.object(contractConfig.objects.Gateway), message], + }); + + tx.moveCall({ + target: `${packageId}::channel::destroy`, + arguments: [channel], + }); }; + + functions.allow_function = (tx) => + tx.moveCall({ + target: `${packageId}::gateway::allow_function`, + arguments: [ + tx.object(contractConfig.objects.Gateway), + tx.object(contractConfig.objects.OwnerCap), + tx.pure.u64(0), + tx.pure.string(''), + ], + }); + functions.disallow_function = (tx) => + tx.moveCall({ + target: `${packageId}::gateway::disallow_function`, + arguments: [ + tx.object(contractConfig.objects.Gateway), + tx.object(contractConfig.objects.OwnerCap), + tx.pure.u64(0), + tx.pure.string(''), + ], + }); + + if (options.allowedFunctions) { + const allowedFunctions = options.allowedFunctions === 'all' ? Object.keys(functions) : options.allowedFunctions.split(','); + + for (const allowedFunction of allowedFunctions) { + const allowed = await isAllowed(client, keypair, chain, functions[allowedFunction]); + const color = allowed ? chalk.green : chalk.red; + console.log(`${allowedFunction} is ${color(allowed ? 'allowed' : 'dissalowed')}`); + } + } + + if (options.disallowedFunctions) { + const disallowedFunctions = options.allowedFunctions === 'all' ? Object.keys(functions) : options.disallowedFunctions.split(','); + + for (const disallowedFunction of disallowedFunctions) { + const allowed = await isAllowed(client, keypair, chain, functions[disallowedFunction]); + const color = allowed ? chalk.red : chalk.green; + console.log(`${disallowedFunction} is ${color(allowed ? 'allowed' : 'dissalowed')}`); + } + } } async function mainProcessor(processor, args, options) { @@ -422,6 +551,15 @@ if (require.main === module) { mainProcessor(disallowFunctions, [versions, functionNames], options); }); + program + .command('check-version-control ') + .description('Check if version control works on a certain version') + .addOption(new Option('--allowed-functions ', 'Functions that should be allowed on this version')) + .addOption(new Option('--disallowed-functions ', 'Functions that should be disallowed on this version')) + .action((version, options) => { + checkVersionControl(version, options); + }); + addOptionsToCommands(program, addBaseOptions, { offline: true }); program.parse(); diff --git a/sui/utils/upgrade-utils.js b/sui/utils/upgrade-utils.js index 5304a128f..909f23252 100644 --- a/sui/utils/upgrade-utils.js +++ b/sui/utils/upgrade-utils.js @@ -1,7 +1,7 @@ const { bcs } = require('@mysten/bcs'); const { fromB64 } = require('@mysten/bcs'); const { printInfo, validateParameters } = require('../../common/utils'); -const { copyMovePackage } = require('@axelar-network/axelar-cgp-sui'); +const { copyMovePackage, updateMoveToml } = require('@axelar-network/axelar-cgp-sui'); const { getObjectIdsByObjectTypes, suiPackageAddress, moveDir, saveGeneratedTx } = require('./utils'); const UPGRADE_POLICIES = { code_upgrade: 'only_additive_upgrades', @@ -71,12 +71,15 @@ async function upgradePackage(client, keypair, packageToUpgrade, contractConfig, const packageId = (result.objectChanges?.filter((a) => a.type === 'published') ?? [])[0].packageId; contractConfig.address = packageId; + contractConfig.versions.push(packageId); const [upgradeCap] = getObjectIdsByObjectTypes(result, [`${suiPackageAddress}::package::UpgradeCap`]); contractConfig.objects.UpgradeCap = upgradeCap; printInfo('Transaction Digest', JSON.stringify(result.digest, null, 2)); printInfo(`${packageName} Upgraded Address`, packageId); + updateMoveToml(packageToUpgrade.packageDir, packageId, moveDir); + return { upgraded: result, packageId }; } } diff --git a/sui/utils/utils.js b/sui/utils/utils.js index e75fab8ad..ec73ddf7d 100644 --- a/sui/utils/utils.js +++ b/sui/utils/utils.js @@ -18,7 +18,11 @@ const { bcsStructs, getDefinedSuiVersion, getInstalledSuiVersion, + STD_PACKAGE_ID, + SUI_PACKAGE_ID, } = require('@axelar-network/axelar-cgp-sui'); +const { Transaction } = require('@mysten/sui/transactions'); +const { broadcast } = require('./sign-utils'); const suiPackageAddress = '0x2'; const suiClockAddress = '0x6'; @@ -293,9 +297,20 @@ const saveGeneratedTx = async (tx, message, client, options) => { printInfo(`Unsigned transaction`, txFilePath); }; -const isAllowed = async (builder, sender = '0x0') => { +const isAllowed = async (client, keypair, chain, exec) => { + const addError = (tx) => { + tx.moveCall({ + target: `${STD_PACKAGE_ID}::ascii::char`, + arguments: [tx.pure.u8(128)], + }); + }; + + const tx = new Transaction(); + exec(tx); + addError(tx); + try { - await builder.devInspect(sender); + await broadcast(client, keypair, tx); } catch (e) { const errorMessage = e.cause.effects.status.error; let regexp = /address: (.*?),/; @@ -307,11 +322,34 @@ const isAllowed = async (builder, sender = '0x0') => { regexp = /Some\("(.*?)"\)/; const functionName = regexp.exec(errorMessage)[1]; - regexp = /Some\(".*?"\) \}, (.*?)\)/; - const errorCode = parseInt(regexp.exec(errorMessage)[1]); - console.log(packageId, module, functionName, errorCode); - console.log(errorMessage); + if (packageId === chain.contracts.VersionControl.address && module === 'version_control' && functionName === 'check') { + regexp = /Some\(".*?"\) \}, (.*?)\)/; + + if (parseInt(regexp.exec(errorMessage)[1]) === 9223372539365950000) { + return false; + } + } + + let suiPackageAddress = SUI_PACKAGE_ID; + + while (suiPackageAddress.length < 66) { + suiPackageAddress = suiPackageAddress.substring(0, suiPackageAddress.length - 1) + '02'; + } + + if ( + packageId === suiPackageAddress && + module === 'dynamic_field' && + (functionName === 'borrow_child_object_mut' || functionName === 'borrow_child_object') + ) { + regexp = /Some\(".*?"\) \}, (.*?)\)/; + + if (parseInt(regexp.exec(errorMessage)[1]) === 2) { + return false; + } + } } + + return true; }; module.exports = { From 2186aa6d253dac487db80e6f89f29fd9031ef759 Mon Sep 17 00:00:00 2001 From: Foivos Date: Tue, 17 Dec 2024 16:20:10 +0200 Subject: [PATCH 3/4] feat(sui): new migrate (#477) Co-authored-by: Blockchain Guy --- sui/gateway.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/sui/gateway.js b/sui/gateway.js index 8cd10a74d..1abbd3bfd 100644 --- a/sui/gateway.js +++ b/sui/gateway.js @@ -187,12 +187,16 @@ async function approve(keypair, client, config, chain, contractConfig, args, opt async function migrate(keypair, client, config, chain, contractConfig, args, options) { const packageId = contractConfig.address; - + const data = new Uint8Array(arrayify(options.migrateData || '0x')); const tx = new Transaction(); tx.moveCall({ target: `${packageId}::gateway::migrate`, - arguments: [tx.object(contractConfig.objects.Gateway)], + arguments: [ + tx.object(contractConfig.objects.Gateway), + tx.object(contractConfig.objects.OwnerCap), + tx.pure(bcs.vector(bcs.u8()).serialize(data).toBytes()), + ], }); return { @@ -518,6 +522,7 @@ if (require.main === module) { program .command('migrate') .description('Migrate the gateway after upgrade') + .addOption(new Option('--migrate-data ', 'bcs encoded data to pass to the migrate function')) .action((options) => { mainProcessor(migrate, null, options); }); From 117389bf95d55ef12ea7ee908e714f0990d2f351 Mon Sep 17 00:00:00 2001 From: Foivos Date: Tue, 17 Dec 2024 17:04:22 +0200 Subject: [PATCH 4/4] feat(sui)!: add a test for the new field added (#478) Co-authored-by: Blockchain Guy --- sui/gateway.js | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/sui/gateway.js b/sui/gateway.js index 1abbd3bfd..6a8e4ef26 100644 --- a/sui/gateway.js +++ b/sui/gateway.js @@ -470,6 +470,45 @@ async function checkVersionControl(version, options) { } } +async function testNewField(value, options) { + const config = loadConfig(options.env); + + const chain = getChainConfig(config, options.chainName); + const [keypair, client] = getWallet(chain, options); + await printWalletInfo(keypair, client, chain, options); + + if (!chain.contracts?.AxelarGateway) { + throw new Error('Axelar Gateway package not found.'); + } + + const contractConfig = chain.contracts.AxelarGateway; + const packageId = contractConfig.address; + + let tx = new Transaction(); + + tx.moveCall({ + target: `${packageId}::gateway::set_new_field`, + arguments: [tx.object(contractConfig.objects.Gateway), tx.pure.u64(value)], + }); + + await broadcast(client, keypair, tx, 'Set new_field'); + await new Promise((resolve) => setTimeout(resolve, 1000)); + + tx = new Transaction(); + + tx.moveCall({ + target: `${packageId}::gateway::new_field`, + arguments: [tx.object(contractConfig.objects.Gateway)], + }); + + const response = await client.devInspectTransactionBlock({ + transactionBlock: tx, + sender: keypair.toSuiAddress(), + }); + const returnedValue = bcs.U64.parse(new Uint8Array(response.results[0].returnValues[0][0])); + console.log(`Set the value to ${value} and it was set to ${returnedValue}.`); +} + async function mainProcessor(processor, args, options) { const config = loadConfig(options.env); @@ -565,6 +604,13 @@ if (require.main === module) { checkVersionControl(version, options); }); + program + .command('test-new-field ') + .description('Test the new field added for upgrade-versioned') + .action((value, options) => { + testNewField(value, options); + }); + addOptionsToCommands(program, addBaseOptions, { offline: true }); program.parse();