From e78060d734aa8f0934b5ab018d1ea4bd478d13ae Mon Sep 17 00:00:00 2001 From: Paul Balaji <10051819+paulbalaji@users.noreply.github.com> Date: Wed, 12 Feb 2025 17:13:40 +0000 Subject: [PATCH] feat: CCIP boiler plate for existing hook/ism deployers (#5431) --- .changeset/old-panthers-matter.md | 5 ++ typescript/sdk/src/hook/EvmHookModule.ts | 18 +++++-- typescript/sdk/src/hook/EvmHookReader.test.ts | 33 ++++++++++++ typescript/sdk/src/hook/EvmHookReader.ts | 51 +++++++++++++++++-- .../sdk/src/hook/HyperlaneHookDeployer.ts | 18 +++++-- typescript/sdk/src/hook/contracts.ts | 2 + typescript/sdk/src/hook/types.ts | 9 ++++ typescript/sdk/src/ism/EvmIsmReader.test.ts | 5 ++ typescript/sdk/src/ism/EvmIsmReader.ts | 22 ++++++++ typescript/sdk/src/ism/HyperlaneIsmFactory.ts | 14 +++++ typescript/sdk/src/ism/types.ts | 14 ++++- typescript/sdk/src/ism/utils.ts | 15 ++++++ 12 files changed, 194 insertions(+), 12 deletions(-) create mode 100644 .changeset/old-panthers-matter.md diff --git a/.changeset/old-panthers-matter.md b/.changeset/old-panthers-matter.md new file mode 100644 index 0000000000..d00cb29bb2 --- /dev/null +++ b/.changeset/old-panthers-matter.md @@ -0,0 +1,5 @@ +--- +'@hyperlane-xyz/sdk': minor +--- + +Add CCIP boiler plate for existing ISM and Hook deployers. diff --git a/typescript/sdk/src/hook/EvmHookModule.ts b/typescript/sdk/src/hook/EvmHookModule.ts index 2bf9999b5c..6049fcbe56 100644 --- a/typescript/sdk/src/hook/EvmHookModule.ts +++ b/typescript/sdk/src/hook/EvmHookModule.ts @@ -4,6 +4,7 @@ import { BigNumber, ethers } from 'ethers'; import { ArbL2ToL1Hook, ArbL2ToL1Ism__factory, + CCIPHook, DomainRoutingHook, DomainRoutingHook__factory, FallbackDomainRoutingHook, @@ -29,6 +30,7 @@ import { Domain, EvmChainId, ProtocolType, + ZERO_ADDRESS_HEX_32, addressToBytes32, deepEquals, eqAddress, @@ -59,6 +61,7 @@ import { DeployedHook, HookFactories, hookFactories } from './contracts.js'; import { AggregationHookConfig, ArbL2ToL1HookConfig, + CCIPHookConfig, DomainRoutingHookConfig, FallbackRoutingHookConfig, HookConfig, @@ -655,6 +658,9 @@ export class EvmHookModule extends HyperlaneModule< case HookType.PAUSABLE: { return this.deployPausableHook({ config }); } + case HookType.CCIP: { + return this.deployCCIPHook({ _config: config }); + } default: throw new Error(`Unsupported hook config: ${config}`); } @@ -794,9 +800,7 @@ export class EvmHookModule extends HyperlaneModule< opstackIsm.address, ); return hook; - } else if ( - authorizedHook !== addressToBytes32(ethers.constants.AddressZero) - ) { + } else if (authorizedHook !== ZERO_ADDRESS_HEX_32) { this.logger.debug( 'Authorized hook mismatch on ism %s, expected %s, got %s', opstackIsm.address, @@ -917,6 +921,14 @@ export class EvmHookModule extends HyperlaneModule< return hook; } + protected async deployCCIPHook({ + _config, + }: { + _config: CCIPHookConfig; + }): Promise { + throw new Error('CCIP Hook deployment not yet implemented'); + } + protected async deployRoutingHook({ config, }: { diff --git a/typescript/sdk/src/hook/EvmHookReader.test.ts b/typescript/sdk/src/hook/EvmHookReader.test.ts index 8f09d39d8a..e0bf029e8a 100644 --- a/typescript/sdk/src/hook/EvmHookReader.test.ts +++ b/typescript/sdk/src/hook/EvmHookReader.test.ts @@ -4,6 +4,8 @@ import { randomBytes } from 'ethers/lib/utils.js'; import sinon from 'sinon'; import { + CCIPHook, + CCIPHook__factory, DefaultHook, DefaultHook__factory, IPostDispatchHook, @@ -25,6 +27,7 @@ import { randomAddress } from '../test/testUtils.js'; import { EvmHookReader } from './EvmHookReader.js'; import { + CCIPHookConfig, HookType, MailboxDefaultHookConfig, MerkleTreeHookConfig, @@ -219,6 +222,36 @@ describe('EvmHookReader', () => { expect(config).to.deep.equal(hookConfig); }); + it('should derive CCIPHook configuration correctly', async () => { + const ccipHookAddress = randomAddress(); + const destinationDomain = test1.domainId; + const ism = randomAddress(); + + // Mock the CCIPHook contract + const mockContract = { + hookType: sandbox.stub().resolves(OnchainHookType.ID_AUTH_ISM), + destinationDomain: sandbox.stub().resolves(destinationDomain), + ism: sandbox.stub().resolves(ism), + }; + + sandbox + .stub(CCIPHook__factory, 'connect') + .returns(mockContract as unknown as CCIPHook); + sandbox + .stub(IPostDispatchHook__factory, 'connect') + .returns(mockContract as unknown as IPostDispatchHook); + + const config = await evmHookReader.deriveCcipConfig(ccipHookAddress); + + const expectedConfig: WithAddress = { + address: ccipHookAddress, + type: HookType.CCIP, + destinationChain: TestChainName.test1, + }; + + expect(config).to.deep.equal(expectedConfig); + }); + it('should throw if derivation fails', async () => { const mockAddress = randomAddress(); const mockOwner = randomAddress(); diff --git a/typescript/sdk/src/hook/EvmHookReader.ts b/typescript/sdk/src/hook/EvmHookReader.ts index c45782905e..11d6a4d4a3 100644 --- a/typescript/sdk/src/hook/EvmHookReader.ts +++ b/typescript/sdk/src/hook/EvmHookReader.ts @@ -2,6 +2,7 @@ import { ethers } from 'ethers'; import { ArbL2ToL1Hook__factory, + CCIPHook__factory, DefaultHook__factory, DomainRoutingHook, DomainRoutingHook__factory, @@ -35,6 +36,7 @@ import { HyperlaneReader } from '../utils/HyperlaneReader.js'; import { AggregationHookConfig, ArbL2ToL1HookConfig, + CCIPHookConfig, DomainRoutingHookConfig, FallbackRoutingHookConfig, HookConfig, @@ -78,6 +80,8 @@ export interface HookReader { derivePausableConfig( address: Address, ): Promise>; + deriveIdAuthIsmConfig(address: Address): Promise; + deriveCcipConfig(address: Address): Promise>; assertHookType( hookType: OnchainHookType, expectedType: OnchainHookType, @@ -153,10 +157,8 @@ export class EvmHookReader extends HyperlaneReader implements HookReader { case OnchainHookType.PROTOCOL_FEE: derivedHookConfig = await this.deriveProtocolFeeConfig(address); break; - // ID_AUTH_ISM could be OPStackHook, ERC5164Hook or LayerZeroV2Hook - // For now assume it's OP_STACK case OnchainHookType.ID_AUTH_ISM: - derivedHookConfig = await this.deriveOpStackConfig(address); + derivedHookConfig = await this.deriveIdAuthIsmConfig(address); break; case OnchainHookType.ARB_L2_TO_L1: derivedHookConfig = await this.deriveArbL2ToL1Config(address); @@ -211,6 +213,49 @@ export class EvmHookReader extends HyperlaneReader implements HookReader { return config; } + async deriveIdAuthIsmConfig(address: Address): Promise { + // First check if it's a CCIP hook + try { + const ccipHook = CCIPHook__factory.connect(address, this.provider); + // This method only exists on CCIPHook + await ccipHook.ccipDestination(); + return this.deriveCcipConfig(address); + } catch { + // Not a CCIP hook, try OPStack + try { + const opStackHook = OPStackHook__factory.connect( + address, + this.provider, + ); + // This method only exists on OPStackHook + await opStackHook.l1Messenger(); + return this.deriveOpStackConfig(address); + } catch { + throw new Error( + `Could not determine hook type - neither CCIP nor OPStack methods found`, + ); + } + } + } + + async deriveCcipConfig( + address: Address, + ): Promise> { + const ccipHook = CCIPHook__factory.connect(address, this.provider); + const destinationDomain = await ccipHook.destinationDomain(); + const destinationChain = this.multiProvider.getChainName(destinationDomain); + + const config: WithAddress = { + address, + type: HookType.CCIP, + destinationChain, + }; + + this._cache.set(address, config); + + return config; + } + async deriveMerkleTreeConfig( address: Address, ): Promise> { diff --git a/typescript/sdk/src/hook/HyperlaneHookDeployer.ts b/typescript/sdk/src/hook/HyperlaneHookDeployer.ts index b2cc8546d3..6519cbc6e0 100644 --- a/typescript/sdk/src/hook/HyperlaneHookDeployer.ts +++ b/typescript/sdk/src/hook/HyperlaneHookDeployer.ts @@ -1,6 +1,5 @@ -import { ethers } from 'ethers'; - import { + CCIPHook, DomainRoutingHook, FallbackDomainRoutingHook, IL1CrossDomainMessenger__factory, @@ -11,6 +10,7 @@ import { } from '@hyperlane-xyz/core'; import { Address, + ZERO_ADDRESS_HEX_32, addressToBytes32, deepEquals, rootLogger, @@ -30,6 +30,7 @@ import { ChainMap, ChainName } from '../types.js'; import { DeployedHook, HookFactories, hookFactories } from './contracts.js'; import { AggregationHookConfig, + CCIPHookConfig, DomainRoutingHookConfig, FallbackRoutingHookConfig, HookConfig, @@ -110,6 +111,8 @@ export class HyperlaneHookDeployer extends HyperlaneDeployer< await this.transferOwnershipOfContracts(chain, config, { [HookType.PAUSABLE]: hook, }); + } else if (config.type === HookType.CCIP) { + hook = await this.deployCCIPHook(chain, config); } else { throw new Error(`Unsupported hook config: ${config}`); } @@ -119,6 +122,13 @@ export class HyperlaneHookDeployer extends HyperlaneDeployer< return deployedContracts; } + async deployCCIPHook( + _chain: ChainName, + _config: CCIPHookConfig, + ): Promise { + throw new Error('CCIP Hook deployment not yet implemented'); + } + async deployProtocolFee( chain: ChainName, config: ProtocolFeeHookConfig, @@ -245,9 +255,7 @@ export class HyperlaneHookDeployer extends HyperlaneDeployer< opstackIsm.address, ); return hook; - } else if ( - authorizedHook !== addressToBytes32(ethers.constants.AddressZero) - ) { + } else if (authorizedHook !== ZERO_ADDRESS_HEX_32) { this.logger.debug( 'Authorized hook mismatch on ism %s, expected %s, got %s', opstackIsm.address, diff --git a/typescript/sdk/src/hook/contracts.ts b/typescript/sdk/src/hook/contracts.ts index 8cc83b3bb1..6f5ef97d52 100644 --- a/typescript/sdk/src/hook/contracts.ts +++ b/typescript/sdk/src/hook/contracts.ts @@ -1,5 +1,6 @@ import { ArbL2ToL1Hook__factory, + CCIPHook__factory, DefaultHook__factory, DomainRoutingHook__factory, FallbackDomainRoutingHook__factory, @@ -25,6 +26,7 @@ export const hookFactories = { [HookType.PAUSABLE]: new PausableHook__factory(), [HookType.ARB_L2_TO_L1]: new ArbL2ToL1Hook__factory(), [HookType.MAILBOX_DEFAULT]: new DefaultHook__factory(), + [HookType.CCIP]: new CCIPHook__factory(), }; export type HookFactories = typeof hookFactories; diff --git a/typescript/sdk/src/hook/types.ts b/typescript/sdk/src/hook/types.ts index 5999fefbf4..6032f78cee 100644 --- a/typescript/sdk/src/hook/types.ts +++ b/typescript/sdk/src/hook/types.ts @@ -25,6 +25,7 @@ export enum OnchainHookType { ARB_L2_TO_L1, OP_L2_TO_L1, MAILBOX_DEFAULT_HOOK, + AMOUNT_ROUTING, } export enum HookType { @@ -39,6 +40,7 @@ export enum HookType { PAUSABLE = 'pausableHook', ARB_L2_TO_L1 = 'arbL2ToL1Hook', MAILBOX_DEFAULT = 'defaultHook', + CCIP = 'ccipHook', } export type MerkleTreeHookConfig = z.infer; @@ -49,6 +51,7 @@ export type OpStackHookConfig = z.infer; export type ArbL2ToL1HookConfig = z.infer; export type MailboxDefaultHookConfig = z.infer; +export type CCIPHookConfig = z.infer; // explicitly typed to avoid zod circular dependency export type AggregationHookConfig = { type: HookType.AGGREGATION; @@ -151,6 +154,11 @@ export const AggregationHookConfigSchema: z.ZodSchema = }), ); +export const CCIPHookSchema = z.object({ + type: z.literal(HookType.CCIP), + destinationChain: z.string(), +}); + export const HookConfigSchema = z.union([ ZHash, ProtocolFeeSchema, @@ -163,4 +171,5 @@ export const HookConfigSchema = z.union([ AggregationHookConfigSchema, ArbL2ToL1HookSchema, MailboxDefaultHookSchema, + CCIPHookSchema, ]); diff --git a/typescript/sdk/src/ism/EvmIsmReader.test.ts b/typescript/sdk/src/ism/EvmIsmReader.test.ts index 4da13a80a9..40e47aa9e9 100644 --- a/typescript/sdk/src/ism/EvmIsmReader.test.ts +++ b/typescript/sdk/src/ism/EvmIsmReader.test.ts @@ -2,6 +2,8 @@ import { expect } from 'chai'; import sinon from 'sinon'; import { + CCIPIsm, + CCIPIsm__factory, IInterchainSecurityModule, IInterchainSecurityModule__factory, IMultisigIsm, @@ -136,6 +138,9 @@ describe('EvmIsmReader', () => { sandbox .stub(TrustedRelayerIsm__factory, 'connect') .returns(mockContract as unknown as TrustedRelayerIsm); + sandbox + .stub(CCIPIsm__factory, 'connect') + .returns(mockContract as unknown as CCIPIsm); sandbox .stub(IInterchainSecurityModule__factory, 'connect') .returns(mockContract as unknown as IInterchainSecurityModule); diff --git a/typescript/sdk/src/ism/EvmIsmReader.ts b/typescript/sdk/src/ism/EvmIsmReader.ts index 2848fd18ec..58a3ee72c9 100644 --- a/typescript/sdk/src/ism/EvmIsmReader.ts +++ b/typescript/sdk/src/ism/EvmIsmReader.ts @@ -3,6 +3,7 @@ import { BigNumber, ethers } from 'ethers'; import { AbstractRoutingIsm__factory, ArbL2ToL1Ism__factory, + CCIPIsm__factory, DefaultFallbackRoutingIsm__factory, IInterchainSecurityModule__factory, IMultisigIsm__factory, @@ -21,6 +22,7 @@ import { rootLogger, } from '@hyperlane-xyz/utils'; +import { getChainNameFromCCIPSelector } from '../ccip/utils.js'; import { DEFAULT_CONTRACT_READ_CONCURRENCY } from '../consts/concurrency.js'; import { DispatchedMessage } from '../core/types.js'; import { ChainTechnicalStack } from '../metadata/chainMetadataTypes.js'; @@ -312,6 +314,26 @@ export class EvmIsmReader extends HyperlaneReader implements IsmReader { ); } + // if it has ccipOrigin property --> CCIP + const ccipIsm = CCIPIsm__factory.connect(address, this.provider); + try { + const ccipOrigin = await ccipIsm.ccipOrigin(); + const originChain = getChainNameFromCCIPSelector(ccipOrigin.toString()); + if (!originChain) { + throw new Error('Unknown CCIP origin chain'); + } + return { + address, + type: IsmType.CCIP, + originChain, + }; + } catch { + this.logger.debug( + 'Error accessing "ccipOrigin" property, implying this is not a CCIP ISM.', + address, + ); + } + // if it has VERIFIED_MASK_INDEX, it's AbstractMessageIdAuthorizedIsm which means OPStackIsm const opStackIsm = OPStackIsm__factory.connect(address, this.provider); try { diff --git a/typescript/sdk/src/ism/HyperlaneIsmFactory.ts b/typescript/sdk/src/ism/HyperlaneIsmFactory.ts index 515a2da0a9..88bf4d19cb 100644 --- a/typescript/sdk/src/ism/HyperlaneIsmFactory.ts +++ b/typescript/sdk/src/ism/HyperlaneIsmFactory.ts @@ -3,6 +3,8 @@ import { Logger } from 'pino'; import { ArbL2ToL1Ism__factory, + CCIPIsm, + CCIPIsm__factory, DefaultFallbackRoutingIsm, DefaultFallbackRoutingIsm__factory, DomainRoutingIsm, @@ -52,6 +54,7 @@ import { ChainMap, ChainName } from '../types.js'; import { AggregationIsmConfig, + CCIPIsmConfig, DeployedIsm, DeployedIsmType, DomainRoutingIsmConfig, @@ -70,6 +73,7 @@ const ismFactories = { [IsmType.TEST_ISM]: new TestIsm__factory(), [IsmType.OP_STACK]: new OPStackIsm__factory(), [IsmType.ARB_L2_TO_L1]: new ArbL2ToL1Ism__factory(), + [IsmType.CCIP]: new CCIPIsm__factory(), }; class IsmDeployer extends HyperlaneDeployer<{}, typeof ismFactories> { @@ -208,6 +212,9 @@ export class HyperlaneIsmFactory extends HyperlaneApp { [config.bridge], ); break; + case IsmType.CCIP: + contract = await this.deployCCIPIsm(destination, config); + break; default: throw new Error(`Unsupported ISM type ${ismType}`); } @@ -230,6 +237,13 @@ export class HyperlaneIsmFactory extends HyperlaneApp { return contract; } + protected async deployCCIPIsm( + _destination: ChainName, + _config: CCIPIsmConfig, + ): Promise { + throw new Error('CCIP ISM deployment not yet implemented'); + } + protected async deployMultisigIsm( destination: ChainName, config: MultisigIsmConfig, diff --git a/typescript/sdk/src/ism/types.ts b/typescript/sdk/src/ism/types.ts index 87adb2cff2..6095a90d05 100644 --- a/typescript/sdk/src/ism/types.ts +++ b/typescript/sdk/src/ism/types.ts @@ -2,6 +2,7 @@ import { z } from 'zod'; import { ArbL2ToL1Ism, + CCIPIsm, IAggregationIsm, IInterchainSecurityModule, IMultisigIsm, @@ -58,6 +59,7 @@ export enum IsmType { ARB_L2_TO_L1 = 'arbL2ToL1Ism', WEIGHTED_MERKLE_ROOT_MULTISIG = 'weightedMerkleRootMultisigIsm', WEIGHTED_MESSAGE_ID_MULTISIG = 'weightedMessageIdMultisigIsm', + CCIP = 'ccipIsm', } // ISM types that can be updated in-place @@ -88,6 +90,7 @@ export function ismTypeToModuleType(ismType: IsmType): ModuleType { case IsmType.PAUSABLE: case IsmType.CUSTOM: case IsmType.TRUSTED_RELAYER: + case IsmType.CCIP: return ModuleType.NULL; case IsmType.ARB_L2_TO_L1: return ModuleType.ARB_L2_TO_L1; @@ -118,13 +121,15 @@ export type OpStackIsmConfig = z.infer; export type TrustedRelayerIsmConfig = z.infer< typeof TrustedRelayerIsmConfigSchema >; +export type CCIPIsmConfig = z.infer; export type ArbL2ToL1IsmConfig = z.infer; export type NullIsmConfig = | TestIsmConfig | PausableIsmConfig | OpStackIsmConfig - | TrustedRelayerIsmConfig; + | TrustedRelayerIsmConfig + | CCIPIsmConfig; type BaseRoutingIsmConfig< T extends IsmType.ROUTING | IsmType.FALLBACK_ROUTING | IsmType.ICA_ROUTING, @@ -164,6 +169,7 @@ export type DeployedIsmType = { [IsmType.TEST_ISM]: TestIsm; [IsmType.PAUSABLE]: PausableIsm; [IsmType.TRUSTED_RELAYER]: TrustedRelayerIsm; + [IsmType.CCIP]: CCIPIsm; [IsmType.ARB_L2_TO_L1]: ArbL2ToL1Ism; [IsmType.WEIGHTED_MERKLE_ROOT_MULTISIG]: IStaticWeightedMultisigIsm; [IsmType.WEIGHTED_MESSAGE_ID_MULTISIG]: IStaticWeightedMultisigIsm; @@ -203,6 +209,11 @@ export const TrustedRelayerIsmConfigSchema = z.object({ relayer: z.string(), }); +export const CCIPIsmConfigSchema = z.object({ + type: z.literal(IsmType.CCIP), + originChain: z.string(), +}); + export const OpStackIsmConfigSchema = z.object({ type: z.literal(IsmType.OP_STACK), origin: z.string(), @@ -275,6 +286,7 @@ export const IsmConfigSchema = z.union([ OpStackIsmConfigSchema, PausableIsmConfigSchema, TrustedRelayerIsmConfigSchema, + CCIPIsmConfigSchema, MultisigIsmConfigSchema, WeightedMultisigIsmConfigSchema, RoutingIsmConfigSchema, diff --git a/typescript/sdk/src/ism/utils.ts b/typescript/sdk/src/ism/utils.ts index 5989854ab7..0708b9e88b 100644 --- a/typescript/sdk/src/ism/utils.ts +++ b/typescript/sdk/src/ism/utils.ts @@ -1,6 +1,7 @@ import { ethers } from 'ethers'; import { + CCIPIsm__factory, DomainRoutingIsm__factory, IAggregationIsm__factory, IInterchainSecurityModule__factory, @@ -22,6 +23,7 @@ import { rootLogger, } from '@hyperlane-xyz/utils'; +import { getChainNameFromCCIPSelector } from '../ccip/utils.js'; import { HyperlaneContracts } from '../contracts/types.js'; import { ProxyFactoryFactories } from '../deploy/contracts.js'; import { MultiProvider } from '../providers/MultiProvider.js'; @@ -361,6 +363,19 @@ export async function moduleMatchesConfig( matches &&= eqAddress(relayer, config.relayer); break; } + case IsmType.CCIP: { + const ccipIsm = CCIPIsm__factory.connect(moduleAddress, provider); + const type = await ccipIsm.moduleType(); + matches &&= type === ModuleType.NULL; + + // Check that the origin chain selector matches the config + const originCcipChainSelector = await ccipIsm.ccipOrigin(); + const chainName = getChainNameFromCCIPSelector( + originCcipChainSelector.toString(), + ); + matches &&= chainName === config.originChain; + break; + } case IsmType.PAUSABLE: { const pausableIsm = PausableIsm__factory.connect(moduleAddress, provider); const owner = await pausableIsm.owner();