@hyperlane-xyz/sdk 41.3.1 → 42.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/dist/ccip/consts.d.ts.map +1 -1
  2. package/dist/ccip/consts.js +20 -0
  3. package/dist/ccip/consts.js.map +1 -1
  4. package/dist/consts/multisigIsm.d.ts.map +1 -1
  5. package/dist/consts/multisigIsm.js +20 -0
  6. package/dist/consts/multisigIsm.js.map +1 -1
  7. package/dist/contracts/contracts.d.ts.map +1 -1
  8. package/dist/contracts/contracts.js +15 -0
  9. package/dist/contracts/contracts.js.map +1 -1
  10. package/dist/contracts/contracts.test.d.ts +2 -0
  11. package/dist/contracts/contracts.test.d.ts.map +1 -0
  12. package/dist/contracts/contracts.test.js +88 -0
  13. package/dist/contracts/contracts.test.js.map +1 -0
  14. package/dist/core/types.d.ts +80 -2602
  15. package/dist/core/types.d.ts.map +1 -1
  16. package/dist/core/types.js +19 -1
  17. package/dist/core/types.js.map +1 -1
  18. package/dist/core/types.test.js +58 -0
  19. package/dist/core/types.test.js.map +1 -1
  20. package/dist/deploy/warp.d.ts +119 -3
  21. package/dist/deploy/warp.d.ts.map +1 -1
  22. package/dist/deploy/warp.js +696 -33
  23. package/dist/deploy/warp.js.map +1 -1
  24. package/dist/deploy/warp.test.d.ts +2 -0
  25. package/dist/deploy/warp.test.d.ts.map +1 -0
  26. package/dist/deploy/warp.test.js +358 -0
  27. package/dist/deploy/warp.test.js.map +1 -0
  28. package/dist/deploy/warpHybridPlan.d.ts +104 -0
  29. package/dist/deploy/warpHybridPlan.d.ts.map +1 -0
  30. package/dist/deploy/warpHybridPlan.js +178 -0
  31. package/dist/deploy/warpHybridPlan.js.map +1 -0
  32. package/dist/deploy/warpHybridPlan.test.d.ts +2 -0
  33. package/dist/deploy/warpHybridPlan.test.d.ts.map +1 -0
  34. package/dist/deploy/warpHybridPlan.test.js +141 -0
  35. package/dist/deploy/warpHybridPlan.test.js.map +1 -0
  36. package/dist/hook/EvmHookModule.d.ts +1 -1
  37. package/dist/hook/EvmHookModule.d.ts.map +1 -1
  38. package/dist/hook/EvmHookModule.js +16 -3
  39. package/dist/hook/EvmHookModule.js.map +1 -1
  40. package/dist/hook/EvmHookReader.d.ts +10 -0
  41. package/dist/hook/EvmHookReader.d.ts.map +1 -1
  42. package/dist/hook/EvmHookReader.js +92 -11
  43. package/dist/hook/EvmHookReader.js.map +1 -1
  44. package/dist/hook/EvmHookReader.test.js +155 -1
  45. package/dist/hook/EvmHookReader.test.js.map +1 -1
  46. package/dist/hook/types.d.ts +107 -1983
  47. package/dist/hook/types.d.ts.map +1 -1
  48. package/dist/hook/types.js +49 -0
  49. package/dist/hook/types.js.map +1 -1
  50. package/dist/hook/types.test.js +21 -1
  51. package/dist/hook/types.test.js.map +1 -1
  52. package/dist/hook/updates.d.ts +1 -0
  53. package/dist/hook/updates.d.ts.map +1 -1
  54. package/dist/hook/updates.js +2 -2
  55. package/dist/hook/updates.js.map +1 -1
  56. package/dist/hook/utils.d.ts +28 -1
  57. package/dist/hook/utils.d.ts.map +1 -1
  58. package/dist/hook/utils.js +89 -0
  59. package/dist/hook/utils.js.map +1 -1
  60. package/dist/ica/types.d.ts +4 -674
  61. package/dist/ica/types.d.ts.map +1 -1
  62. package/dist/index.d.ts +6 -5
  63. package/dist/index.d.ts.map +1 -1
  64. package/dist/index.js +6 -5
  65. package/dist/index.js.map +1 -1
  66. package/dist/ism/EvmIsmModule.d.ts +28 -2
  67. package/dist/ism/EvmIsmModule.d.ts.map +1 -1
  68. package/dist/ism/EvmIsmModule.hardhat-test.js +478 -3
  69. package/dist/ism/EvmIsmModule.hardhat-test.js.map +1 -1
  70. package/dist/ism/EvmIsmModule.js +224 -10
  71. package/dist/ism/EvmIsmModule.js.map +1 -1
  72. package/dist/ism/EvmIsmReader.d.ts.map +1 -1
  73. package/dist/ism/EvmIsmReader.js +77 -1
  74. package/dist/ism/EvmIsmReader.js.map +1 -1
  75. package/dist/ism/EvmIsmReader.test.js +245 -4
  76. package/dist/ism/EvmIsmReader.test.js.map +1 -1
  77. package/dist/ism/HyperlaneIsmFactory.d.ts +14 -4
  78. package/dist/ism/HyperlaneIsmFactory.d.ts.map +1 -1
  79. package/dist/ism/HyperlaneIsmFactory.hardhat-test.js +372 -3
  80. package/dist/ism/HyperlaneIsmFactory.hardhat-test.js.map +1 -1
  81. package/dist/ism/HyperlaneIsmFactory.js +80 -6
  82. package/dist/ism/HyperlaneIsmFactory.js.map +1 -1
  83. package/dist/ism/delayedFlow.d.ts +31 -0
  84. package/dist/ism/delayedFlow.d.ts.map +1 -0
  85. package/dist/ism/delayedFlow.js +33 -0
  86. package/dist/ism/delayedFlow.js.map +1 -0
  87. package/dist/ism/types.d.ts +135 -4
  88. package/dist/ism/types.d.ts.map +1 -1
  89. package/dist/ism/types.js +263 -11
  90. package/dist/ism/types.js.map +1 -1
  91. package/dist/ism/types.test.js +433 -1
  92. package/dist/ism/types.test.js.map +1 -1
  93. package/dist/ism/utils.d.ts.map +1 -1
  94. package/dist/ism/utils.js +113 -3
  95. package/dist/ism/utils.js.map +1 -1
  96. package/dist/ism/utils.test.js +44 -0
  97. package/dist/ism/utils.test.js.map +1 -1
  98. package/dist/providers/MultiProviderAdapter.d.ts +3 -3
  99. package/dist/providers/MultiProviderAdapter.d.ts.map +1 -1
  100. package/dist/providers/MultiProviderAdapter.js +2 -1
  101. package/dist/providers/MultiProviderAdapter.js.map +1 -1
  102. package/dist/providers/SmartProvider/HyperlaneJsonRpcProvider.d.ts.map +1 -1
  103. package/dist/providers/SmartProvider/HyperlaneJsonRpcProvider.js +4 -0
  104. package/dist/providers/SmartProvider/HyperlaneJsonRpcProvider.js.map +1 -1
  105. package/dist/providers/transactionFeeEstimators.d.ts +16 -9
  106. package/dist/providers/transactionFeeEstimators.d.ts.map +1 -1
  107. package/dist/providers/transactionFeeEstimators.js +66 -17
  108. package/dist/providers/transactionFeeEstimators.js.map +1 -1
  109. package/dist/providers/transactionFeeEstimators.test.js +153 -1
  110. package/dist/providers/transactionFeeEstimators.test.js.map +1 -1
  111. package/dist/providers/transactions/submitter/ethersV5/EV5JsonRpcTxSubmitter.d.ts +14 -1
  112. package/dist/providers/transactions/submitter/ethersV5/EV5JsonRpcTxSubmitter.d.ts.map +1 -1
  113. package/dist/providers/transactions/submitter/ethersV5/EV5JsonRpcTxSubmitter.js +46 -5
  114. package/dist/providers/transactions/submitter/ethersV5/EV5JsonRpcTxSubmitter.js.map +1 -1
  115. package/dist/providers/transactions/submitter/ethersV5/EV5JsonRpcTxSubmitter.test.d.ts +2 -0
  116. package/dist/providers/transactions/submitter/ethersV5/EV5JsonRpcTxSubmitter.test.d.ts.map +1 -0
  117. package/dist/providers/transactions/submitter/ethersV5/EV5JsonRpcTxSubmitter.test.js +125 -0
  118. package/dist/providers/transactions/submitter/ethersV5/EV5JsonRpcTxSubmitter.test.js.map +1 -0
  119. package/dist/router/HyperlaneRouterDeployer.d.ts +3 -1
  120. package/dist/router/HyperlaneRouterDeployer.d.ts.map +1 -1
  121. package/dist/router/HyperlaneRouterDeployer.js +4 -2
  122. package/dist/router/HyperlaneRouterDeployer.js.map +1 -1
  123. package/dist/router/types.d.ts +6 -1011
  124. package/dist/router/types.d.ts.map +1 -1
  125. package/dist/test/testUtils.d.ts +1 -1
  126. package/dist/test/testUtils.d.ts.map +1 -1
  127. package/dist/test/testUtils.js +17 -0
  128. package/dist/test/testUtils.js.map +1 -1
  129. package/dist/test/testUtils.test.d.ts +2 -0
  130. package/dist/test/testUtils.test.d.ts.map +1 -0
  131. package/dist/test/testUtils.test.js +28 -0
  132. package/dist/test/testUtils.test.js.map +1 -0
  133. package/dist/token/EvmWarpModule.d.ts +44 -8
  134. package/dist/token/EvmWarpModule.d.ts.map +1 -1
  135. package/dist/token/EvmWarpModule.hardhat-test.js +574 -4
  136. package/dist/token/EvmWarpModule.hardhat-test.js.map +1 -1
  137. package/dist/token/EvmWarpModule.js +237 -25
  138. package/dist/token/EvmWarpModule.js.map +1 -1
  139. package/dist/token/configUtils.d.ts +17 -7
  140. package/dist/token/configUtils.d.ts.map +1 -1
  141. package/dist/token/configUtils.js +108 -0
  142. package/dist/token/configUtils.js.map +1 -1
  143. package/dist/token/configUtils.test.js +155 -1
  144. package/dist/token/configUtils.test.js.map +1 -1
  145. package/dist/token/deploy.d.ts +3 -1
  146. package/dist/token/deploy.d.ts.map +1 -1
  147. package/dist/token/deploy.hardhat-test.js +454 -3
  148. package/dist/token/deploy.hardhat-test.js.map +1 -1
  149. package/dist/token/deploy.js +25 -6
  150. package/dist/token/deploy.js.map +1 -1
  151. package/dist/token/types.d.ts +169 -2931
  152. package/dist/token/types.d.ts.map +1 -1
  153. package/dist/token/types.test.js +48 -0
  154. package/dist/token/types.test.js.map +1 -1
  155. package/dist/token/warpCheck.d.ts.map +1 -1
  156. package/dist/token/warpCheck.js +36 -1
  157. package/dist/token/warpCheck.js.map +1 -1
  158. package/dist/utils/ism.d.ts +167 -1
  159. package/dist/utils/ism.d.ts.map +1 -1
  160. package/dist/utils/ism.js +412 -2
  161. package/dist/utils/ism.js.map +1 -1
  162. package/dist/utils/ism.test.d.ts +2 -0
  163. package/dist/utils/ism.test.d.ts.map +1 -0
  164. package/dist/utils/ism.test.js +372 -0
  165. package/dist/utils/ism.test.js.map +1 -0
  166. package/package.json +10 -10
