diff --git a/typescript/infra/scripts/ica/check-owner-ica.ts b/typescript/infra/scripts/ica/check-owner-ica.ts index d61ac06fb..360c54d83 100644 --- a/typescript/infra/scripts/ica/check-owner-ica.ts +++ b/typescript/infra/scripts/ica/check-owner-ica.ts @@ -1,7 +1,19 @@ +import chalk from 'chalk'; +import { ethers } from 'ethers'; + import { AccountConfig, InterchainAccount } from '@hyperlane-xyz/sdk'; -import { eqAddress, isZeroishAddress } from '@hyperlane-xyz/utils'; +import { + LogFormat, + LogLevel, + assert, + configureRootLogger, + eqAddress, + isZeroishAddress, + rootLogger, +} from '@hyperlane-xyz/utils'; import awIcas from '../../config/environments/mainnet3/aw-icas.json'; +import { oldIcas } from '../../config/environments/mainnet3/owners.js'; import { chainsToSkip } from '../../src/config/chain.js'; import { IcaArtifact } from '../../src/config/icas.js'; import { isEthereumProtocolChain } from '../../src/utils/utils.js'; @@ -9,61 +21,82 @@ import { getArgs as getEnvArgs, withChains } from '../agent-utils.js'; import { getEnvironmentConfig, getHyperlaneCore } from '../core-utils.js'; function getArgs() { - return withChains(getEnvArgs()).option('ownerChain', { - type: 'string', - description: 'Origin chain where the Safe owner lives', - default: 'ethereum', - }).argv; + return withChains(getEnvArgs()) + .option('ownerChain', { + type: 'string', + description: 'Origin chain where the Safe owner lives', + default: 'ethereum', + }) + .describe('legacy', 'If enabled, checks legacy ICAs') + .boolean('legacy') + .default('legacy', false).argv; } async function main() { - const { environment, ownerChain, chains } = await getArgs(); + const { environment, ownerChain, chains, legacy } = await getArgs(); + configureRootLogger(LogFormat.Pretty, LogLevel.Info); + assert(environment === 'mainnet3', 'Only mainnet3 is supported'); + const config = getEnvironmentConfig(environment); const multiProvider = await config.getMultiProvider(); - const owner = config.owners[ownerChain].ownerOverrides?._safeAddress; - if (!owner) { - console.error(`No Safe owner found for ${ownerChain}`); + const ownerAddress = config.owners[ownerChain].ownerOverrides?._safeAddress; + if (!ownerAddress) { + rootLogger.error(chalk.bold.red(`No Safe owner found for ${ownerChain}`)); process.exit(1); } - console.log(`Safe owner on ${ownerChain}: ${owner}`); + rootLogger.info(`Safe owner on ${ownerChain}: ${ownerAddress}`); const { chainAddresses } = await getHyperlaneCore(environment, multiProvider); const ica = InterchainAccount.fromAddressesMap(chainAddresses, multiProvider); const checkOwnerIcaChains = ( - chains?.length ? chains : Object.keys(awIcas) + chains?.length ? chains : Object.keys(legacy ? oldIcas : awIcas) ).filter( (chain) => isEthereumProtocolChain(chain) && !chainsToSkip.includes(chain), ); - const ownerConfig: AccountConfig = { - origin: ownerChain, - owner: owner, - }; + // Check that the interchain account router address is not zero const ownerChainInterchainAccountRouter = ica.contractsMap[ownerChain].interchainAccountRouter.address; - if (isZeroishAddress(ownerChainInterchainAccountRouter)) { - console.error(`Interchain account router address is zero`); + rootLogger.error( + chalk.bold.red(`Interchain account router address is zero`), + ); process.exit(1); } + // Create the owner config + const ownerConfig: AccountConfig = { + origin: ownerChain, + owner: ownerAddress, + routerOverride: ownerChainInterchainAccountRouter, + }; + + const expectedIcas: Record = {}; + const mismatchedResults: Record< string, { Expected: IcaArtifact; Actual: IcaArtifact } > = {}; for (const chain of checkOwnerIcaChains) { - const expected = awIcas[chain as keyof typeof awIcas]; + const expected = getExpectedIca(chain, legacy); + expectedIcas[chain] = expected; + if (!expected) { - console.error(`No expected address found for ${chain}`); + rootLogger.warn(chalk.yellow(`No expected address found for ${chain}`)); continue; } + + if (isZeroishAddress(expected.ica)) { + rootLogger.warn(chalk.yellow(`ICA address is zero for ${chain}`)); + continue; + } + const actualAccount = await ica.getAccount(chain, { ...ownerConfig, ismOverride: expected.ism, - routerOverride: ownerChainInterchainAccountRouter, }); if (!eqAddress(expected.ica, actualAccount)) { mismatchedResults[chain] = { @@ -77,16 +110,31 @@ async function main() { } if (Object.keys(mismatchedResults).length > 0) { - console.error('\nMismatched ICAs found:'); + rootLogger.error(chalk.bold.red('\nMismatched ICAs found:')); console.table(mismatchedResults); process.exit(1); } else { - console.log('✅ All ICAs match the expected addresses.'); + rootLogger.info( + chalk.bold.green('✅ All ICAs match the expected addresses.'), + ); + console.table(expectedIcas); } process.exit(0); } +// Enables support for checking legacy ICAs +function getExpectedIca(chain: string, legacy: boolean): IcaArtifact { + return legacy + ? { + ica: + oldIcas[chain as keyof typeof oldIcas] || + ethers.constants.AddressZero, + ism: ethers.constants.AddressZero, + } + : awIcas[chain as keyof typeof awIcas]; +} + main().catch((err) => { - console.error('Error:', err); + rootLogger.error('Error:', err); process.exit(1); }); diff --git a/typescript/infra/scripts/ica/get-owner-ica.ts b/typescript/infra/scripts/ica/get-owner-ica.ts index 64072ef91..e63da03cb 100644 --- a/typescript/infra/scripts/ica/get-owner-ica.ts +++ b/typescript/infra/scripts/ica/get-owner-ica.ts @@ -1,9 +1,15 @@ +import chalk from 'chalk'; + import { AccountConfig, ChainMap, InterchainAccount } from '@hyperlane-xyz/sdk'; import { + LogFormat, + LogLevel, assert, + configureRootLogger, eqAddress, isZeroishAddress, objFilter, + rootLogger, } from '@hyperlane-xyz/utils'; import { getIcaIsm } from '../../config/environments/mainnet3/ica.js'; @@ -52,6 +58,7 @@ async function main() { deploy, owner: ownerOverride, } = await getArgs(); + configureRootLogger(LogFormat.Pretty, LogLevel.Info); assert(environment === 'mainnet3', 'Only mainnet3 is supported'); @@ -63,7 +70,10 @@ async function main() { try { artifacts = await readAbacusWorksIcas(environment); } catch (err) { - console.error('Error reading artifacts, defaulting to no artifacts:', err); + rootLogger.error( + chalk.bold.red('Error reading artifacts, defaulting to no artifacts:'), + err, + ); artifacts = {}; } @@ -82,7 +92,7 @@ async function main() { } // Log the owner address - console.log(`Governance owner on ${ownerChain}: ${originOwner}`); + rootLogger.info(`Governance owner on ${ownerChain}: ${originOwner}`); // Get the chain addresses const { chainAddresses } = await getHyperlaneCore(environment, multiProvider); @@ -102,7 +112,9 @@ async function main() { const ownerChainInterchainAccountRouter = ica.contractsMap[ownerChain].interchainAccountRouter.address; if (isZeroishAddress(ownerChainInterchainAccountRouter)) { - console.error(`Interchain account router address is zero`); + rootLogger.error( + chalk.bold.red(`Interchain account router address is zero`), + ); process.exit(1); } @@ -119,8 +131,10 @@ async function main() { chains = chainsArg; } else { chains = ica.chains().filter((chain) => chain !== ownerChain); - console.log( - 'Chains not supplied, using all ICA supported chains other than the owner chain:', + rootLogger.debug( + chalk.italic.gray( + 'Chains not supplied, using all ICA supported chains other than the owner chain:', + ), chains, ); } @@ -137,15 +151,16 @@ async function main() { // Verify or deploy each chain's ICA const settledResults = await Promise.allSettled( chains.map(async (chain) => { - return abacusWorksIca.verifyOrDeployChainIca(chain, { - chainArtifact: artifacts[chain], - deploy, + return abacusWorksIca.recoverOrDeployChainIca( + chain, ownerConfig, - }); + artifacts[chain], + deploy, + ); }), ); - // User-friendly output for the console.table + // User-friendly output for the rootLogger.table const results: Record> = {}; // Map of chain to ICA artifact const icaArtifacts: ChainMap = {}; @@ -153,7 +168,7 @@ async function main() { if (settledResult.status === 'fulfilled') { const { chain, result, error, deployed, recovered } = settledResult.value; if (error || !result) { - console.error(`Failed to process ${chain}:`, error); + rootLogger.error(chalk.red(`Failed to process ${chain}:`), error); } else { results[chain] = { deployed, @@ -163,23 +178,26 @@ async function main() { icaArtifacts[chain] = result; } } else { - console.error(`Promise rejected:`, settledResult.reason); + rootLogger.error(chalk.red(`Promise rejected:`), settledResult.reason); } }); console.table(results); - console.log( - `Writing results to local artifacts: ${getAbacusWorksIcasPath( - environment, - )}`, + rootLogger.info( + chalk.italic.gray( + `Writing results to local artifacts: ${getAbacusWorksIcasPath( + environment, + )}`, + ), ); persistAbacusWorksIcas(environment, icaArtifacts); + process.exit(0); } main() .then() .catch((err) => { - console.error('Error:', err); + rootLogger.error(chalk.bold.red('Error:'), err); process.exit(1); }); diff --git a/typescript/infra/src/config/icas.ts b/typescript/infra/src/config/icas.ts index a0a395285..51fe92b6e 100644 --- a/typescript/infra/src/config/icas.ts +++ b/typescript/infra/src/config/icas.ts @@ -1,3 +1,4 @@ +import chalk from 'chalk'; import { ethers } from 'ethers'; import { @@ -11,7 +12,13 @@ import { MultiProvider, normalizeConfig, } from '@hyperlane-xyz/sdk'; -import { Address, deepEquals, eqAddress } from '@hyperlane-xyz/utils'; +import { + Address, + deepEquals, + eqAddress, + rootLogger, + stringifyObject, +} from '@hyperlane-xyz/utils'; import { getAbacusWorksIcasPath } from '../../scripts/agent-utils.js'; import { readJSONAtPath, writeMergedJSONAtPath } from '../utils/utils.js'; @@ -24,7 +31,7 @@ export interface IcaArtifact { } export interface IcaDeployResult { - chain: string; + chain: ChainName; result?: IcaArtifact; error?: string; deployed?: string; @@ -45,7 +52,35 @@ export function readAbacusWorksIcas( return readJSONAtPath(getAbacusWorksIcasPath(environment)); } +/** + * Manages the Interchain Accounts (ICAs) for Abacus Works + * + * Public methods: + * - getIcaAccount(ownerConfig: AccountConfig, icaChain: ChainName, ismAddress: Address): Promise + * Gets the ICA address using the owner config and ISM address. + * + * - recoverOrDeployChainIca(chain: ChainName, ownerConfig: AccountConfig, chainArtifact?: IcaArtifact, deploy: boolean): Promise + * Recovers or deploys the ICA for a given chain. If deploy is true and the existing ICA does not match expected config, deploys a new one. + * Returns result containing ICA address, ISM address, and deployment status. + * + * - artifactMatchesExpectedConfig(ownerConfig: AccountConfig, icaChain: ChainName, icaArtifact: IcaArtifact): Promise + * Verifies that an ICA artifact matches the expected configuration by checking ISM config and ICA address recovery + * + * - deployNewIca(chain: ChainName, ownerConfig: AccountConfig): Promise + * Deploys a new ICA with proper ownership setup: + * 1. Deploys ISM with deployer as initial owner. + * 2. Deploys ICA using the ISM. + * 3. Transfers ISM ownership to the ICA. + * 4. Verifies deployment matches expected config. + * + * - getExpectedIsmConfig(chain: ChainName, ownerConfig: AccountConfig, icaAddress: Address): Promise + * Gets the expected ISM configuration for a chain and owner. + */ export class AbacusWorksIcaManager { + private readonly logger = rootLogger.child({ + module: 'AbacusWorksIcaManager', + }); + constructor( private readonly multiProvider: MultiProvider, private readonly ica: InterchainAccount, @@ -60,6 +95,11 @@ export class AbacusWorksIcaManager { /** * Gets the ICA address using the owner config and ISM address + * + * @param ownerConfig - The owner config for the ICA + * @param icaChain - The chain where the ICA exists + * @param ismAddress - The address of the ISM + * @returns The ICA address */ public async getIcaAccount( ownerConfig: AccountConfig, @@ -77,33 +117,33 @@ export class AbacusWorksIcaManager { } /** - * Verifies or deploys the ICA for a given chain. + * Recovers or deploys the ICA for a given chain. * @param chain - The chain to process. - * @param options - The options for verifying or deploying the ICA. - * @returns The result of verifying or deploying the ICA. + * @param ownerConfig - The owner config for the ICA. + * @param chainArtifact - The existing ICA artifact for the chain, if it exists. + * @param deploy - Whether to deploy a new ICA if the existing one does not match the expected config. + * @returns The result of recovering or deploying the ICA. */ - public async verifyOrDeployChainIca( - chain: string, - { - ownerConfig, - chainArtifact, - deploy, - }: { - ownerConfig: AccountConfig; - chainArtifact?: IcaArtifact; - deploy: boolean; - }, + public async recoverOrDeployChainIca( + chain: ChainName, + ownerConfig: AccountConfig, + chainArtifact: IcaArtifact | undefined, + deploy: boolean, ): Promise { // Try to recover existing ICA + // If the chain artifact is undefined, we assume the ICA is not deployed + // If the ISM address is zero, we assume the ICA is not deployed if ( chainArtifact && !eqAddress(chainArtifact.ism, ethers.constants.AddressZero) ) { - console.log( - 'Attempting ICA recovery on chain', - chain, - 'with existing artifact', - chainArtifact, + this.logger.debug( + chalk.italic.gray( + 'Attempting ICA recovery on chain', + chain, + 'with existing artifact', + chainArtifact, + ), ); const matches = await this.artifactMatchesExpectedConfig( @@ -113,7 +153,7 @@ export class AbacusWorksIcaManager { ); if (matches) { - console.log('Recovered ICA on chain', chain); + this.logger.info(chalk.bold.green(`Recovered ICA on chain ${chain}`)); return { chain, result: chainArtifact, @@ -122,17 +162,17 @@ export class AbacusWorksIcaManager { }; } - console.warn( + this.logger.warn( `Chain ${chain} ICA artifact does not match expected config, will redeploy`, ); } - // Handle case where deployment is not allowed + // If we're not deploying, we can't have an ICA if (!deploy) { - console.log( - 'Skipping required ISM deployment for chain', - chain, - ', will not have an ICA', + this.logger.debug( + chalk.italic.gray( + `Skipping required ISM deployment for chain ${chain}, will not have an ICA`, + ), ); return { chain, @@ -154,8 +194,7 @@ export class AbacusWorksIcaManager { * 1. Checking that the ISM configuration matches what we expect * 2. Verifying we can recover the correct ICA address * - * @param originChain - The chain where the owner account exists - * @param originOwner - The address of the owner account + * @param ownerConfig - The owner config for the ICA * @param icaChain - The chain where the ICA exists * @param icaArtifact - The artifact containing ICA and ISM addresses * @returns True if the artifact matches expected config, false otherwise @@ -215,13 +254,15 @@ export class AbacusWorksIcaManager { ); if (!matches) { - console.log( - `Somehow after everything, the ICA artifact on chain ${chain} still does not match the expected config! There's probably a bug.`, + this.logger.error( + chalk.bold.red( + `Somehow after everything, the ICA artifact on chain ${chain} still does not match the expected config! There's probably a bug.`, + ), ); return { chain, result: undefined, deployed: '❌', recovered: '❌' }; } - return { chain, result: newChainArtifact, deployed: '✅', recovered: '❌' }; + return { chain, result: newChainArtifact, deployed: '✅', recovered: '-' }; } /** @@ -255,16 +296,25 @@ export class AbacusWorksIcaManager { // Read the actual config from the deployed ISM const actualIsmConfig = await ismModule.read(); + const normalizedActualIsmConfig = normalizeConfig(actualIsmConfig); + const normalizedDesiredIsmConfig = normalizeConfig(desiredIsmConfig); + // Compare normalized configs to handle any formatting differences const configsMatch = deepEquals( - normalizeConfig(actualIsmConfig), - normalizeConfig(desiredIsmConfig), + normalizedActualIsmConfig, + normalizedDesiredIsmConfig, ); if (!configsMatch) { - console.log('ISM mismatch for', icaChain); - console.log('actualIsmConfig:', JSON.stringify(actualIsmConfig)); - console.log('desiredIsmConfig:', JSON.stringify(desiredIsmConfig)); + this.logger.error(chalk.bold.red(`ISM mismatch for ${icaChain}`)); + this.logger.error( + chalk.red('Actual ISM config:\n'), + stringifyObject(normalizedActualIsmConfig), + ); + this.logger.error( + chalk.red('Desired ISM config:\n'), + stringifyObject(normalizedDesiredIsmConfig), + ); } return configsMatch; @@ -273,8 +323,7 @@ export class AbacusWorksIcaManager { /** * Verifies that we can recover the correct ICA address using the owner config * - * @param originChain - The chain where the owner account exists - * @param originOwner - The address of the owner account + * @param ownerConfig - The owner config for the ICA * @param icaChain - The chain where the ICA exists * @param icaArtifact - The artifact containing the ICA address * @returns True if recovered address matches artifact, false otherwise @@ -294,14 +343,16 @@ export class AbacusWorksIcaManager { // Check if recovered address matches the artifact const accountMatches = eqAddress(account, icaArtifact.ica); if (!accountMatches) { - console.error( - `⚠️⚠️⚠️ Failed to recover ICA for ${icaChain}. Expected: ${ - icaArtifact.ica - }, got: ${account}. Chain owner config: ${JSON.stringify({ - origin: ownerConfig.origin, - owner: ownerConfig.owner, - ismOverride: icaArtifact.ism, - })} ⚠️⚠️⚠️`, + this.logger.error( + chalk.bold.red( + `⚠️⚠️⚠️ Failed to recover ICA for ${icaChain}. Expected: ${ + icaArtifact.ica + }, got: ${account}. Chain owner config: ${JSON.stringify({ + origin: ownerConfig.origin, + owner: ownerConfig.owner, + ismOverride: icaArtifact.ism, + })} ⚠️⚠️⚠️`, + ), ); } @@ -314,7 +365,10 @@ export class AbacusWorksIcaManager { * @param chain - The destination chain for the ICA deployment * @returns ISM module and address */ - private async deployInitialIsm(chain: ChainName) { + private async deployInitialIsm(chain: ChainName): Promise<{ + ismModule: EvmIsmModule; + ismAddress: Address; + }> { // Initially configure ISM with deployer as owner since ICA address is unknown const deployerOwnedIsm = this.getIcaIsm( chain, @@ -322,7 +376,9 @@ export class AbacusWorksIcaManager { this.deployer, ); - console.log('Deploying ISM for ICA on chain', chain); + this.logger.info( + chalk.italic.blue(`Deploying ISM for ICA on chain ${chain}`), + ); // Create and deploy the ISM module const ismModule = await EvmIsmModule.create({ chain, @@ -350,23 +406,25 @@ export class AbacusWorksIcaManager { chain: ChainName, ownerConfig: AccountConfig, ismAddress: Address, - ) { + ): Promise
{ // Configure ICA with deployed ISM address - const chainOwnerConfig = { + const icaOwnerConfig = { ...ownerConfig, ismOverride: ismAddress, }; - console.log( - 'Deploying ICA on chain', - chain, - 'with owner config', - chainOwnerConfig, + this.logger.info( + chalk.italic.blue( + `Deploying ICA on chain ${chain} with owner config`, + stringifyObject(icaOwnerConfig), + ), ); // Deploy the ICA - const deployedIca = await this.ica.deployAccount(chain, chainOwnerConfig); - console.log(`Deployed ICA on chain: ${chain}: ${deployedIca}`); + const deployedIca = await this.ica.deployAccount(chain, icaOwnerConfig); + this.logger.info( + chalk.bold.green(`Deployed ICA on chain ${chain}: ${deployedIca}`), + ); return deployedIca; } @@ -391,9 +449,11 @@ export class AbacusWorksIcaManager { chain, }); const updateTxs = await ismModule.update(icaOwnedIsmConfig); - console.log( - `Updating routing ISM owner on ${chain} with transactions:`, - updateTxs, + this.logger.info( + chalk.italic.blue(`Updating routing ISM owner on ${chain}`), + ); + this.logger.debug( + chalk.italic.gray(`Update transactions:`, stringifyObject(updateTxs)), ); await submitter.submit(...updateTxs); }