From 7a895db75789af55a339b570b7412b7cbb85f2eb Mon Sep 17 00:00:00 2001 From: Attiss Ngo <92927591+AttissNgo@users.noreply.github.com> Date: Fri, 19 Dec 2025 16:47:40 -0600 Subject: [PATCH] feat(cosmwasm): load-time validation for instantiated contract addresses - add InstantiatedContractConfig interface with required address field - add load-time validation for Amplifier Protocol contracts - add new getter methods - replace awkward validateRequired pattern --- common/config.ts | 46 ++++++++++++++++++- cosmwasm/contract.ts | 25 +++++----- cosmwasm/coordinator.ts | 9 ++-- cosmwasm/migrate/chain-codec.ts | 16 +++---- cosmwasm/migrate/sdk50.ts | 20 ++++---- .../update-rewards-pool-epoch-duration.ts | 6 +-- cosmwasm/query.ts | 9 ++-- cosmwasm/utils.js | 4 +- 8 files changed, 84 insertions(+), 51 deletions(-) diff --git a/common/config.ts b/common/config.ts index 1c495bac5b..3fe45ceab7 100644 --- a/common/config.ts +++ b/common/config.ts @@ -67,6 +67,10 @@ export interface ContractConfig { lastUploadedCodeId?: number; } +export interface InstantiatedContractConfig extends ContractConfig { + address: string; +} + export interface ContractsChainInfo { address: string; codeId: number; @@ -125,6 +129,8 @@ export interface GatewayChainConfig { address?: string; } +const AMPLIFIER_PROTOCOL_CONTRACTS = ['ServiceRegistry', 'Router', 'Coordinator', 'Multisig', 'Rewards'] as const; + export class ConfigManager implements FullConfig { private environment: string; @@ -146,6 +152,7 @@ export class ConfigManager implements FullConfig { this.chains = fullConfig.chains; this.validateConfig(); + this.validateAmplifierProtocolContracts(); } private validateConfig(): void { @@ -333,6 +340,21 @@ export class ConfigManager implements FullConfig { printWarn(''); } + private validateAmplifierProtocolContracts(): void { + const missing: string[] = []; + + for (const contractName of AMPLIFIER_PROTOCOL_CONTRACTS) { + const config = this.getContractConfig(contractName); + if (!config.address) { + missing.push(contractName); + } + } + + if (missing.length > 0) { + throw new Error(`Amplifier Protocol contracts missing addresses in ${this.environment} config: ${missing.join(', ')}.`); + } + } + private isValidGasPrice(price: string): boolean { const numericOnlyPattern = /^\d+$/; const withDenominationPattern = /^\d+(\.\d+)?[a-zA-Z]+$/; @@ -424,6 +446,27 @@ export class ConfigManager implements FullConfig { return contractConfig[chainName]; } + public getContractAddress(name: string): string { + const config = this.getContractConfig(name); + return config.address!; + } + + public getInstantiatedContractConfig(name: string): InstantiatedContractConfig { + const config = this.getContractConfig(name); + if (!config.address) { + throw new Error(`Contract '${name}' has not been instantiated`); + } + return config as InstantiatedContractConfig; + } + + public getInstantiatedContractByChain(name: string, chainName: string): InstantiatedContractConfig { + const config = this.getContractConfigByChain(name, chainName); + if (!config.address) { + throw new Error(`Contract '${name}' for chain '${chainName}' has not been instantiated`); + } + return config as InstantiatedContractConfig; + } + public validateRequired(value: T | undefined | null, configPath: string, type?: string): T { if (value === undefined || value === null || (typeof value === 'string' && value.trim() === '')) { throw new Error(`Missing required configuration for the chain. Please configure it in ${configPath}.`); @@ -535,8 +578,7 @@ export class ConfigManager implements FullConfig { public getChainCodecAddress(chainType: string): string { const chainCodec = this.getChainCodecContractForChainType(chainType); - const chainCodecConfig = this.getContractConfig(chainCodec); - return this.validateRequired(chainCodecConfig.address, `${chainCodec}.address`); + return this.getInstantiatedContractConfig(chainCodec).address; } public getVotingVerifierContract(chainName: string): VotingVerifierChainConfig { diff --git a/cosmwasm/contract.ts b/cosmwasm/contract.ts index 022fca0971..3e76228231 100644 --- a/cosmwasm/contract.ts +++ b/cosmwasm/contract.ts @@ -62,7 +62,7 @@ const executeContractMessage = async ( throw new Error('At least one message is required'); } - const contractAddress = config.validateRequired(config.getContractConfig(contractName).address, `${contractName}.address`); + const contractAddress = config.getInstantiatedContractConfig(contractName).address; if (options.governance) { validateGovernanceMode(config, contractName, chainName); @@ -138,9 +138,9 @@ const registerProtocol = async ( _args?: string[], fee?: string | StdFee, ): Promise => { - const serviceRegistry = config.validateRequired(config.getContractConfig('ServiceRegistry').address, 'ServiceRegistry.address'); - const router = config.validateRequired(config.getContractConfig('Router').address, 'Router.address'); - const multisig = config.validateRequired(config.getContractConfig('Multisig').address, 'Multisig.address'); + const serviceRegistry = config.getContractAddress('ServiceRegistry'); + const router = config.getContractAddress('Router'); + const multisig = config.getContractAddress('Multisig'); const msg = [ { @@ -186,11 +186,10 @@ const createRewardPools = async ( const threshold: string[] = config.parseThreshold(participationThreshold, '--participationThreshold'); - const votingVerifierAddress = config.validateRequired( - config.getVotingVerifierContract(chainName).address, - `VotingVerifier[${chainName}].address`, - ); - const multisigAddress = config.validateRequired(config.getContractConfig('Multisig').address, 'Multisig.address'); + const chainConfig = config.getChainConfig(chainName); + const verifierContractName = config.getVotingVerifierContractForChainType(chainConfig.chainType); + const votingVerifierAddress = config.getInstantiatedContractByChain(verifierContractName, chainName).address; + const multisigAddress = config.getContractAddress('Multisig'); const messages = [ { @@ -245,7 +244,7 @@ const routerFreezeChain = async ( throw new Error('Router freeze_chain can only be executed by Admin EOA, not via governance'); } - const contractAddress = config.validateRequired(config.getContractConfig('Router').address, 'Router.address'); + const contractAddress = config.getContractAddress('Router'); printDirectExecutionInfo(msg, contractAddress); return executeDirectly(client, contractAddress, msg, fee); }; @@ -265,7 +264,7 @@ const routerUnfreezeChain = async ( throw new Error('Router unfreeze_chain can only be executed by Admin EOA, not via governance'); } - const contractAddress = config.validateRequired(config.getContractConfig('Router').address, 'Router.address'); + const contractAddress = config.getContractAddress('Router'); printDirectExecutionInfo(msg, contractAddress); return executeDirectly(client, contractAddress, msg, fee); }; @@ -283,7 +282,7 @@ const routerDisableRouting = async ( throw new Error('Router disable_routing can only be executed by Admin EOA, not via governance'); } - const contractAddress = config.validateRequired(config.getContractConfig('Router').address, 'Router.address'); + const contractAddress = config.getContractAddress('Router'); printDirectExecutionInfo(msg, contractAddress); return executeDirectly(client, contractAddress, msg, fee); }; @@ -301,7 +300,7 @@ const routerEnableRouting = async ( throw new Error('Router enable_routing can only be executed by Admin EOA, not via governance'); } - const contractAddress = config.validateRequired(config.getContractConfig('Router').address, 'Router.address'); + const contractAddress = config.getContractAddress('Router'); printDirectExecutionInfo(msg, contractAddress); return executeDirectly(client, contractAddress, msg, fee); }; diff --git a/cosmwasm/coordinator.ts b/cosmwasm/coordinator.ts index 01656ac0c8..82d34646b7 100644 --- a/cosmwasm/coordinator.ts +++ b/cosmwasm/coordinator.ts @@ -78,10 +78,7 @@ export class CoordinatorManager { public constructExecuteMessage(chainName: string, salt: string, admin: string): InstantiateChainContractsMsg { try { const chainConfig = this.configManager.getChainConfig(chainName); - const rewardsConfig = this.configManager.getContractConfig('Rewards'); - const multisigConfig = this.configManager.getContractConfig('Multisig'); - const routerConfig = this.configManager.getContractConfig('Router'); - const multisigAddress = this.configManager.validateRequired(multisigConfig.address, `Multisig.address`); + const multisigAddress = this.configManager.getContractAddress('Multisig'); const proverContractName = this.configManager.getMultisigProverContractForChainType(chainConfig.chainType); const verifierContractName = this.configManager.getVotingVerifierContractForChainType(chainConfig.chainType); @@ -97,8 +94,8 @@ export class CoordinatorManager { `${proverContractName}.${chainName}.codeId`, ); const deploymentName = this.generateDeploymentName(chainName, `${gatewayCodeId}-${verifierCodeId}-${proverCodeId}`); - const rewardsAddress = this.configManager.validateRequired(rewardsConfig.address, `Rewards.address`); - const routerAddress = this.configManager.validateRequired(routerConfig.address, `Router.address`); + const rewardsAddress = this.configManager.getContractAddress('Rewards'); + const routerAddress = this.configManager.getContractAddress('Router'); const domainSeparator = calculateDomainSeparator(chainName, routerAddress, this.configManager.axelar.chainId); if (!isKeccak256Hash(domainSeparator)) { throw new Error(`Invalid ${proverContractName}[${chainName}].domainSeparator in axelar info`); diff --git a/cosmwasm/migrate/chain-codec.ts b/cosmwasm/migrate/chain-codec.ts index 7944a950fd..2d61262502 100644 --- a/cosmwasm/migrate/chain-codec.ts +++ b/cosmwasm/migrate/chain-codec.ts @@ -164,16 +164,16 @@ async function migrate(client: ClientManager, config: ConfigManager, options: Mi } // migration data for MultisigProver contract - const multisigProver = config.getMultisigProverContract(chainName); - const multisigProverAddress = config.validateRequired(multisigProver.address, `MultisigProver[${chainName}].address`); - const proverConfig = config.getContractConfig('MultisigProver'); - const proverCodeId = config.validateRequired(proverConfig.lastUploadedCodeId, `MultisigProver.lastUploadedCodeId`); + const proverContractName = config.getMultisigProverContractForChainType(chainConfig.chainType); + const multisigProverAddress = config.getInstantiatedContractByChain(proverContractName, chainName).address; + const proverConfig = config.getContractConfig(proverContractName); + const proverCodeId = config.validateRequired(proverConfig.lastUploadedCodeId, `${proverContractName}.lastUploadedCodeId`); // migration data for VotingVerifier contract - const votingVerifier = config.getVotingVerifierContract(chainName); - const votingVerifierAddress = config.validateRequired(votingVerifier.address, `VotingVerifier[${chainName}].address`); - const verifierConfig = config.getContractConfig('VotingVerifier'); - const verifierCodeId = config.validateRequired(verifierConfig.lastUploadedCodeId, `VotingVerifier.lastUploadedCodeId`); + const verifierContractName = config.getVotingVerifierContractForChainType(chainConfig.chainType); + const votingVerifierAddress = config.getInstantiatedContractByChain(verifierContractName, chainName).address; + const verifierConfig = config.getContractConfig(verifierContractName); + const verifierCodeId = config.validateRequired(verifierConfig.lastUploadedCodeId, `${verifierContractName}.lastUploadedCodeId`); migrations.push({ proverAddress: multisigProverAddress, diff --git a/cosmwasm/migrate/sdk50.ts b/cosmwasm/migrate/sdk50.ts index 91340a8c92..942ebef4bc 100644 --- a/cosmwasm/migrate/sdk50.ts +++ b/cosmwasm/migrate/sdk50.ts @@ -29,16 +29,15 @@ async function migrateAllVotingVerifiers( const votingVerifiers: Array<{ chainName: string; address: string; contractName: string }> = []; for (const { name: chainName, config: chainConfig } of chains) { - const votingVerifierConfig = config.getVotingVerifierContract(chainName); const contractName = config.getVotingVerifierContractForChainType(chainConfig.chainType); - config.validateRequired(votingVerifierConfig.address, 'votingVerifierConfig.address'); + const address = config.getInstantiatedContractByChain(contractName, chainName).address; votingVerifiers.push({ chainName, - address: votingVerifierConfig.address, + address, contractName, }); - printInfo(`Added ${chainName} voting verifier (address: ${votingVerifierConfig.address})`); + printInfo(`Added ${chainName} voting verifier (address: ${address})`); } printInfo(`Found ${votingVerifiers.length} voting verifier(s) to migrate`); @@ -80,9 +79,10 @@ async function updateBlockTimeRelatedParameters( chains.map(async ({ name: chainName, config: chainConfig }) => { try { const votingVerifierConfig = config.getVotingVerifierContract(chainName); - config.validateRequired(votingVerifierConfig.address, 'votingVerifierConfig.address'); + const contractName = config.getVotingVerifierContractForChainType(chainConfig.chainType); + const address = config.getInstantiatedContractByChain(contractName, chainName).address; - const { block_expiry } = await client.queryContractSmart(votingVerifierConfig.address, 'voting_parameters'); + const { block_expiry } = await client.queryContractSmart(address, 'voting_parameters'); const message = { update_voting_parameters: { @@ -99,12 +99,10 @@ async function updateBlockTimeRelatedParameters( `Current voting parameters for ${chainName}: block_expiry: ${block_expiry}. New proposed block_expiry: ${message.update_voting_parameters.block_expiry}`, ); - const contractName = config.getVotingVerifierContractForChainType(chainConfig.chainType); - return { chainName, contractName, - address: votingVerifierConfig.address, + address, message, }; } catch (error) { @@ -147,10 +145,10 @@ async function updateSigningParametersForMultisig( fee: string | StdFee, ): Promise { const multisigConfig = config.getContractConfig('Multisig'); - config.validateRequired(multisigConfig.address, 'axelar.contracts.Multisig.address', 'string'); + const multisigAddress = config.getContractAddress('Multisig'); config.validateRequired(multisigConfig.blockExpiry, 'axelar.contracts.Multisig.blockExpiry', 'number'); - const { block_expiry } = await client.queryContractSmart(multisigConfig.address, 'signing_parameters'); + const { block_expiry } = await client.queryContractSmart(multisigAddress, 'signing_parameters'); printInfo(`Current signing parameters: block_expiry: ${block_expiry}. New proposed block_expiry: ${multisigConfig.blockExpiry}`); const msg = { diff --git a/cosmwasm/migrate/update-rewards-pool-epoch-duration.ts b/cosmwasm/migrate/update-rewards-pool-epoch-duration.ts index 6cc6904b87..7c88695509 100644 --- a/cosmwasm/migrate/update-rewards-pool-epoch-duration.ts +++ b/cosmwasm/migrate/update-rewards-pool-epoch-duration.ts @@ -37,8 +37,7 @@ interface UpdatePoolParamsMessage { async function queryAllRewardsPools(client: CosmWasmClient, configManager: ConfigManager): Promise { const poolParams: PoolParams[] = []; - const rewardsConfig = configManager.getContractConfig('Rewards'); - const rewardsAddress = configManager.validateRequired(rewardsConfig.address, 'Rewards.address'); + const rewardsAddress = configManager.getContractAddress('Rewards'); const amplifierChains = getAmplifierChains(configManager.chains); @@ -46,8 +45,7 @@ async function queryAllRewardsPools(client: CosmWasmClient, configManager: Confi throw new Error('No amplifier chains found'); } - const multisigConfig = configManager.getContractConfig('Multisig'); - const multisigAddress = configManager.validateRequired(multisigConfig.address, 'Multisig.address'); + const multisigAddress = configManager.getContractAddress('Multisig'); for (const { name: chainName } of amplifierChains) { const chainPools: PoolParams[] = []; diff --git a/cosmwasm/query.ts b/cosmwasm/query.ts index 281ec7224e..57098edb2f 100644 --- a/cosmwasm/query.ts +++ b/cosmwasm/query.ts @@ -40,13 +40,14 @@ export async function queryRewardsPool( async function rewards(client, config, _options, args) { const [chainName] = args; - const rewardsAddress = config.getContractConfig('Rewards').address; + const rewardsAddress = config.getContractAddress('Rewards'); - const votingVerifier = config.getVotingVerifierContract(chainName); - const votingVerifierAddress = config.validateRequired(votingVerifier.address, `VotingVerifier.${chainName}.address`); + const chainConfig = config.getChainConfig(chainName); + const verifierContractName = config.getVotingVerifierContractForChainType(chainConfig.chainType); + const votingVerifierAddress = config.getInstantiatedContractByChain(verifierContractName, chainName).address; const rewardsContractAddresses = { - multisig: config.getContractConfig('Multisig').address, + multisig: config.getContractAddress('Multisig'), voting_verifier: votingVerifierAddress, }; diff --git a/cosmwasm/utils.js b/cosmwasm/utils.js index 14a7dc9d28..547530904a 100644 --- a/cosmwasm/utils.js +++ b/cosmwasm/utils.js @@ -915,9 +915,7 @@ const itsHubChainParams = (config, chainConfig) => { const key = chainConfig.axelarId.toLowerCase(); const chainParams = config.axelar.contracts.InterchainTokenService[key]; - const itsMsgTranslator = - chainParams?.msgTranslator || - config.validateRequired(config.getContractConfig('ItsAbiTranslator').address, 'ItsAbiTranslator.address'); + const itsMsgTranslator = chainParams?.msgTranslator || config.getInstantiatedContractConfig('ItsAbiTranslator').address; return { itsEdgeContractAddress,