@@ -3,15 +3,17 @@ import chaiAsPromised from 'chai-as-promised';
3
3
  import { ethers } from 'ethers';
4
4
  import hre from 'hardhat';
5
5
  import sinon from 'sinon';
6
- import { CONTRACTS_PACKAGE_VERSION, CrossCollateralRoutingFee__factory, CrossCollateralRouter__factory, ERC20Test__factory, ERC4626Test__factory, GasRouter__factory, HypERC20__factory, HypERC4626Collateral__factory, HypNative__factory, HypXERC20__factory, LinearFee__factory, MailboxClient__factory, Mailbox__factory, MockEverclearAdapter__factory, MovableCollateralRouter__factory, TokenBridgeCctpV2__factory, XERC20Test__factory, } from '@hyperlane-xyz/core';
7
- import { EvmIsmModule, HookType, IsmType, TestChainName, TokenFeeType, proxyAdmin, proxyImplementation, serializeContracts, } from '@hyperlane-xyz/sdk';
8
- import { addressToBytes32, assert, deepCopy, eqAddress, normalizeAddressEvm, objMap, randomInt, } from '@hyperlane-xyz/utils';
6
+ import { CONTRACTS_PACKAGE_VERSION, CrossCollateralRoutingFee__factory, CrossCollateralRouter__factory, DelayedFlowRouterHookIsm__factory, ERC20Test__factory, ERC4626Test__factory, GasRouter__factory, HypERC20__factory, HypERC4626Collateral__factory, HypNative__factory, HypXERC20__factory, LinearFee__factory, MailboxClient__factory, Mailbox__factory, MockEverclearAdapter__factory, MovableCollateralRouter__factory, StaticAggregationIsm__factory, TokenBridgeCctpV2__factory, TokenRouter__factory, XERC20Test__factory, } from '@hyperlane-xyz/core';
7
+ import { HookType, IsmType, TestChainName, TokenFeeType, proxyAdmin, proxyImplementation, serializeContracts, } from '@hyperlane-xyz/sdk';
8
+ import { addressToBytes32, assert, deepCopy, eqAddress, isZeroishAddress, normalizeAddressEvm, objMap, randomInt, } from '@hyperlane-xyz/utils';
9
9
  import { TestCoreDeployer } from '../core/TestCoreDeployer.js';
