Skip to content

Commit 3518f89

Browse files
authored
feat: implement HyperlaneCCIPDeployer (#5429)
1 parent 57137da commit 3518f89

File tree

10 files changed

+353
-6
lines changed

10 files changed

+353
-6
lines changed

.changeset/late-clouds-flash.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+
Implement HyperlaneCCIPDeployer and CCIPContractCache, for deploying and initializing CCIP ISMs/Hooks for supported pairs of CCIP chains.

.changeset/sweet-coins-appear.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@hyperlane-xyz/utils': minor
3+
---
4+
5+
Add ZERO_ADDRESS_HEX_32 constant.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { ChainMap, ChainName, getCCIPChains } from '@hyperlane-xyz/sdk';
2+
3+
export function getCCIPDeployConfig(
4+
targetNetworks: ChainName[],
5+
): ChainMap<Set<ChainName>> {
6+
const chains = getCCIPChains().filter((chain) =>
7+
targetNetworks.includes(chain),
8+
);
9+
return Object.fromEntries(
10+
chains.map((origin) => [
11+
origin,
12+
new Set(chains.filter((chain) => chain !== origin)),
13+
]),
14+
);
15+
}

typescript/infra/scripts/agent-utils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export enum Modules {
7272
HELLO_WORLD = 'helloworld',
7373
WARP = 'warp',
7474
HAAS = 'haas',
75+
CCIP = 'ccip',
7576
}
7677

7778
export const REGISTRY_MODULES = [
@@ -82,6 +83,7 @@ export const REGISTRY_MODULES = [
8283
Modules.INTERCHAIN_QUERY_SYSTEM,
8384
Modules.TEST_RECIPIENT,
8485
Modules.HOOK,
86+
Modules.CCIP,
8587
];
8688

8789
export function getArgs() {

typescript/infra/scripts/deploy.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
ContractVerifier,
1010
ExplorerLicenseType,
1111
HypERC20Deployer,
12+
HyperlaneCCIPDeployer,
1213
HyperlaneCoreDeployer,
1314
HyperlaneDeployer,
1415
HyperlaneHookDeployer,
@@ -24,6 +25,7 @@ import {
2425
import { objFilter, objMap } from '@hyperlane-xyz/utils';
2526

2627
import { Contexts } from '../config/contexts.js';
28+
import { getCCIPDeployConfig } from '../config/environments/mainnet3/ccip.js';
2729
import { core as coreConfig } from '../config/environments/mainnet3/core.js';
2830
import { getEnvAddresses } from '../config/registry.js';
2931
import { getWarpConfig } from '../config/warp.js';
@@ -79,6 +81,13 @@ async function main() {
7981
chains,
8082
);
8183

84+
const targetNetworks =
85+
chains && chains.length > 0 ? chains : !fork ? [] : [fork];
86+
87+
const filteredTargetNetworks = targetNetworks.filter(
88+
(chain) => !chainsToSkip.includes(chain),
89+
);
90+
8291
if (fork) {
8392
multiProvider = multiProvider.extendChainMetadata({
8493
[fork]: { blocks: { confirmations: 0 } },
@@ -241,6 +250,16 @@ async function main() {
241250
config = {
242251
ethereum: coreConfig.ethereum.defaultHook,
243252
};
253+
} else if (module === Modules.CCIP) {
254+
if (environment !== 'mainnet3') {
255+
throw new Error('CCIP is only supported on mainnet3');
256+
}
257+
config = getCCIPDeployConfig(filteredTargetNetworks);
258+
deployer = new HyperlaneCCIPDeployer(
259+
multiProvider,
260+
getEnvAddresses(environment),
261+
contractVerifier,
262+
);
244263
} else {
245264
console.log(`Skipping ${module}, deployer unimplemented`);
246265
return;
@@ -284,12 +303,6 @@ async function main() {
284303
}
285304
}
286305

287-
const targetNetworks =
288-
chains && chains.length > 0 ? chains : !fork ? [] : [fork];
289-
290-
const filteredTargetNetworks = targetNetworks.filter(
291-
(chain) => !chainsToSkip.includes(chain),
292-
);
293306
chainsToSkip.forEach((chain) => delete config[chain]);
294307

295308
await deployWithArtifacts({
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
import { CCIPHook__factory, CCIPIsm__factory } from '@hyperlane-xyz/core';
2+
import {
3+
ZERO_ADDRESS_HEX_32,
4+
addressToBytes32,
5+
assert,
6+
rootLogger,
7+
} from '@hyperlane-xyz/utils';
8+
9+
import {
10+
HyperlaneAddressesMap,
11+
HyperlaneContracts,
12+
} from '../contracts/types.js';
13+
import { CoreAddresses } from '../core/contracts.js';
14+
import { HyperlaneDeployer } from '../deploy/HyperlaneDeployer.js';
15+
import { ContractVerifier } from '../deploy/verify/ContractVerifier.js';
16+
import { MultiProvider } from '../providers/MultiProvider.js';
17+
import { ChainMap, ChainName } from '../types.js';
18+
19+
import {
20+
CCIPContractCache,
21+
getCCIPChainSelector,
22+
getCCIPRouterAddress,
23+
} from './utils.js';
24+
25+
export class HyperlaneCCIPDeployer extends HyperlaneDeployer<
26+
Set<ChainName>,
27+
{}
28+
> {
29+
private ccipContractCache: CCIPContractCache = new CCIPContractCache();
30+
31+
constructor(
32+
multiProvider: MultiProvider,
33+
readonly core: ChainMap<Partial<CoreAddresses>>,
34+
contractVerifier?: ContractVerifier,
35+
) {
36+
super(
37+
multiProvider,
38+
{},
39+
{
40+
logger: rootLogger.child({ module: 'HyperlaneCCIPDeployer' }),
41+
contractVerifier,
42+
},
43+
);
44+
}
45+
46+
cacheAddressesMap(addressesMap: HyperlaneAddressesMap<any>): void {
47+
super.cacheAddressesMap(addressesMap);
48+
this.ccipContractCache.cacheAddressesMap(addressesMap);
49+
}
50+
51+
async deployContracts(
52+
origin: ChainName,
53+
config: Set<ChainName>,
54+
): Promise<HyperlaneContracts<{}>> {
55+
// Deploy ISMs from chain to each destination chain concurrently.
56+
// This is done in parallel since the ISM is deployed on discrete destination chains.
57+
await Promise.all(
58+
Array.from(config).map(async (destination) => {
59+
// Deploy CCIP ISM for this origin->destination pair
60+
await this.deployCCIPIsm(origin, destination);
61+
}),
62+
);
63+
64+
// On the origin chain, deploy hooks for each destination chain in series.
65+
// This is done in series to avoid nonce contention on the origin chain.
66+
for (const destination of config) {
67+
await this.deployCCIPHook(origin, destination);
68+
}
69+
70+
// Authorize hooks for each destination chain concurrently.
71+
// This is done in parallel since the ISM is deployed on discrete destination chains.
72+
await Promise.all(
73+
Array.from(config).map(async (destination) => {
74+
await this.authorizeHook(origin, destination);
75+
}),
76+
);
77+
78+
this.ccipContractCache.writeBack(this.cachedAddresses);
79+
return {};
80+
}
81+
82+
private async authorizeHook(origin: ChainName, destination: ChainName) {
83+
const ccipIsmAddress = this.ccipContractCache.getIsm(origin, destination);
84+
assert(
85+
ccipIsmAddress,
86+
`CCIP ISM not found for ${origin} -> ${destination}`,
87+
);
88+
89+
const ccipHookAddress = this.ccipContractCache.getHook(origin, destination);
90+
assert(
91+
ccipHookAddress,
92+
`CCIP Hook not found for ${origin} -> ${destination}`,
93+
);
94+
95+
const bytes32HookAddress = addressToBytes32(ccipHookAddress);
96+
const ccipIsm = CCIPIsm__factory.connect(
97+
ccipIsmAddress,
98+
this.multiProvider.getSigner(destination),
99+
);
100+
101+
const authorizedHook = await ccipIsm.authorizedHook();
102+
this.logger.debug(
103+
'Authorized hook on ism %s: %s',
104+
ccipIsm.address,
105+
authorizedHook,
106+
);
107+
108+
// If the hook is already set, return
109+
if (authorizedHook === bytes32HookAddress) {
110+
this.logger.info(
111+
'Authorized hook already set on ism %s',
112+
ccipIsm.address,
113+
);
114+
return;
115+
}
116+
117+
// If not already set, must not be initialised yet
118+
if (authorizedHook !== ZERO_ADDRESS_HEX_32) {
119+
this.logger.error(
120+
'Authorized hook mismatch on ism %s, expected %s, got %s',
121+
ccipIsm.address,
122+
bytes32HookAddress,
123+
authorizedHook,
124+
);
125+
throw new Error('Authorized hook mismatch');
126+
}
127+
128+
// If not initialised, set the hook
129+
this.logger.info(
130+
'Setting authorized hook %s on ism %s on destination %s',
131+
ccipHookAddress,
132+
ccipIsm.address,
133+
destination,
134+
);
135+
await this.multiProvider.handleTx(
136+
destination,
137+
ccipIsm.setAuthorizedHook(
138+
bytes32HookAddress,
139+
this.multiProvider.getTransactionOverrides(destination),
140+
),
141+
);
142+
}
143+
144+
protected async deployCCIPIsm(
145+
origin: ChainName,
146+
destination: ChainName,
147+
): Promise<void> {
148+
const cachedIsm = this.ccipContractCache.getIsm(origin, destination);
149+
if (cachedIsm) {
150+
this.logger.debug(
151+
'CCIP ISM already deployed for %s -> %s: %s',
152+
origin,
153+
destination,
154+
cachedIsm,
155+
);
156+
return;
157+
}
158+
159+
const ccipOriginChainSelector = getCCIPChainSelector(origin);
160+
const ccipIsmChainRouterAddress = getCCIPRouterAddress(destination);
161+
assert(
162+
ccipOriginChainSelector,
163+
`CCIP chain selector not found for ${origin}`,
164+
);
165+
assert(
166+
ccipIsmChainRouterAddress,
167+
`CCIP router address not found for ${origin}`,
168+
);
169+
170+
const ccipIsm = await this.deployContractFromFactory(
171+
destination,
172+
new CCIPIsm__factory(),
173+
'CCIPIsm',
174+
[ccipIsmChainRouterAddress, ccipOriginChainSelector],
175+
undefined,
176+
false,
177+
);
178+
179+
this.ccipContractCache.setIsm(origin, destination, ccipIsm);
180+
}
181+
182+
protected async deployCCIPHook(
183+
origin: ChainName,
184+
destination: ChainName,
185+
): Promise<void> {
186+
// Grab the ISM from the cache
187+
const ccipIsmAddress = this.ccipContractCache.getIsm(origin, destination);
188+
assert(
189+
ccipIsmAddress,
190+
`CCIP ISM not found for ${origin} -> ${destination}`,
191+
);
192+
193+
const cachedHook = this.ccipContractCache.getHook(origin, destination);
194+
if (cachedHook) {
195+
this.logger.debug(
196+
'CCIP Hook already deployed for %s -> %s: %s',
197+
origin,
198+
destination,
199+
cachedHook,
200+
);
201+
return;
202+
}
203+
204+
const mailbox = this.core[origin].mailbox;
205+
assert(mailbox, `Mailbox address is required for ${origin}`);
206+
207+
const ccipDestinationChainSelector = getCCIPChainSelector(destination);
208+
const ccipOriginChainRouterAddress = getCCIPRouterAddress(origin);
209+
assert(
210+
ccipDestinationChainSelector,
211+
`CCIP chain selector not found for ${destination}`,
212+
);
213+
assert(
214+
ccipOriginChainRouterAddress,
215+
`CCIP router address not found for ${destination}`,
216+
);
217+
218+
const destinationDomain = this.multiProvider.getDomainId(destination);
219+
220+
const ccipHook = await this.deployContractFromFactory(
221+
origin,
222+
new CCIPHook__factory(),
223+
'CCIPHook',
224+
[
225+
ccipOriginChainRouterAddress,
226+
ccipDestinationChainSelector,
227+
mailbox,
228+
destinationDomain,
229+
addressToBytes32(ccipIsmAddress),
230+
],
231+
undefined,
232+
false,
233+
);
234+
235+
this.ccipContractCache.setHook(origin, destination, ccipHook);
236+
}
237+
}

0 commit comments

Comments
 (0)