Skip to content

Commit 291cf50

Browse files
committed
validate USDT params for LayerZero bridge
1 parent 869f6f2 commit 291cf50

2 files changed

Lines changed: 91 additions & 0 deletions

File tree

typescript/rebalancer/src/bridges/LayerZeroBridge.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
ARB_HUB_EID,
1212
getRouteNetwork,
1313
getOFTContractForRoute,
14+
getUSDTAddress,
1415
getEID,
1516
SOLANA_CHAIN_ID,
1617
SOLANA_OFT_PROGRAM,
@@ -247,11 +248,72 @@ describe('LayerZeroBridge', function () {
247248
expect(quote.toAmount).to.equal(9997000n);
248249
});
249250

251+
it('rejects non-USDT fromToken params', async () => {
252+
let threw = false;
253+
try {
254+
await bridge.quote({
255+
...BASE_PARAMS,
256+
fromToken: '0x1111111111111111111111111111111111111111',
257+
fromAmount: 10000000000n,
258+
});
259+
} catch (error) {
260+
threw = true;
261+
expect((error as Error).message).to.match(/USDT-only.*fromToken/i);
262+
}
263+
expect(threw).to.equal(true);
264+
});
265+
266+
it('rejects non-USDT toToken params', async () => {
267+
let threw = false;
268+
try {
269+
await bridge.quote({
270+
...BASE_PARAMS,
271+
toToken: '0x1111111111111111111111111111111111111111',
272+
fromAmount: 10000000000n,
273+
});
274+
} catch (error) {
275+
threw = true;
276+
expect((error as Error).message).to.match(/USDT-only.*toToken/i);
277+
}
278+
expect(threw).to.equal(true);
279+
});
280+
281+
it('accepts mixed-case EVM USDT token params', async () => {
282+
const quoteOFTResponse = createMockQuoteOFTResponse();
283+
const quoteSendResponse = createMockQuoteSendResponse();
284+
285+
const quoteOFTStub = sinon
286+
.stub()
287+
.resolves([
288+
quoteOFTResponse.oftLimit,
289+
quoteOFTResponse.oftFeeDetails,
290+
quoteOFTResponse.oftReceipt,
291+
]);
292+
const quoteSendStub = sinon.stub().resolves([quoteSendResponse]);
293+
stubContractConstructor(() => ({
294+
quoteOFT: quoteOFTStub,
295+
quoteSend: quoteSendStub,
296+
}));
297+
298+
const quote = await bridge.quote({
299+
...BASE_PARAMS,
300+
fromToken: BASE_PARAMS.fromToken.toUpperCase(),
301+
toToken: BASE_PARAMS.toToken.toUpperCase(),
302+
fromAmount: 10000000000n,
303+
});
304+
305+
expect(quote.tool).to.equal('layerzero');
306+
expect(quoteOFTStub.calledOnce).to.equal(true);
307+
expect(quoteSendStub.calledOnce).to.equal(true);
308+
});
309+
250310
it('quotes non-Solana compose routes through the Arbitrum hub', async () => {
251311
const composeParams = {
252312
...BASE_PARAMS,
253313
fromChain: TRON_CHAIN_ID,
254314
toChain: 9745,
315+
fromToken: getUSDTAddress(TRON_CHAIN_ID),
316+
toToken: getUSDTAddress(9745),
255317
fromAmount: 10000000000n,
256318
};
257319
const { firstHopOFT, secondHopOFT } = getComposeHopContracts(
@@ -327,6 +389,8 @@ describe('LayerZeroBridge', function () {
327389
...BASE_PARAMS,
328390
fromChain: TRON_CHAIN_ID,
329391
toChain: 9745,
392+
fromToken: getUSDTAddress(TRON_CHAIN_ID),
393+
toToken: getUSDTAddress(9745),
330394
fromAmount: 10000000000n,
331395
};
332396
const { firstHopOFT, secondHopOFT } = getComposeHopContracts(

typescript/rebalancer/src/bridges/LayerZeroBridge.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ export class LayerZeroBridge implements IExternalBridge {
8686
fromAmount !== undefined || toAmount !== undefined,
8787
'Must specify either fromAmount or toAmount',
8888
);
89+
this.assertUsdtRoute(params);
8990

9091
const network = getRouteNetwork(fromChain, toChain);
9192
assert(network, `Unsupported route: ${fromChain} -> ${toChain}`);
@@ -744,4 +745,30 @@ export class LayerZeroBridge implements IExternalBridge {
744745
assert(rpcUrl, `No RPC URL configured for chain ${chainId}`);
745746
return rpcUrl;
746747
}
748+
749+
private assertUsdtRoute(params: BridgeQuoteParams): void {
750+
const { fromChain, toChain, fromToken, toToken } = params;
751+
const expectedFromToken = getUSDTAddress(fromChain);
752+
const expectedToToken = getUSDTAddress(toChain);
753+
754+
assert(
755+
this.matchesBridgeToken(fromToken, expectedFromToken),
756+
`LayerZero bridge is USDT-only: fromToken ${fromToken} does not match USDT ${expectedFromToken} on chain ${fromChain}`,
757+
);
758+
assert(
759+
this.matchesBridgeToken(toToken, expectedToToken),
760+
`LayerZero bridge is USDT-only: toToken ${toToken} does not match USDT ${expectedToToken} on chain ${toChain}`,
761+
);
762+
}
763+
764+
private matchesBridgeToken(
765+
actualToken: string,
766+
expectedToken: string,
767+
): boolean {
768+
if (/^0x/i.test(actualToken) && /^0x/i.test(expectedToken)) {
769+
return actualToken.toLowerCase() === expectedToken.toLowerCase();
770+
}
771+
772+
return actualToken === expectedToken;
773+
}
747774
}

0 commit comments

Comments
 (0)