10
10
  import { HyperlaneProxyFactoryDeployer } from '../deploy/HyperlaneProxyFactoryDeployer.js';
11
+ import { deriveDelayedFlowEnrollmentTargets } from '../deploy/warp.js';
12
+ import { EvmIsmModule } from '../ism/EvmIsmModule.js';
11
13
  import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory.js';
12
14
  import { MultiProvider } from '../providers/MultiProvider.js';
13
15
  import { randomAddress } from '../test/testUtils.js';
14
- import { normalizeConfig } from '../utils/ism.js';
16
+ import { collectHybridIsmNodes, normalizeConfig } from '../utils/ism.js';
15
17
  import { EvmTokenFeeModule } from '../fee/EvmTokenFeeModule.js';
16
18
  import { DEFAULT_ROUTER_KEY } from '../fee/types.js';
17
19
  import { EvmWarpModule, MAX_LEGACY_BRIDGE_APPROVAL_VERSION, } from './EvmWarpModule.js';
@@ -19,6 +21,13 @@ import { TokenType, isMovableCollateralTokenType, } from './config.js';
19
21
  import { HypTokenRouterConfigSchema, derivedHookAddress, isEverclearTokenBridgeConfig, isMovableCollateralTokenConfig, } from './types.js';
20
22
  chai.use(chaiAsPromised);
21
23
  const { expect } = chai;
