Skip to content

Commit

Permalink
feat: CCIP boiler plate for existing hook/ism deployers (#5431)
Browse files Browse the repository at this point in the history
  • Loading branch information
paulbalaji authored Feb 12, 2025
1 parent 3518f89 commit e78060d
Show file tree
Hide file tree
Showing 12 changed files with 194 additions and 12 deletions.
5 changes: 5 additions & 0 deletions .changeset/old-panthers-matter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hyperlane-xyz/sdk': minor
---

Add CCIP boiler plate for existing ISM and Hook deployers.
18 changes: 15 additions & 3 deletions typescript/sdk/src/hook/EvmHookModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { BigNumber, ethers } from 'ethers';
import {
ArbL2ToL1Hook,
ArbL2ToL1Ism__factory,
CCIPHook,
DomainRoutingHook,
DomainRoutingHook__factory,
FallbackDomainRoutingHook,
Expand All @@ -29,6 +30,7 @@ import {
Domain,
EvmChainId,
ProtocolType,
ZERO_ADDRESS_HEX_32,
addressToBytes32,
deepEquals,
eqAddress,
Expand Down Expand Up @@ -59,6 +61,7 @@ import { DeployedHook, HookFactories, hookFactories } from './contracts.js';
import {
AggregationHookConfig,
ArbL2ToL1HookConfig,
CCIPHookConfig,
DomainRoutingHookConfig,
FallbackRoutingHookConfig,
HookConfig,
Expand Down Expand Up @@ -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}`);
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -917,6 +921,14 @@ export class EvmHookModule extends HyperlaneModule<
return hook;
}

protected async deployCCIPHook({
_config,
}: {
_config: CCIPHookConfig;
}): Promise<CCIPHook> {
throw new Error('CCIP Hook deployment not yet implemented');
}

protected async deployRoutingHook({
config,
}: {
Expand Down
33 changes: 33 additions & 0 deletions typescript/sdk/src/hook/EvmHookReader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { randomBytes } from 'ethers/lib/utils.js';
import sinon from 'sinon';

import {
CCIPHook,
CCIPHook__factory,
DefaultHook,
DefaultHook__factory,
IPostDispatchHook,
Expand All @@ -25,6 +27,7 @@ import { randomAddress } from '../test/testUtils.js';

import { EvmHookReader } from './EvmHookReader.js';
import {
CCIPHookConfig,
HookType,
MailboxDefaultHookConfig,
MerkleTreeHookConfig,
Expand Down Expand Up @@ -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<CCIPHookConfig> = {
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();
Expand Down
51 changes: 48 additions & 3 deletions typescript/sdk/src/hook/EvmHookReader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ethers } from 'ethers';

import {
ArbL2ToL1Hook__factory,
CCIPHook__factory,
DefaultHook__factory,
DomainRoutingHook,
DomainRoutingHook__factory,
Expand Down Expand Up @@ -35,6 +36,7 @@ import { HyperlaneReader } from '../utils/HyperlaneReader.js';
import {
AggregationHookConfig,
ArbL2ToL1HookConfig,
CCIPHookConfig,
DomainRoutingHookConfig,
FallbackRoutingHookConfig,
HookConfig,
Expand Down Expand Up @@ -78,6 +80,8 @@ export interface HookReader {
derivePausableConfig(
address: Address,
): Promise<WithAddress<PausableHookConfig>>;
deriveIdAuthIsmConfig(address: Address): Promise<DerivedHookConfig>;
deriveCcipConfig(address: Address): Promise<WithAddress<CCIPHookConfig>>;
assertHookType(
hookType: OnchainHookType,
expectedType: OnchainHookType,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -211,6 +213,49 @@ export class EvmHookReader extends HyperlaneReader implements HookReader {
return config;
}

async deriveIdAuthIsmConfig(address: Address): Promise<DerivedHookConfig> {
// 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<WithAddress<CCIPHookConfig>> {
const ccipHook = CCIPHook__factory.connect(address, this.provider);
const destinationDomain = await ccipHook.destinationDomain();
const destinationChain = this.multiProvider.getChainName(destinationDomain);

const config: WithAddress<CCIPHookConfig> = {
address,
type: HookType.CCIP,
destinationChain,
};

this._cache.set(address, config);

return config;
}

async deriveMerkleTreeConfig(
address: Address,
): Promise<WithAddress<MerkleTreeHookConfig>> {
Expand Down
18 changes: 13 additions & 5 deletions typescript/sdk/src/hook/HyperlaneHookDeployer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { ethers } from 'ethers';

import {
CCIPHook,
DomainRoutingHook,
FallbackDomainRoutingHook,
IL1CrossDomainMessenger__factory,
Expand All @@ -11,6 +10,7 @@ import {
} from '@hyperlane-xyz/core';
import {
Address,
ZERO_ADDRESS_HEX_32,
addressToBytes32,
deepEquals,
rootLogger,
Expand All @@ -30,6 +30,7 @@ import { ChainMap, ChainName } from '../types.js';
import { DeployedHook, HookFactories, hookFactories } from './contracts.js';
import {
AggregationHookConfig,
CCIPHookConfig,
DomainRoutingHookConfig,
FallbackRoutingHookConfig,
HookConfig,
Expand Down Expand Up @@ -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}`);
}
Expand All @@ -119,6 +122,13 @@ export class HyperlaneHookDeployer extends HyperlaneDeployer<
return deployedContracts;
}

