Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: CCIP boiler plate for existing hook/ism deployers #5431

Merged
merged 8 commits into from
Feb 12, 2025
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
Loading