24
+ const routerInstallIndex = (txs, router, fn) => {
25
+ const sighash = MailboxClient__factory.createInterface().getSighash(fn);
26
+ return txs.findIndex((tx) => !!tx.to &&
27
+ eqAddress(tx.to, router) &&
28
+ !!tx.data &&
29
+ tx.data.startsWith(sighash));
30
+ };
22
31
  const randomRemoteRouters = (n) => {
23
32
  const routers = {};
24
33
  for (let domain = 0; domain < n; domain++) {
@@ -455,6 +464,40 @@ describe('EvmWarpModule', async () => {
455
464
  expect(normalizeConfig(updatedConfig.hook)).to.deep.equal(hook);
456
465
  }
457
466
  });
467
+ // The batch is submitted sequentially with no rollback, so a failure
468
+ // between the two installs is a state the route can be left in. Installing
469
+ // the hook first leaves the previous ISM verifying inbound messages;
470
+ // installing the ISM first can leave a hybrid hook/ISM gating delivery with
471
+ // nothing driving its postDispatch, which strands every message dispatched
472
+ // in that window.
473
+ it('installs the hook before the ISM when both change', async () => {
474
+ const evmERC20WarpModule = await EvmWarpModule.create({
475
+ chain,
476
+ config: {
477
+ ...baseConfig,
478
+ type: TokenType.native,
479
+ interchainSecurityModule: ismAddress,
480
+ },
481
+ multiProvider,
482
+ proxyFactoryFactories: ismFactoryAddresses,
483
+ });
484
+ const { deployedTokenRoute } = evmERC20WarpModule.serialize();
485
+ const expectedConfig = {
486
+ ...(await evmERC20WarpModule.read()),
487
+ interchainSecurityModule: {
488
+ type: IsmType.PAUSABLE,
489
+ owner: signer.address,
490
+ paused: false,
491
+ },
492
+ hook: { type: HookType.MERKLE_TREE },
493
+ };
494
+ const txs = await evmERC20WarpModule.update(expectedConfig);
495
+ const hookIndex = routerInstallIndex(txs, deployedTokenRoute, 'setHook');
496
+ const ismIndex = routerInstallIndex(txs, deployedTokenRoute, 'setInterchainSecurityModule');
497
+ expect(hookIndex).to.be.greaterThan(-1);
498
+ expect(ismIndex).to.be.greaterThan(-1);
499
+ expect(hookIndex).to.be.lessThan(ismIndex);
500
+ });
458
501
  it('should set new deployed hook mailbox to WarpConfig.owner', async () => {
459
502
  const config = {
460
503
  ...baseConfig,
@@ -2540,6 +2583,533 @@ describe('EvmWarpModule', async () => {
2540
2583
  expect(txs[0].data).to.equal(TokenBridgeCctpV2__factory.createInterface().encodeFunctionData('setMaxFeePpm', [200]));
2541
2584
  });
2542
2585
  });
2586
+ describe('hybrid hook/ISM updates', () => {
2587
+ // Mirrors the shape `warp apply` produces: the hybrid must be composed
2588
+ // under an authenticating ISM, and expandWarpDeployConfig defaults the
2589
+ // expected hook to the hybrid node when the user leaves `hook` unset.
2590
+ function delayedFlowNode(owner, maxDelay = 3600) {
2591
+ return {
2592
+ type: IsmType.DELAYED_FLOW_ROUTER,
2593
+ thresholdBps: 10000,
2594
+ maxDelay,
2595
+ duration: 86400n,
2596
+ owner,
2597
+ };
2598
+ }
2599
+ // Hook-side view of the same contract (same type string by design), which
2600
+ // is what expandWarpDeployConfig puts in the expected `hook` field.
2601
+ function delayedFlowHookNode(owner, maxDelay = 3600) {
2602
+ return {
2603
+ type: HookType.DELAYED_FLOW_ROUTER,
2604
+ thresholdBps: 10000,
2605
+ maxDelay,
2606
+ duration: 86400n,
2607
+ owner,
2608
+ };
2609
+ }
2610
+ // Hook-side view of the OTHER hybrid: same family, different contract.
2611
+ function netFlowHookNode(owner) {
2612
+ return {
2613
+ type: HookType.NET_FLOW_RATE_LIMITED,
2614
+ thresholdBps: 5000,
2615
+ duration: 86400n,
2616
+ owner,
2617
+ };
2618
+ }
2619
+ function delayedFlowIsm(owner, maxDelay = 3600) {
2620
+ return {
2621
+ type: IsmType.AGGREGATION,
2622
+ threshold: 2,
2623
+ modules: [
2624
+ { type: IsmType.TRUSTED_RELAYER, relayer: owner },
2625
+ delayedFlowNode(owner, maxDelay),
2626
+ ],
2627
+ };
2628
+ }
2629
+ // ISM-side view of the OTHER hybrid, with the same parameters as
2630
+ // netFlowHookNode so the pair describes one instance.
2631
+ function netFlowIsm(owner) {
2632
+ return {
2633
+ type: IsmType.AGGREGATION,
2634
+ threshold: 2,
2635
+ modules: [
2636
+ { type: IsmType.TRUSTED_RELAYER, relayer: owner },
2637
+ {
2638
+ type: IsmType.NET_FLOW_RATE_LIMITED,
2639
+ thresholdBps: 5000,
2640
+ duration: 86400n,
2641
+ owner,
2642
+ },
2643
+ ],
2644
+ };
2645
+ }
2646
+ // A route that exists without any hybrid: the starting point for
2647
+ // "user adds a DFR to an existing route".
2648
+ async function createPlainRoute() {
2649
+ return EvmWarpModule.create({
2650
+ chain,
2651
+ config: {
2652
+ ...baseConfig,
2653
+ type: TokenType.collateral,
2654
+ token: token.address,
2655
+ interchainSecurityModule: ismAddress,
2656
+ },
2657
+ multiProvider,
2658
+ proxyFactoryFactories: ismFactoryAddresses,
2659
+ });
2660
+ }
2661
+ it('adds a hybrid hook/ISM to an existing route, wiring it as ISM and hook', async () => {
2662
+ const warpModule = await createPlainRoute();
2663
+ const { deployedTokenRoute } = warpModule.serialize();
2664
+ const expectedConfig = {
2665
+ ...(await warpModule.read()),
2666
+ interchainSecurityModule: delayedFlowIsm(signer.address),
2667
+ hook: delayedFlowHookNode(signer.address),
2668
+ };
2669
+ const txs = await warpModule.update(expectedConfig);
2670
+ await sendTxs(txs);
2671
+ const client = MailboxClient__factory.connect(deployedTokenRoute, multiProvider.getProvider(chain));
2672
+ const installedIsm = await client.interchainSecurityModule();
2673
+ const installedHook = await client.hook();
2674
+ expect(isZeroishAddress(installedHook)).to.be.false;
2675
+ // the hook is the DFR paired with this router...
2676
+ const dfr = DelayedFlowRouterHookIsm__factory.connect(installedHook, multiProvider.getProvider(chain));
2677
+ expect(eqAddress(await dfr.warpRouter(), deployedTokenRoute)).to.be.true;
2678
+ expect(await dfr.maxDelay()).to.equal(3600);
2679
+ // ...and the same instance is a member of the installed aggregation ISM
2680
+ const aggregation = StaticAggregationIsm__factory.connect(installedIsm, multiProvider.getProvider(chain));
2681
+ const [modules] = await aggregation.modulesAndThreshold(ethers.constants.AddressZero);
2682
+ expect(modules.map((m) => m.toLowerCase())).to.include(installedHook.toLowerCase());
2683
+ });
2684
+ // A DFR installed as the ISM before it is the hook refuses every inbound
2685
+ // message (`readyAt == 0`) while nothing sends the preverification, and no
2686
+ // later run can preverify an already-dispatched message: the reverse order
2687
+ // only leaves the previous ISM in charge for the rest of the batch.
2688
+ it('wires the hybrid as the hook before installing it as the ISM', async () => {
2689
+ const warpModule = await createPlainRoute();
2690
+ const { deployedTokenRoute } = warpModule.serialize();
2691
+ const expectedConfig = {
2692
+ ...(await warpModule.read()),
2693
+ interchainSecurityModule: delayedFlowIsm(signer.address),
2694
+ hook: delayedFlowHookNode(signer.address),
2695
+ };
2696
+ const txs = await warpModule.update(expectedConfig);
2697
+ const hookIndex = routerInstallIndex(txs, deployedTokenRoute, 'setHook');
2698
+ const ismIndex = routerInstallIndex(txs, deployedTokenRoute, 'setInterchainSecurityModule');
2699
+ expect(hookIndex).to.be.greaterThan(-1);
2700
+ expect(ismIndex).to.be.greaterThan(-1);
2701
+ expect(hookIndex).to.be.lessThan(ismIndex);
2702
+ });
2703
+ it('removes the hybrid ISM before removing its hook', async () => {
2704
+ const warpModule = await createPlainRoute();
2705
+ const { deployedTokenRoute } = warpModule.serialize();
2706
+ const hybridConfig = {
2707
+ ...(await warpModule.read()),
2708
+ interchainSecurityModule: delayedFlowIsm(signer.address),
2709
+ hook: delayedFlowHookNode(signer.address),
2710
+ };
2711
+ await sendTxs(await warpModule.update(hybridConfig));
2712
+ const client = MailboxClient__factory.connect(deployedTokenRoute, multiProvider.getProvider(chain));
2713
+ const installedHook = await client.hook();
2714
+ const removalConfig = {
2715
+ ...(await warpModule.read()),
2716
+ interchainSecurityModule: ethers.constants.AddressZero,
2717
+ hook: ethers.constants.AddressZero,
2718
+ };
2719
+ const txs = await warpModule.update(removalConfig);
2720
+ const hookIndex = routerInstallIndex(txs, deployedTokenRoute, 'setHook');
2721
+ const ismIndex = routerInstallIndex(txs, deployedTokenRoute, 'setInterchainSecurityModule');
2722
+ expect(ismIndex).to.be.greaterThan(-1);
2723
+ expect(hookIndex).to.be.greaterThan(-1);
2724
+ expect(ismIndex).to.be.lessThan(hookIndex);
2725
+ // If submission stops at this boundary, inbound verification is already
2726
+ // back on the mailbox default while origins may harmlessly send an extra
2727
+ // preverification until their hook-removal phase executes.
2728
+ await sendTxs(txs.slice(0, hookIndex));
2729
+ expect(await client.interchainSecurityModule()).to.equal(ethers.constants.AddressZero);
2730
+ expect(await client.hook()).to.equal(installedHook);
2731
+ await sendTxs(txs.slice(hookIndex));
2732
+ expect(await client.hook()).to.equal(ethers.constants.AddressZero);
2733
+ });
2734
+ it('rejects a hybrid config that also sets a predicateWrapper', async () => {
2735
+ const warpModule = await createPlainRoute();
2736
+ const actualConfig = await warpModule.read();
2737
+ assert(actualConfig.type === TokenType.collateral, 'Expected a collateral warp router config');
2738
+ const expectedConfig = {
2739
+ ...actualConfig,
2740
+ interchainSecurityModule: delayedFlowIsm(signer.address),
2741
+ hook: delayedFlowHookNode(signer.address),
2742
+ predicateWrapper: {
2743
+ predicateRegistry: randomAddress(),
2744
+ policyId: 'test-policy',
2745
+ owner: signer.address,
2746
+ },
2747
+ };
2748
+ // Both want the router's hook slot; silently dropping either would
2749
+ // remove a policy hook or the flow limiter from the dispatch path.
2750
+ await expect(warpModule.update(expectedConfig)).to.be.rejectedWith('both must own');
2751
+ });
2752
+ it('rejects a hybrid added to a router type it cannot meter', async () => {
2753
+ const warpModule = await createPlainRoute();
2754
+ // collateralVault is not an LpCollateralRouter, so localCollateral()
2755
+ // reverts: the DFR would deploy fine and then brick every dispatch and
2756
+ // delivery. The deploy path rejects this; apply must too.
2757
+ const expectedConfig = {
2758
+ ...(await warpModule.read()),
2759
+ type: TokenType.collateralVault,
2760
+ token: token.address,
2761
+ interchainSecurityModule: delayedFlowIsm(signer.address),
2762
+ hook: delayedFlowHookNode(signer.address),
2763
+ };
2764
+ await expect(warpModule.update(expectedConfig)).to.be.rejectedWith('cannot meter');
2765
+ });
2766
+ it('rejects an unsupported hybrid config without deploying the ISM', async () => {
2767
+ const warpModule = await createPlainRoute();
2768
+ // The config-only guards used to run after the ISM step, so a rejected
2769
+ // apply had already paid for (and orphaned) a fresh hybrid instance.
2770
+ const deploySpy = sinon.spy(warpModule, 'deployOrUpdateIsm');
2771
+ const expectedConfig = {
2772
+ ...(await warpModule.read()),
2773
+ type: TokenType.collateralVault,
2774
+ token: token.address,
2775
+ interchainSecurityModule: delayedFlowIsm(signer.address),
2776
+ hook: delayedFlowHookNode(signer.address),
2777
+ };
2778
+ try {
2779
+ await expect(warpModule.update(expectedConfig)).to.be.rejectedWith('cannot meter');
2780
+ expect(deploySpy.called).to.be.false;
2781
+ }
2782
+ finally {
2783
+ deploySpy.restore();
2784
+ }
2785
+ });
2786
+ it('rejects a hybrid ISM tree pointed at a different hook', async () => {
2787
+ const warpModule = await createPlainRoute();
2788
+ // A read -> edit -> apply round trip that keeps an explicit non-hybrid
2789
+ // hook (e.g. an IGP) while adding a DFR to the ISM tree. Without a
2790
+ // guard the ISM deploys and is installed, but the hybrid never becomes
2791
+ // the router's hook, so it can never preverify: every delivery from
2792
+ // this chain would revert forever while `warp check` still converges.
2793
+ const nonHybridHook = await mailbox.defaultHook();
2794
+ const expectedConfig = {
2795
+ ...(await warpModule.read()),
2796
+ interchainSecurityModule: delayedFlowIsm(signer.address),
2797
+ hook: nonHybridHook,
2798
+ };
2799
+ await expect(warpModule.update(expectedConfig)).to.be.rejectedWith("not in the 'hook' tree");
2800
+ });
2801
+ it('rejects a hook node of a different hybrid type than the ISM tree installs', async () => {
2802
+ const warpModule = await createPlainRoute();
2803
+ // The ISM tree installs a DELAYED_FLOW instance while `hook` declares a
2804
+ // NET_FLOW one. Only the tree's own instance is ever wired, so the
2805
+ // declared hook would be silently ignored and the route would stay
2806
+ // permanently divergent from the config the operator wrote.
2807
+ const expectedConfig = {
2808
+ ...(await warpModule.read()),
2809
+ interchainSecurityModule: delayedFlowIsm(signer.address),
2810
+ hook: netFlowHookNode(signer.address),
2811
+ };
2812
+ await expect(warpModule.update(expectedConfig)).to.be.rejectedWith('is declared differently on the two surfaces');
2813
+ });
2814
+ it('ignores derived address metadata when hybrid declarations match', async () => {
2815
+ const warpModule = await createPlainRoute();
2816
+ // `address` is reader output, not declarative identity. Ignoring stale
2817
+ // metadata lets read -> edit -> apply replace a hybrid whose parameters
2818
+ // changed while the two config surfaces still describe the same target.
2819
+ const foreignInstanceHook = {
2820
+ type: HookType.DELAYED_FLOW_ROUTER,
2821
+ thresholdBps: 10000,
2822
+ maxDelay: 3600,
2823
+ duration: 86400n,
2824
+ owner: signer.address,
2825
+ address: randomAddress(),
2826
+ };
2827
+ const expectedConfig = {
2828
+ ...(await warpModule.read()),
2829
+ interchainSecurityModule: delayedFlowIsm(signer.address),
2830
+ hook: foreignInstanceHook,
2831
+ };
2832
+ await expect(warpModule.update(expectedConfig)).to.be.fulfilled;
2833
+ });
2834
+ it('explains a router whose ISM tree has no hybrid instance during enrollment', async () => {
2835
+ const warpModule = await createPlainRoute();
2836
+ const { deployedTokenRoute } = warpModule.serialize();
2837
+ // No hybrid was ever wired, so the enrollment pass cannot resolve a
2838
+ // DelayedFlowRouterHookIsm from the router's ISM tree.
2839
+ const deployConfig = {
2840
+ [chain]: {
2841
+ ...baseConfig,
2842
+ type: TokenType.collateral,
2843
+ token: token.address,
2844
+ interchainSecurityModule: delayedFlowIsm(signer.address),
2845
+ },
2846
+ };
2847
+ await expect(deriveDelayedFlowEnrollmentTargets(multiProvider, deployConfig, {
2848
+ [chain]: deployedTokenRoute,
2849
+ })).to.be.rejectedWith('no instance was resolved');
2850
+ });
2851
+ it('rejects a NetFlowRateLimitedHookIsm as a delayed-flow enrollment target', async () => {
2852
+ const warpModule = await createPlainRoute();
2853
+ const { deployedTokenRoute } = warpModule.serialize();
2854
+ // Wire the OTHER hybrid as the router's hook and ISM.
2855
+ await sendTxs(await warpModule.update({
2856
+ ...(await warpModule.read()),
2857
+ interchainSecurityModule: netFlowIsm(signer.address),
2858
+ hook: netFlowHookNode(signer.address),
2859
+ }));
2860
+ // Both hybrids expose warpRouter() and both are paired with this router,
2861
+ // so a warpRouter()-only probe returns the NetFlow instance as a
2862
+ // DELAYED_FLOW_ROUTER enrollment target and pairs the wrong contract.
2863
+ const deployConfig = {
2864
+ [chain]: {
2865
+ ...baseConfig,
2866
+ type: TokenType.collateral,
2867
+ token: token.address,
2868
+ interchainSecurityModule: delayedFlowIsm(signer.address),
2869
+ },
2870
+ };
2871
+ await expect(deriveDelayedFlowEnrollmentTargets(multiProvider, deployConfig, {
2872
+ [chain]: deployedTokenRoute,
2873
+ })).to.be.rejectedWith('does not expose maxDelay()');
2874
+ });
2875
+ it('converges to zero transactions once the hybrid is wired', async () => {
2876
+ const warpModule = await createPlainRoute();
2877
+ const expectedConfig = {
2878
+ ...(await warpModule.read()),
2879
+ interchainSecurityModule: delayedFlowIsm(signer.address),
2880
+ hook: delayedFlowHookNode(signer.address),
2881
+ };
2882
+ await sendTxs(await warpModule.update(expectedConfig));
2883
+ // second apply of the same config must be a no-op
2884
+ expect(await warpModule.update(expectedConfig)).to.deep.equal([]);
2885
+ });
2886
+ it('round-trips a delayed-flow hybrid nested in an aggregation hook', async () => {
2887
+ const warpModule = await createPlainRoute();
2888
+ const expectedConfig = {
2889
+ ...(await warpModule.read()),
2890
+ interchainSecurityModule: delayedFlowIsm(signer.address),
2891
+ hook: {
2892
+ type: HookType.AGGREGATION,
2893
+ hooks: [
2894
+ delayedFlowHookNode(signer.address),
2895
+ { type: HookType.MERKLE_TREE },
2896
+ ],
2897
+ },
2898
+ };
2899
+ await sendTxs(await warpModule.update(expectedConfig));
2900
+ const readConfig = await warpModule.read();
2901
+ assert(typeof readConfig.hook === 'object' &&
2902
+ readConfig.hook.type === HookType.AGGREGATION, 'Expected the aggregation hook to round-trip');
2903
+ expect(readConfig.hook.hooks.some((hook) => typeof hook === 'object' &&
2904
+ hook.type === HookType.DELAYED_FLOW_ROUTER)).to.be.true;
2905
+ const { deployedTokenRoute } = warpModule.serialize();
2906
+ const deployConfig = {
2907
+ [chain]: {
2908
+ ...baseConfig,
2909
+ type: TokenType.collateral,
2910
+ token: token.address,
2911
+ interchainSecurityModule: delayedFlowIsm(signer.address),
2912
+ },
2913
+ };
2914
+ const targets = await deriveDelayedFlowEnrollmentTargets(multiProvider, deployConfig, { [chain]: deployedTokenRoute });
2915
+ const delayedHook = readConfig.hook.hooks.find((hook) => typeof hook === 'object' &&
2916
+ hook.type === HookType.DELAYED_FLOW_ROUTER);
2917
+ assert(delayedHook &&
2918
+ typeof delayedHook === 'object' &&
2919
+ 'address' in delayedHook, 'Expected the delayed-flow hook address');
2920
+ expect(targets[chain].ismAddress).to.equal(delayedHook.address);
2921
+ expect(await warpModule.update(readConfig)).to.deep.equal([]);
2922
+ });
2923
+ it('rejects an aggregated hybrid when warp apply would retain an existing fee hook', async () => {
2924
+ const warpModule = await createPlainRoute();
2925
+ const feeHook = randomAddress();
2926
+ await sendTxs(await warpModule.update({
2927
+ ...(await warpModule.read()),
2928
+ feeHook,
2929
+ }));
2930
+ const actualConfig = await warpModule.read();
2931
+ expect(actualConfig.feeHook && eqAddress(actualConfig.feeHook, feeHook)).to.equal(true);
2932
+ const expectedConfig = {
2933
+ ...actualConfig,
2934
+ // Omission means "leave unchanged" during warp apply.
2935
+ feeHook: undefined,
2936
+ interchainSecurityModule: delayedFlowIsm(signer.address),
2937
+ hook: {
2938
+ type: HookType.AGGREGATION,
2939
+ hooks: [
2940
+ delayedFlowHookNode(signer.address),
2941
+ { type: HookType.MERKLE_TREE },
2942
+ ],
2943
+ },
2944
+ tokenFee: {
2945
+ type: TokenFeeType.LinearFee,
2946
+ owner: signer.address,
2947
+ bps: 100,
2948
+ },
2949
+ };
2950
+ const nonceBefore = await signer.getTransactionCount();
2951
+ await expect(warpModule.update(expectedConfig)).to.be.rejectedWith('cannot be combined with non-zero feeHook');
2952
+ await expect(warpModule.update({
2953
+ ...expectedConfig,
2954
+ feeHook: ethers.constants.AddressZero,
2955
+ })).to.be.rejectedWith('while clearing existing feeHook');
2956
+ expect(await signer.getTransactionCount()).to.equal(nonceBefore);
2957
+ });
2958
+ it('requires fee-hook clearing before replacing or recomposing an installed delayed-flow hook', async () => {
2959
+ const warpModule = await createPlainRoute();
2960
+ const initialConfig = {
2961
+ ...(await warpModule.read()),
2962
+ interchainSecurityModule: delayedFlowIsm(signer.address),
2963
+ hook: delayedFlowHookNode(signer.address),
2964
+ };
2965
+ await sendTxs(await warpModule.update(initialConfig));
2966
+ const { deployedTokenRoute } = warpModule.serialize();
2967
+ const feeHook = randomAddress();
2968
+ await TokenRouter__factory.connect(deployedTokenRoute, signer).setFeeHook(feeHook);
2969
+ const actualConfig = await warpModule.read();
2970
+ assert(typeof actualConfig.hook === 'object' &&
2971
+ actualConfig.hook.type === HookType.DELAYED_FLOW_ROUTER, 'Expected an installed delayed-flow hook');
2972
+ const nonceBefore = await signer.getTransactionCount();
2973
+ await expect(warpModule.update({
2974
+ ...actualConfig,
2975
+ feeHook: ethers.constants.AddressZero,
2976
+ interchainSecurityModule: delayedFlowIsm(signer.address, 7200),
2977
+ hook: delayedFlowHookNode(signer.address, 7200),
2978
+ })).to.be.rejectedWith('introduce, replace, or recompose');
2979
+ await expect(warpModule.update({
2980
+ ...actualConfig,
2981
+ feeHook: ethers.constants.AddressZero,
2982
+ hook: {
2983
+ type: HookType.AGGREGATION,
2984
+ hooks: [actualConfig.hook, { type: HookType.MERKLE_TREE }],
2985
+ },
2986
+ })).to.be.rejectedWith('introduce, replace, or recompose');
2987
+ expect(await signer.getTransactionCount()).to.equal(nonceBefore);
2988
+ const clearTxs = await warpModule.update({
2989
+ ...actualConfig,
2990
+ feeHook: ethers.constants.AddressZero,
2991
+ });
2992
+ expect(clearTxs.some((tx) => tx.data?.startsWith(MailboxClient__factory.createInterface().getSighash('setHook')) ?? false)).to.equal(false);
2993
+ await sendTxs(clearTxs);
2994
+ expect((await warpModule.read()).feeHook).to.equal(undefined);
2995
+ });
2996
+ it('rejects an existing hybrid owned separately from its router', async () => {
2997
+ const warpModule = await createPlainRoute();
2998
+ const { deployedTokenRoute } = warpModule.serialize();
2999
+ const expectedConfig = {
3000
+ ...(await warpModule.read()),
3001
+ interchainSecurityModule: delayedFlowIsm(signer.address),
3002
+ hook: delayedFlowHookNode(signer.address),
3003
+ };
3004
+ await sendTxs(await warpModule.update(expectedConfig));
3005
+ const client = MailboxClient__factory.connect(deployedTokenRoute, multiProvider.getProvider(chain));
3006
+ const hybrid = DelayedFlowRouterHookIsm__factory.connect(await client.hook(), signer);
3007
+ const separateOwner = randomAddress();
3008
+ await hybrid.transferOwnership(separateOwner);
3009
+ await expect(warpModule.update(expectedConfig)).to.be.rejectedWith('is owned by');
3010
+ });
3011
+ it('hands a replacement hybrid and its router to the new owner last', async () => {
3012
+ const warpModule = await createPlainRoute();
3013
+ const initialConfig = {
3014
+ ...(await warpModule.read()),
3015
+ interchainSecurityModule: delayedFlowIsm(signer.address),
3016
+ hook: delayedFlowHookNode(signer.address),
3017
+ };
3018
+ await sendTxs(await warpModule.update(initialConfig));
3019
+ const newOwner = randomAddress();
3020
+ const expectedConfig = {
3021
+ ...(await warpModule.read()),
3022
+ owner: newOwner,
3023
+ interchainSecurityModule: delayedFlowIsm(newOwner, 7200),
3024
+ hook: delayedFlowHookNode(newOwner, 7200),
3025
+ };
3026
+ const phases = await warpModule.updatePhases(expectedConfig);
3027
+ expect(phases.ownershipTxs).to.have.length(2);
3028
+ await sendTxs([
3029
+ ...phases.upgradeTxs,
3030
+ ...phases.instanceTxs,
3031
+ ...phases.hookTxs,
3032
+ ...phases.ismTxs,
3033
+ ...phases.txs,
3034
+ ...phases.feeTxs,
3035
+ ...phases.ownershipTxs,
3036
+ ]);
3037
+ const actual = await warpModule.read();
3038
+ expect(eqAddress(actual.owner, newOwner)).to.be.true;
3039
+ const hybrid = collectHybridIsmNodes(actual.interchainSecurityModule)[0];
3040
+ assert(hybrid.type === IsmType.DELAYED_FLOW_ROUTER, 'Expected delayed-flow hybrid');
3041
+ expect(hybrid.owner && eqAddress(hybrid.owner, newOwner)).to.be.true;
3042
+ expect(hybrid.maxDelay).to.equal(7200);
3043
+ });
3044
+ it('returns hybrid instance mutations before installation and ownership', async () => {
3045
+ const warpModule = await createPlainRoute();
3046
+ const initialConfig = {
3047
+ ...(await warpModule.read()),
3048
+ interchainSecurityModule: delayedFlowIsm(signer.address),
3049
+ hook: delayedFlowHookNode(signer.address),
3050
+ };
3051
+ await sendTxs(await warpModule.update(initialConfig));
3052
+ const instanceTx = {
3053
+ chainId: Number(multiProvider.getChainId(chain)),
3054
+ to: randomAddress(),
3055
+ data: '0x1234',
3056
+ };
3057
+ const updateStub = sinon
3058
+ .stub(EvmIsmModule.prototype, 'updateDeployedInstance')
3059
+ .resolves([instanceTx]);
3060
+ try {
3061
+ const currentConfig = await warpModule.read();
3062
+ const currentHybrid = collectHybridIsmNodes(currentConfig.interchainSecurityModule)[0];
3063
+ assert(currentHybrid && 'address' in currentHybrid, 'Expected an installed delayed-flow hybrid');
3064
+ const targetConfig = {
3065
+ ...currentConfig,
3066
+ interchainSecurityModule: delayedFlowIsm(signer.address),
3067
+ hook: delayedFlowHookNode(signer.address),
3068
+ };
3069
+ const phases = await warpModule.updatePhases(targetConfig);
3070
+ expect(updateStub.calledOnce).to.be.true;
3071
+ expect(phases.instanceTxs).to.deep.equal([instanceTx]);
3072
+ expect(phases.ownershipTxs).not.to.include(instanceTx);
3073
+ }
3074
+ finally {
3075
+ updateStub.restore();
3076
+ }
3077
+ });
3078
+ it('resumes after hybrid ownership transfers but router ownership does not', async () => {
3079
+ const warpModule = await createPlainRoute();
3080
+ const initialConfig = {
3081
+ ...(await warpModule.read()),
3082
+ interchainSecurityModule: delayedFlowIsm(signer.address),
3083
+ hook: delayedFlowHookNode(signer.address),
3084
+ };
3085
+ await sendTxs(await warpModule.update(initialConfig));
3086
+ const newOwner = randomAddress();
3087
+ const expectedConfig = {
3088
+ ...(await warpModule.read()),
3089
+ owner: newOwner,
3090
+ interchainSecurityModule: delayedFlowIsm(newOwner),
3091
+ hook: delayedFlowHookNode(newOwner),
3092
+ };
3093
+ const interruptedPhases = await warpModule.updatePhases(expectedConfig);
3094
+ expect(interruptedPhases.ownershipTxs).to.have.length(2);
3095
+ await sendTxs([interruptedPhases.ownershipTxs[0]]);
3096
+ const nonceBeforeRejectedChange = await signer.getTransactionCount();
3097
+ await expect(warpModule.updatePhases({
3098
+ ...expectedConfig,
3099
+ interchainSecurityModule: delayedFlowIsm(newOwner, 7200),
3100
+ hook: delayedFlowHookNode(newOwner, 7200),
3101
+ })).to.be.rejectedWith('was already transferred to the target owner');
3102
+ expect(await signer.getTransactionCount()).to.equal(nonceBeforeRejectedChange);
3103
+ const resumedPhases = await warpModule.updatePhases(expectedConfig);
3104
+ expect(resumedPhases.ownershipTxs).to.have.length(1);
3105
+ await sendTxs(resumedPhases.ownershipTxs);
3106
+ const actual = await warpModule.read();
3107
+ expect(eqAddress(actual.owner, newOwner)).to.be.true;
3108
+ const hybrid = collectHybridIsmNodes(actual.interchainSecurityModule)[0];
3109
+ assert(hybrid.type === IsmType.DELAYED_FLOW_ROUTER, 'Expected delayed-flow hybrid');
3110
+ expect(hybrid.owner && eqAddress(hybrid.owner, newOwner)).to.be.true;
3111
+ });
3112
+ });
2543
3113
  describe('createHookUpdateTxs', () => {
2544
3114
  const ROUTE_ADDRESS = '0x1111111111111111111111111111111111111111';
2545
3115
  const TOKEN_ADDRESS = '0x2222222222222222222222222222222222222222';