async deployCCIPHook(
_chain: ChainName,
_config: CCIPHookConfig,
): Promise<CCIPHook> {
throw new Error('CCIP Hook deployment not yet implemented');
}

async deployProtocolFee(
chain: ChainName,
config: ProtocolFeeHookConfig,
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions typescript/sdk/src/hook/contracts.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
ArbL2ToL1Hook__factory,
CCIPHook__factory,
DefaultHook__factory,
DomainRoutingHook__factory,
FallbackDomainRoutingHook__factory,
Expand All @@ -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;
Expand Down
9 changes: 9 additions & 0 deletions typescript/sdk/src/hook/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export enum OnchainHookType {
ARB_L2_TO_L1,
OP_L2_TO_L1,
MAILBOX_DEFAULT_HOOK,
AMOUNT_ROUTING,
}

export enum HookType {
Expand All @@ -39,6 +40,7 @@ export enum HookType {
PAUSABLE = 'pausableHook',
ARB_L2_TO_L1 = 'arbL2ToL1Hook',
MAILBOX_DEFAULT = 'defaultHook',
CCIP = 'ccipHook',
}

export type MerkleTreeHookConfig = z.infer<typeof MerkleTreeSchema>;
Expand All @@ -49,6 +51,7 @@ export type OpStackHookConfig = z.infer<typeof OpStackHookSchema>;
export type ArbL2ToL1HookConfig = z.infer<typeof ArbL2ToL1HookSchema>;
export type MailboxDefaultHookConfig = z.infer<typeof MailboxDefaultHookSchema>;

export type CCIPHookConfig = z.infer<typeof CCIPHookSchema>;
// explicitly typed to avoid zod circular dependency
export type AggregationHookConfig = {
type: HookType.AGGREGATION;
Expand Down Expand Up @@ -151,6 +154,11 @@ export const AggregationHookConfigSchema: z.ZodSchema<AggregationHookConfig> =
}),
);

export const CCIPHookSchema = z.object({
type: z.literal(HookType.CCIP),
destinationChain: z.string(),
});

export const HookConfigSchema = z.union([
ZHash,
ProtocolFeeSchema,
Expand All @@ -163,4 +171,5 @@ export const HookConfigSchema = z.union([
AggregationHookConfigSchema,
ArbL2ToL1HookSchema,
MailboxDefaultHookSchema,
CCIPHookSchema,
]);
5 changes: 5 additions & 0 deletions typescript/sdk/src/ism/EvmIsmReader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { expect } from 'chai';
import sinon from 'sinon';

import {
CCIPIsm,
CCIPIsm__factory,
IInterchainSecurityModule,
IInterchainSecurityModule__factory,
IMultisigIsm,
Expand Down Expand Up @@ -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);
Expand Down
22 changes: 22 additions & 0 deletions typescript/sdk/src/ism/EvmIsmReader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { BigNumber, ethers } from 'ethers';
import {
AbstractRoutingIsm__factory,
ArbL2ToL1Ism__factory,
CCIPIsm__factory,
DefaultFallbackRoutingIsm__factory,
IInterchainSecurityModule__factory,
IMultisigIsm__factory,
Expand All @@ -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';
Expand Down Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit e78060d

Please sign in to comment.