Skip to content

Commit 0105e2b

Browse files
authored
fix(rebalancer): skip provider init for non-EVM warp route chains (#8281)
1 parent 5c6c65b commit 0105e2b

2 files changed

Lines changed: 298 additions & 3 deletions

File tree

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
import { expect } from 'chai';
2+
import { pino } from 'pino';
3+
import Sinon from 'sinon';
4+
5+
import {
6+
type MultiProtocolProvider,
7+
MultiProvider,
8+
type WarpCoreConfig,
9+
} from '@hyperlane-xyz/sdk';
10+
import { ProtocolType } from '@hyperlane-xyz/utils';
11+
12+
import type { RebalancerConfig } from '../config/RebalancerConfig.js';
13+
import {
14+
DEFAULT_INTENT_TTL_MS,
15+
RebalancerStrategyOptions,
16+
} from '../config/types.js';
17+
import { TEST_ADDRESSES } from '../test/helpers.js';
18+
19+
import { RebalancerContextFactory } from './RebalancerContextFactory.js';
20+
21+
const testLogger = pino({ level: 'silent' });
22+
23+
function createMockRegistry() {
24+
return {
25+
getAddresses: Sinon.stub().resolves({
26+
ethereum: { mailbox: TEST_ADDRESSES.ethereum },
27+
arbitrum: { mailbox: TEST_ADDRESSES.arbitrum },
28+
paradex: { mailbox: TEST_ADDRESSES.polygon },
29+
}),
30+
getWarpRoute: Sinon.stub().resolves(null),
31+
getChainAddresses: Sinon.stub().resolves({
32+
mailbox: TEST_ADDRESSES.ethereum,
33+
}),
34+
getWarpDeployConfig: Sinon.stub().resolves({}),
35+
} as any;
36+
}
37+
38+
function createMockConfig(): RebalancerConfig {
39+
return {
40+
warpRouteId: 'USDC/paradex',
41+
strategyConfig: [
42+
{
43+
rebalanceStrategy: RebalancerStrategyOptions.Weighted,
44+
chains: {
45+
ethereum: {
46+
bridge: TEST_ADDRESSES.bridge,
47+
bridgeMinAcceptedAmount: 0,
48+
weighted: { weight: 50n, tolerance: 10n },
49+
},
50+
arbitrum: {
51+
bridge: TEST_ADDRESSES.bridge,
52+
bridgeMinAcceptedAmount: 0,
53+
weighted: { weight: 50n, tolerance: 10n },
54+
},
55+
},
56+
},
57+
],
58+
intentTTL: DEFAULT_INTENT_TTL_MS,
59+
} as RebalancerConfig;
60+
}
61+
62+
describe('RebalancerContextFactory', () => {
63+
let sandbox: Sinon.SinonSandbox;
64+
65+
beforeEach(() => {
66+
sandbox = Sinon.createSandbox();
67+
});
68+
69+
afterEach(() => {
70+
sandbox.restore();
71+
});
72+
73+
describe('create() — non-EVM chain handling', () => {
74+
it('should skip provider initialization for StarkNet chains', async () => {
75+
const getProviderStub = Sinon.stub();
76+
const getProtocolStub = Sinon.stub().callsFake((chain: string) => {
77+
if (chain === 'paradex') return ProtocolType.Starknet;
78+
return ProtocolType.Ethereum;
79+
});
80+
81+
const multiProvider = {
82+
getProvider: getProviderStub,
83+
getProtocol: getProtocolStub,
84+
metadata: {
85+
ethereum: {
86+
name: 'ethereum',
87+
chainId: 1,
88+
protocol: ProtocolType.Ethereum,
89+
},
90+
arbitrum: {
91+
name: 'arbitrum',
92+
chainId: 42161,
93+
protocol: ProtocolType.Ethereum,
94+
},
95+
paradex: {
96+
name: 'paradex',
97+
chainId:
98+
'0x505249564154455f534e5f50415241434c4541525f4d41494e4e4554',
99+
protocol: ProtocolType.Starknet,
100+
},
101+
},
102+
} as unknown as MultiProvider;
103+
104+
const mockMpp = {
105+
extendChainMetadata: Sinon.stub().returnsThis(),
106+
} as unknown as MultiProtocolProvider;
107+
108+
const warpCoreConfig: WarpCoreConfig = {
109+
tokens: [
110+
{
111+
chainName: 'ethereum',
112+
addressOrDenom: TEST_ADDRESSES.ethereum,
113+
standard: 'EvmHypCollateral',
114+
decimals: 6,
115+
symbol: 'USDC',
116+
name: 'USDC',
117+
},
118+
{
119+
chainName: 'arbitrum',
120+
addressOrDenom: TEST_ADDRESSES.arbitrum,
121+
standard: 'EvmHypSynthetic',
122+
decimals: 6,
123+
symbol: 'USDC',
124+
name: 'USDC',
125+
},
126+
{
127+
chainName: 'paradex',
128+
addressOrDenom: '0xparadex',
129+
standard: 'StarknetHypSynthetic',
130+
decimals: 6,
131+
symbol: 'USDC',
132+
name: 'USDC',
133+
},
134+
],
135+
options: {},
136+
} as any;
137+
138+
const registry = createMockRegistry();
139+
140+
await RebalancerContextFactory.create(
141+
createMockConfig(),
142+
multiProvider,
143+
undefined,
144+
mockMpp,
145+
registry,
146+
testLogger,
147+
warpCoreConfig,
148+
);
149+
150+
// getProvider should be called for EVM chains only
151+
expect(getProviderStub.callCount).to.equal(2);
152+
const providerChains = getProviderStub.getCalls().map((c) => c.args[0]);
153+
expect(providerChains).to.include('ethereum');
154+
expect(providerChains).to.include('arbitrum');
155+
expect(providerChains).to.not.include('paradex');
156+
});
157+
158+
it('should skip provider initialization for Sealevel chains', async () => {
159+
const getProviderStub = Sinon.stub();
160+
const getProtocolStub = Sinon.stub().callsFake((chain: string) => {
161+
if (chain === 'solana') return ProtocolType.Sealevel;
162+
return ProtocolType.Ethereum;
163+
});
164+
165+
const multiProvider = {
166+
getProvider: getProviderStub,
167+
getProtocol: getProtocolStub,
168+
metadata: {
169+
ethereum: {
170+
name: 'ethereum',
171+
chainId: 1,
172+
protocol: ProtocolType.Ethereum,
173+
},
174+
solana: {
175+
name: 'solana',
176+
chainId: 'solana-mainnet',
177+
protocol: ProtocolType.Sealevel,
178+
},
179+
},
180+
} as unknown as MultiProvider;
181+
182+
const mockMpp = {
183+
extendChainMetadata: Sinon.stub().returnsThis(),
184+
} as unknown as MultiProtocolProvider;
185+
186+
const warpCoreConfig: WarpCoreConfig = {
187+
tokens: [
188+
{
189+
chainName: 'ethereum',
190+
addressOrDenom: TEST_ADDRESSES.ethereum,
191+
standard: 'EvmHypCollateral',
192+
decimals: 6,
193+
symbol: 'USDC',
194+
name: 'USDC',
195+
},
196+
{
197+
chainName: 'solana',
198+
addressOrDenom: 'SolToken111',
199+
standard: 'SealevelHypSynthetic',
200+
decimals: 6,
201+
symbol: 'USDC',
202+
name: 'USDC',
203+
},
204+
],
205+
options: {},
206+
} as any;
207+
208+
const registry = createMockRegistry();
209+
210+
await RebalancerContextFactory.create(
211+
createMockConfig(),
212+
multiProvider,
213+
undefined,
214+
mockMpp,
215+
registry,
216+
testLogger,
217+
warpCoreConfig,
218+
);
219+
220+
expect(getProviderStub.callCount).to.equal(1);
221+
expect(getProviderStub.firstCall.args[0]).to.equal('ethereum');
222+
});
223+
224+
it('should call getProvider for all chains when all are EVM', async () => {
225+
const getProviderStub = Sinon.stub();
226+
const getProtocolStub = Sinon.stub().returns(ProtocolType.Ethereum);
227+
228+
const multiProvider = {
229+
getProvider: getProviderStub,
230+
getProtocol: getProtocolStub,
231+
metadata: {
232+
ethereum: {
233+
name: 'ethereum',
234+
chainId: 1,
235+
protocol: ProtocolType.Ethereum,
236+
},
237+
arbitrum: {
238+
name: 'arbitrum',
239+
chainId: 42161,
240+
protocol: ProtocolType.Ethereum,
241+
},
242+
},
243+
} as unknown as MultiProvider;
244+
245+
const mockMpp = {
246+
extendChainMetadata: Sinon.stub().returnsThis(),
247+
} as unknown as MultiProtocolProvider;
248+
249+
const warpCoreConfig: WarpCoreConfig = {
250+
tokens: [
251+
{
252+
chainName: 'ethereum',
253+
addressOrDenom: TEST_ADDRESSES.ethereum,
254+
standard: 'EvmHypCollateral',
255+
decimals: 6,
256+
symbol: 'USDC',
257+
name: 'USDC',
258+
},
259+
{
260+
chainName: 'arbitrum',
261+
addressOrDenom: TEST_ADDRESSES.arbitrum,
262+
standard: 'EvmHypSynthetic',
263+
decimals: 6,
264+
symbol: 'USDC',
265+
name: 'USDC',
266+
},
267+
],
268+
options: {},
269+
} as any;
270+
271+
const registry = createMockRegistry();
272+
273+
await RebalancerContextFactory.create(
274+
createMockConfig(),
275+
multiProvider,
276+
undefined,
277+
mockMpp,
278+
registry,
279+
testLogger,
280+
warpCoreConfig,
281+
);
282+
283+
expect(getProviderStub.callCount).to.equal(2);
284+
const providerChains = getProviderStub.getCalls().map((c) => c.args[0]);
285+
expect(providerChains).to.include('ethereum');
286+
expect(providerChains).to.include('arbitrum');
287+
});
288+
});
289+
});

typescript/rebalancer/src/factories/RebalancerContextFactory.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
WarpCore,
1212
type WarpCoreConfig,
1313
} from '@hyperlane-xyz/sdk';
14-
import { objMap, toWei } from '@hyperlane-xyz/utils';
14+
import { ProtocolType, objMap, toWei } from '@hyperlane-xyz/utils';
1515

1616
import { LiFiBridge } from '../bridges/LiFiBridge.js';
1717
import { type RebalancerConfig } from '../config/RebalancerConfig.js';
@@ -119,12 +119,18 @@ export class RebalancerContextFactory {
119119
);
120120
}
121121

122-
// Force-initialize providers for all warp route chains
123-
// This ensures fromMultiProvider() snapshots actual provider instances
122+
// Force-initialize providers for EVM warp route chains only.
123+
// This ensures fromMultiProvider() snapshots actual provider instances.
124+
// Non-EVM chains (StarkNet, Sealevel, etc.) don't use ethers providers
125+
// and would crash if we tried to build one (e.g. non-numeric chainId).
124126
const warpChains = [
125127
...new Set(warpCoreConfig.tokens.map((t: any) => t.chainName)),
126128
];
127129
for (const chain of warpChains) {
130+
if (multiProvider.getProtocol(chain) !== ProtocolType.Ethereum) {
131+
logger.info({ chain }, 'Skipping provider init for non-EVM chain');
132+
continue;
133+
}
128134
multiProvider.getProvider(chain);
129135
}
130136

0 commit comments

Comments
 (0)