Skip to content

Commit e78060d

Browse files
authored
feat: CCIP boiler plate for existing hook/ism deployers (#5431)
1 parent 3518f89 commit e78060d

File tree

12 files changed

+194
-12
lines changed

12 files changed

+194
-12
lines changed

.changeset/old-panthers-matter.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@hyperlane-xyz/sdk': minor
3+
---
4+
5+
Add CCIP boiler plate for existing ISM and Hook deployers.

typescript/sdk/src/hook/EvmHookModule.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { BigNumber, ethers } from 'ethers';
44
import {
55
ArbL2ToL1Hook,
66
ArbL2ToL1Ism__factory,
7+
CCIPHook,
78
DomainRoutingHook,
89
DomainRoutingHook__factory,
910
FallbackDomainRoutingHook,
@@ -29,6 +30,7 @@ import {
2930
Domain,
3031
EvmChainId,
3132
ProtocolType,
33+
ZERO_ADDRESS_HEX_32,
3234
addressToBytes32,
3335
deepEquals,
3436
eqAddress,
@@ -59,6 +61,7 @@ import { DeployedHook, HookFactories, hookFactories } from './contracts.js';
5961
import {
6062
AggregationHookConfig,
6163
ArbL2ToL1HookConfig,
64+
CCIPHookConfig,
6265
DomainRoutingHookConfig,
6366
FallbackRoutingHookConfig,
6467
HookConfig,
@@ -655,6 +658,9 @@ export class EvmHookModule extends HyperlaneModule<
655658
case HookType.PAUSABLE: {
656659
return this.deployPausableHook({ config });
657660
}
661+
case HookType.CCIP: {
662+
return this.deployCCIPHook({ _config: config });
663+
}
658664
default:
659665
throw new Error(`Unsupported hook config: ${config}`);
660666
}
@@ -794,9 +800,7 @@ export class EvmHookModule extends HyperlaneModule<
794800
opstackIsm.address,
795801
);
796802
return hook;
797-
} else if (
798-
authorizedHook !== addressToBytes32(ethers.constants.AddressZero)
799-
) {
803+
} else if (authorizedHook !== ZERO_ADDRESS_HEX_32) {
800804
this.logger.debug(
801805
'Authorized hook mismatch on ism %s, expected %s, got %s',
802806
opstackIsm.address,
@@ -917,6 +921,14 @@ export class EvmHookModule extends HyperlaneModule<
917921
return hook;
918922
}
919923

924+
protected async deployCCIPHook({
925+
_config,
926+
}: {
927+
_config: CCIPHookConfig;
928+
}): Promise<CCIPHook> {
929+
throw new Error('CCIP Hook deployment not yet implemented');
930+
}
931+
920932
protected async deployRoutingHook({
921933
config,
922934
}: {

typescript/sdk/src/hook/EvmHookReader.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { randomBytes } from 'ethers/lib/utils.js';
44
import sinon from 'sinon';
55

66
import {
7+
CCIPHook,
8+
CCIPHook__factory,
79
DefaultHook,
810
DefaultHook__factory,
911
IPostDispatchHook,
@@ -25,6 +27,7 @@ import { randomAddress } from '../test/testUtils.js';
2527

2628
import { EvmHookReader } from './EvmHookReader.js';
2729
import {
30+
CCIPHookConfig,
2831
HookType,
2932
MailboxDefaultHookConfig,
3033
MerkleTreeHookConfig,
@@ -219,6 +222,36 @@ describe('EvmHookReader', () => {
219222
expect(config).to.deep.equal(hookConfig);
220223
});
221224

225+
it('should derive CCIPHook configuration correctly', async () => {
226+
const ccipHookAddress = randomAddress();
227+
const destinationDomain = test1.domainId;
228+
const ism = randomAddress();
229+
230+
// Mock the CCIPHook contract
231+
const mockContract = {
232+
hookType: sandbox.stub().resolves(OnchainHookType.ID_AUTH_ISM),
233+
destinationDomain: sandbox.stub().resolves(destinationDomain),
234+
ism: sandbox.stub().resolves(ism),
235+
};
236+
237+
sandbox
238+
.stub(CCIPHook__factory, 'connect')
239+
.returns(mockContract as unknown as CCIPHook);
240+
sandbox
241+
.stub(IPostDispatchHook__factory, 'connect')
242+
.returns(mockContract as unknown as IPostDispatchHook);
243+
244+
const config = await evmHookReader.deriveCcipConfig(ccipHookAddress);
245+
246+
const expectedConfig: WithAddress<CCIPHookConfig> = {
247+
address: ccipHookAddress,
248+
type: HookType.CCIP,
249+
destinationChain: TestChainName.test1,
250+
};
251+
252+
expect(config).to.deep.equal(expectedConfig);
253+
});
254+
222255
it('should throw if derivation fails', async () => {
223256
const mockAddress = randomAddress();
224257
const mockOwner = randomAddress();

typescript/sdk/src/hook/EvmHookReader.ts

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { ethers } from 'ethers';
22

33
import {
44
ArbL2ToL1Hook__factory,
5+
CCIPHook__factory,
56
DefaultHook__factory,
67
DomainRoutingHook,
78
DomainRoutingHook__factory,
@@ -35,6 +36,7 @@ import { HyperlaneReader } from '../utils/HyperlaneReader.js';
3536
import {
3637
AggregationHookConfig,
3738
ArbL2ToL1HookConfig,
39+
CCIPHookConfig,
3840
DomainRoutingHookConfig,
3941
FallbackRoutingHookConfig,
4042
HookConfig,
@@ -78,6 +80,8 @@ export interface HookReader {
7880
derivePausableConfig(
7981
address: Address,
8082
): Promise<WithAddress<PausableHookConfig>>;
83+
deriveIdAuthIsmConfig(address: Address): Promise<DerivedHookConfig>;
84+
deriveCcipConfig(address: Address): Promise<WithAddress<CCIPHookConfig>>;
8185
assertHookType(
8286
hookType: OnchainHookType,
8387
expectedType: OnchainHookType,
@@ -153,10 +157,8 @@ export class EvmHookReader extends HyperlaneReader implements HookReader {
153157
case OnchainHookType.PROTOCOL_FEE:
154158
derivedHookConfig = await this.deriveProtocolFeeConfig(address);
155159
break;
156-
// ID_AUTH_ISM could be OPStackHook, ERC5164Hook or LayerZeroV2Hook
157-
// For now assume it's OP_STACK
158160
case OnchainHookType.ID_AUTH_ISM:
159-
derivedHookConfig = await this.deriveOpStackConfig(address);
161+
derivedHookConfig = await this.deriveIdAuthIsmConfig(address);
160162
break;
161163
case OnchainHookType.ARB_L2_TO_L1:
162164
derivedHookConfig = await this.deriveArbL2ToL1Config(address);
@@ -211,6 +213,49 @@ export class EvmHookReader extends HyperlaneReader implements HookReader {
211213
return config;
212214
}
213215

216+
async deriveIdAuthIsmConfig(address: Address): Promise<DerivedHookConfig> {
217+
// First check if it's a CCIP hook
218+
try {
219+
const ccipHook = CCIPHook__factory.connect(address, this.provider);
220+
// This method only exists on CCIPHook
221+
await ccipHook.ccipDestination();
222+
return this.deriveCcipConfig(address);
223+
} catch {
224+
// Not a CCIP hook, try OPStack
225+
try {
226+
const opStackHook = OPStackHook__factory.connect(
227+
address,
228+
this.provider,
229+
);
230+
// This method only exists on OPStackHook
231+
await opStackHook.l1Messenger();
232+
return this.deriveOpStackConfig(address);
233+
} catch {
234+
throw new Error(
235+
`Could not determine hook type - neither CCIP nor OPStack methods found`,
236+
);
237+
}
238+
}
239+
}
240+
241+
async deriveCcipConfig(
242+
address: Address,
243+
): Promise<WithAddress<CCIPHookConfig>> {
244+
const ccipHook = CCIPHook__factory.connect(address, this.provider);
245+
const destinationDomain = await ccipHook.destinationDomain();
246+
const destinationChain = this.multiProvider.getChainName(destinationDomain);
247+
248+
const config: WithAddress<CCIPHookConfig> = {
249+
address,
250+
type: HookType.CCIP,
251+
destinationChain,
252+
};
253+
254+
this._cache.set(address, config);
255+
256+
return config;
257+
}
258+
214259
async deriveMerkleTreeConfig(
215260
address: Address,
216261
): Promise<WithAddress<MerkleTreeHookConfig>> {

typescript/sdk/src/hook/HyperlaneHookDeployer.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import { ethers } from 'ethers';
2-
31
import {
2+
CCIPHook,
43
DomainRoutingHook,
54
FallbackDomainRoutingHook,
65
IL1CrossDomainMessenger__factory,
@@ -11,6 +10,7 @@ import {
1110
} from '@hyperlane-xyz/core';
1211
import {
1312
Address,
13+
ZERO_ADDRESS_HEX_32,
1414
addressToBytes32,
1515
deepEquals,
1616
rootLogger,
@@ -30,6 +30,7 @@ import { ChainMap, ChainName } from '../types.js';
3030
import { DeployedHook, HookFactories, hookFactories } from './contracts.js';
3131
import {
3232
AggregationHookConfig,
33+
CCIPHookConfig,
3334
DomainRoutingHookConfig,
3435
FallbackRoutingHookConfig,
3536
HookConfig,
@@ -110,6 +111,8 @@ export class HyperlaneHookDeployer extends HyperlaneDeployer<
110111
await this.transferOwnershipOfContracts(chain, config, {
111112
[HookType.PAUSABLE]: hook,
112113
});
114+
} else if (config.type === HookType.CCIP) {
115+
hook = await this.deployCCIPHook(chain, config);
113116
} else {
114117
throw new Error(`Unsupported hook config: ${config}`);
115118
}
@@ -119,6 +122,13 @@ export class HyperlaneHookDeployer extends HyperlaneDeployer<
119122
return deployedContracts;
120123
}
121124

125+
async deployCCIPHook(
126+
_chain: ChainName,
127+
_config: CCIPHookConfig,
128+
): Promise<CCIPHook> {
129+
throw new Error('CCIP Hook deployment not yet implemented');
130+
}
131+
122132
async deployProtocolFee(
123133
chain: ChainName,
124134
config: ProtocolFeeHookConfig,
@@ -245,9 +255,7 @@ export class HyperlaneHookDeployer extends HyperlaneDeployer<
245255
opstackIsm.address,
246256
);
247257
return hook;
248-
} else if (
249-
authorizedHook !== addressToBytes32(ethers.constants.AddressZero)
250-
) {
258+
} else if (authorizedHook !== ZERO_ADDRESS_HEX_32) {
251259
this.logger.debug(
252260
'Authorized hook mismatch on ism %s, expected %s, got %s',
253261
opstackIsm.address,

typescript/sdk/src/hook/contracts.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
ArbL2ToL1Hook__factory,
3+
CCIPHook__factory,
34
DefaultHook__factory,
45
DomainRoutingHook__factory,
56
FallbackDomainRoutingHook__factory,
@@ -25,6 +26,7 @@ export const hookFactories = {
2526
[HookType.PAUSABLE]: new PausableHook__factory(),
2627
[HookType.ARB_L2_TO_L1]: new ArbL2ToL1Hook__factory(),
2728
[HookType.MAILBOX_DEFAULT]: new DefaultHook__factory(),
29+
[HookType.CCIP]: new CCIPHook__factory(),
2830
};
2931

3032
export type HookFactories = typeof hookFactories;

typescript/sdk/src/hook/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export enum OnchainHookType {
2525
ARB_L2_TO_L1,
2626
OP_L2_TO_L1,
2727
MAILBOX_DEFAULT_HOOK,
28+
AMOUNT_ROUTING,
2829
}
2930

3031
export enum HookType {
@@ -39,6 +40,7 @@ export enum HookType {
3940
PAUSABLE = 'pausableHook',
4041
ARB_L2_TO_L1 = 'arbL2ToL1Hook',
4142
MAILBOX_DEFAULT = 'defaultHook',
43+
CCIP = 'ccipHook',
4244
}
4345

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

54+
export type CCIPHookConfig = z.infer<typeof CCIPHookSchema>;
5255
// explicitly typed to avoid zod circular dependency
5356
export type AggregationHookConfig = {
5457
type: HookType.AGGREGATION;
@@ -151,6 +154,11 @@ export const AggregationHookConfigSchema: z.ZodSchema<AggregationHookConfig> =
151154
}),
152155
);
153156

157+
export const CCIPHookSchema = z.object({
158+
type: z.literal(HookType.CCIP),
159+
destinationChain: z.string(),
160+
});
161+
154162
export const HookConfigSchema = z.union([
155163
ZHash,
156164
ProtocolFeeSchema,
@@ -163,4 +171,5 @@ export const HookConfigSchema = z.union([
163171
AggregationHookConfigSchema,
164172
ArbL2ToL1HookSchema,
165173
MailboxDefaultHookSchema,
174+
CCIPHookSchema,
166175
]);

typescript/sdk/src/ism/EvmIsmReader.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { expect } from 'chai';
22
import sinon from 'sinon';
33

44
import {
5+
CCIPIsm,
6+
CCIPIsm__factory,
57
IInterchainSecurityModule,
68
IInterchainSecurityModule__factory,
79
IMultisigIsm,
@@ -136,6 +138,9 @@ describe('EvmIsmReader', () => {
136138
sandbox
137139
.stub(TrustedRelayerIsm__factory, 'connect')
138140
.returns(mockContract as unknown as TrustedRelayerIsm);
141+
sandbox
142+
.stub(CCIPIsm__factory, 'connect')
143+
.returns(mockContract as unknown as CCIPIsm);
139144
sandbox
140145
.stub(IInterchainSecurityModule__factory, 'connect')
141146
.returns(mockContract as unknown as IInterchainSecurityModule);

typescript/sdk/src/ism/EvmIsmReader.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { BigNumber, ethers } from 'ethers';
33
import {
44
AbstractRoutingIsm__factory,
55
ArbL2ToL1Ism__factory,
6+
CCIPIsm__factory,
67
DefaultFallbackRoutingIsm__factory,
78
IInterchainSecurityModule__factory,
89
IMultisigIsm__factory,
@@ -21,6 +22,7 @@ import {
2122
rootLogger,
2223
} from '@hyperlane-xyz/utils';
2324

25+
import { getChainNameFromCCIPSelector } from '../ccip/utils.js';
2426
import { DEFAULT_CONTRACT_READ_CONCURRENCY } from '../consts/concurrency.js';
2527
import { DispatchedMessage } from '../core/types.js';
2628
import { ChainTechnicalStack } from '../metadata/chainMetadataTypes.js';
@@ -312,6 +314,26 @@ export class EvmIsmReader extends HyperlaneReader implements IsmReader {
312314
);
313315
}
314316

317+
// if it has ccipOrigin property --> CCIP
318+
const ccipIsm = CCIPIsm__factory.connect(address, this.provider);
319+
try {
320+
const ccipOrigin = await ccipIsm.ccipOrigin();
321+
const originChain = getChainNameFromCCIPSelector(ccipOrigin.toString());
322+
if (!originChain) {
323+
throw new Error('Unknown CCIP origin chain');
324+
}
325+
return {
326+
address,
327+
type: IsmType.CCIP,
328+
originChain,
329+
};
330+
} catch {
331+
this.logger.debug(
332+
'Error accessing "ccipOrigin" property, implying this is not a CCIP ISM.',
333+
address,
334+
);
335+
}
336+
315337
// if it has VERIFIED_MASK_INDEX, it's AbstractMessageIdAuthorizedIsm which means OPStackIsm
316338
const opStackIsm = OPStackIsm__factory.connect(address, this.provider);
317339
try {

0 commit comments

Comments
 (0)