Skip to content

Commit 02a666f

Browse files
authored
feat: Handle cases where a deposit's inputToken and originChain cannot be mapped to a PoolRebalanceRoute (#2177)
* feat: Handle cases where a deposit's inputToken and originChain cannot be mapped to a PoolRebalanceRoute Relayer: - Refuses to fill such deposits Dataworker: - Does not let such deposits affect bundle LP fees and running balance calculations * Update InventoryClient.ts * more cases * add another early exit to Relayer#filterDeposit * fixes based on latest sdk changes * improve checks * Update FillUtils.ts * Add casework for when outputToken = 0x0 * Update test/Dataworker.loadData.deposit.ts * Update test/Dataworker.loadData.deposit.ts * fix * Update InventoryClient.RefundChain.ts * Update InventoryClient.RefundChain.ts * Remove test * update package * update * Update Dataworker.loadData.fill.ts * Update Dataworker.loadData.fill.ts * Update InventoryClient.RefundChain.ts
1 parent a003a5e commit 02a666f

11 files changed

+117
-126
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"dependencies": {
1313
"@across-protocol/constants": "^3.1.52",
1414
"@across-protocol/contracts": "^4.0.5",
15-
"@across-protocol/sdk": "4.1.44",
15+
"@across-protocol/sdk": "4.1.45",
1616
"@arbitrum/sdk": "^4.0.2",
1717
"@consensys/linea-sdk": "^0.2.1",
1818
"@defi-wonderland/smock": "^2.3.5",

src/clients/InventoryClient.ts

+11
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
Profiler,
2929
getNativeTokenSymbol,
3030
getL1TokenInfo,
31+
depositHasPoolRebalanceRouteMapping,
3132
} from "../utils";
3233
import { HubPoolClient, TokenClient, BundleDataClient } from ".";
3334
import { Deposit, ProposedRootBundle } from "../interfaces";
@@ -438,6 +439,16 @@ export class InventoryClient {
438439
const { originChainId, destinationChainId, inputToken, outputToken, inputAmount } = deposit;
439440
const hubChainId = this.hubPoolClient.chainId;
440441

442+
// If the token cannot be mapped to any PoolRebalanceRoute, then the decision for now is to return zero repayment
443+
// chains and force the relayer to ignore this deposit.
444+
if (!depositHasPoolRebalanceRouteMapping(deposit, this.hubPoolClient)) {
445+
return [];
446+
}
447+
448+
if (sdkUtils.invalidOutputToken(deposit)) {
449+
return [];
450+
}
451+
441452
if (!this.isInventoryManagementEnabled()) {
442453
return [deposit.fromLiteChain ? originChainId : destinationChainId];
443454
}

src/dataworker/DataworkerUtils.ts

+13-2
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,16 @@ export function _buildRelayerRefundRoot(
237237
Object.entries(combinedRefunds).forEach(([_repaymentChainId, refundsForChain]) => {
238238
const repaymentChainId = Number(_repaymentChainId);
239239
Object.entries(refundsForChain).forEach(([l2TokenAddress, refunds]) => {
240+
// If the token cannot be mapped to any PoolRebalanceRoute, then the amount to return must be 0 since there
241+
// is no way to send the token back to the HubPool.
242+
if (!clients.hubPoolClient.l2TokenHasPoolRebalanceRoute(l2TokenAddress, repaymentChainId, endBlockForMainnet)) {
243+
relayerRefundLeaves.push(
244+
..._getRefundLeaves(refunds, bnZero, repaymentChainId, l2TokenAddress, maxRefundCount)
245+
);
246+
return;
247+
}
248+
// If the token can be mapped to a PoolRebalanceRoute, then we need to calculate the amount to return based
249+
// on its running balances.
240250
const l1TokenCounterpart = clients.hubPoolClient.getL1TokenForL2TokenAtBlock(
241251
l2TokenAddress,
242252
repaymentChainId,
@@ -255,8 +265,9 @@ export function _buildRelayerRefundRoot(
255265
runningBalances[repaymentChainId][l1TokenCounterpart]
256266
);
257267

258-
const _refundLeaves = _getRefundLeaves(refunds, amountToReturn, repaymentChainId, l2TokenAddress, maxRefundCount);
259-
relayerRefundLeaves.push(..._refundLeaves);
268+
relayerRefundLeaves.push(
269+
..._getRefundLeaves(refunds, amountToReturn, repaymentChainId, l2TokenAddress, maxRefundCount)
270+
);
260271
});
261272
});
262273

src/relayer/Relayer.ts

+20
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
Profiler,
2323
formatGwei,
2424
toBytes32,
25+
depositHasPoolRebalanceRouteMapping,
2526
} from "../utils";
2627
import { RelayerClients } from "./RelayerClientHelper";
2728
import { RelayerConfig } from "./RelayerConfig";
@@ -234,6 +235,25 @@ export class Relayer {
234235
return ignoreDeposit();
235236
}
236237

238+
if (!depositHasPoolRebalanceRouteMapping(deposit, this.clients.hubPoolClient)) {
239+
this.logger.debug({
240+
at: "Relayer::filterDeposit",
241+
message: `Skipping ${srcChain} deposit for input token ${inputToken} due to missing pool rebalance route.`,
242+
deposit,
243+
transactionHash: deposit.transactionHash,
244+
});
245+
return ignoreDeposit();
246+
}
247+
248+
if (sdkUtils.invalidOutputToken(deposit)) {
249+
this.logger.debug({
250+
at: "Relayer::filterDeposit",
251+
message: `Skipping ${srcChain} deposit for invalid output token ${deposit.outputToken}.`,
252+
transactionHash: deposit.transactionHash,
253+
});
254+
return ignoreDeposit();
255+
}
256+
237257
if (!this.routeEnabled(originChainId, destinationChainId)) {
238258
this.logger.debug({
239259
at: "Relayer::filterDeposit",

src/utils/FillUtils.ts

+7
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@ export function getUnfilledDeposits(
4242
});
4343
}
4444

45+
export function depositHasPoolRebalanceRouteMapping(
46+
deposit: Pick<DepositWithBlock, "inputToken" | "originChainId">,
47+
hubPoolClient: HubPoolClient
48+
): boolean {
49+
return hubPoolClient.l2TokenHasPoolRebalanceRoute(deposit.inputToken, deposit.originChainId);
50+
}
51+
4552
export function getAllUnfilledDeposits(
4653
spokePoolClients: SpokePoolClientsByChain,
4754
hubPoolClient: HubPoolClient

test/Dataworker.loadData.deposit.ts

+20
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,26 @@ describe("Dataworker: Load bundle data", async function () {
250250
expect(data1.expiredDepositsToRefundV3).to.deep.equal({});
251251
});
252252

253+
it("Can refund expired deposits with unresolvable output token", async function () {
254+
const bundleBlockTimestamps = await dataworkerInstance.clients.bundleDataClient.getBundleBlockTimestamps(
255+
[originChainId, destinationChainId],
256+
getDefaultBlockRange(5),
257+
spokePoolClients
258+
);
259+
260+
generateV3Deposit({
261+
outputToken: ZERO_ADDRESS,
262+
fillDeadline: bundleBlockTimestamps[destinationChainId][1] - 1,
263+
});
264+
265+
await mockOriginSpokePoolClient.update(["V3FundsDeposited"]);
266+
const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(
267+
getDefaultBlockRange(5),
268+
spokePoolClients
269+
);
270+
expect(data1.expiredDepositsToRefundV3[originChainId][erc20_1.address].length).to.equal(1);
271+
});
272+
253273
it("Does not consider expired zero value deposits from prior bundle", async function () {
254274
const bundleBlockTimestamps = await dataworkerInstance.clients.bundleDataClient.getBundleBlockTimestamps(
255275
[originChainId, destinationChainId],

test/Dataworker.loadData.fill.ts

+24-13
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,25 @@ describe("Dataworker: Load bundle data", async function () {
263263
expect(spy.getCalls().filter((e) => e.lastArg.message.includes("invalid fills")).length).to.equal(0);
264264
});
265265

266+
it("Does not refund fills for zero address output token deposits", async function () {
267+
generateV3Deposit({
268+
outputToken: ZERO_ADDRESS,
269+
});
270+
271+
await mockOriginSpokePoolClient.update(["V3FundsDeposited"]);
272+
const deposits = mockOriginSpokePoolClient.getDeposits();
273+
generateV3FillFromDeposit(deposits[0]);
274+
275+
await mockDestinationSpokePoolClient.update(["FilledV3Relay"]);
276+
const data1 = await dataworkerInstance.clients.bundleDataClient.loadData(
277+
getDefaultBlockRange(5),
278+
spokePoolClients
279+
);
280+
281+
expect(data1.bundleFillsV3).to.deep.equal({});
282+
expect(spy.getCalls().filter((e) => e.lastArg.message.includes("invalid fills")).length).to.equal(0);
283+
});
284+
266285
describe("Duplicate deposits in same bundle as fill", function () {
267286
it("Sends duplicate deposit refunds for fills in bundle", async function () {
268287
// Send duplicate deposits.
@@ -904,13 +923,9 @@ describe("Dataworker: Load bundle data", async function () {
904923
const bundleDepositsV3 = {};
905924
const bundleFillsV3 = {};
906925
deposits.forEach((deposit) => {
907-
const legacyDeposit = {
908-
...deposit,
909-
messageHash: "", // Superstruct defaults to "" for undefined.
910-
};
911926
bundleDepositsV3[deposit.originChainId] ??= {};
912927
bundleDepositsV3[deposit.originChainId][deposit.inputToken] ??= [];
913-
bundleDepositsV3[deposit.originChainId][deposit.inputToken].push(legacyDeposit);
928+
bundleDepositsV3[deposit.originChainId][deposit.inputToken].push(deposit);
914929
});
915930
fills.forEach((fill) => {
916931
bundleFillsV3[fill.originChainId] ??= {};
@@ -964,14 +979,10 @@ describe("Dataworker: Load bundle data", async function () {
964979
bundleDepositsV3[deposit.originChainId][deposit.inputToken].push(deposit);
965980
});
966981
fills.forEach((fill) => {
967-
const legacyFill = {
968-
...fill,
969-
messageHash: "", // Superstruct defaults to "" for undefined.
970-
};
971982
bundleFillsV3[fill.originChainId] ??= {};
972983
bundleFillsV3[fill.originChainId][fill.inputToken] ??= {};
973984
bundleFillsV3[fill.originChainId][fill.inputToken]["fills"] ??= [];
974-
bundleFillsV3[fill.originChainId][fill.inputToken].fills.push(legacyFill);
985+
bundleFillsV3[fill.originChainId][fill.inputToken].fills.push(fill);
975986
});
976987
const mockArweaveData = [
977988
{
@@ -1001,9 +1012,9 @@ describe("Dataworker: Load bundle data", async function () {
10011012
const fillV3Events: interfaces.Log[] = [];
10021013
const destinationChainId = mockDestinationSpokePoolClient.chainId;
10031014
// Create three valid deposits
1004-
depositV3Events.push(generateV3Deposit({ outputToken: randomAddress() }));
1005-
depositV3Events.push(generateV3Deposit({ outputToken: randomAddress() }));
1006-
depositV3Events.push(generateV3Deposit({ outputToken: randomAddress() }));
1015+
depositV3Events.push(generateV3Deposit());
1016+
depositV3Events.push(generateV3Deposit());
1017+
depositV3Events.push(generateV3Deposit());
10071018
await mockOriginSpokePoolClient.update(["V3FundsDeposited"]);
10081019
const deposits = mockOriginSpokePoolClient.getDeposits();
10091020

test/Dataworker.loadData.prefill.ts

+7-7
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ describe("Dataworker: Load bundle data: Pre-fill and Pre-Slow-Fill request logic
231231

232232
describe("Pre-fill has invalid repayment information", function () {
233233
it("Refunds fill to msg.sender if fill is not in-memory and repayment info is invalid", async function () {
234-
generateV3Deposit({ outputToken: randomAddress() });
234+
generateV3Deposit();
235235
await mockOriginSpokePoolClient.update(["V3FundsDeposited"]);
236236
const deposits = mockOriginSpokePoolClient.getDeposits();
237237

@@ -287,7 +287,7 @@ describe("Dataworker: Load bundle data: Pre-fill and Pre-Slow-Fill request logic
287287
});
288288

289289
it("Refunds fill to msg.sender if fill is in-memory and repayment info is invalid", async function () {
290-
generateV3Deposit({ outputToken: randomAddress() });
290+
generateV3Deposit();
291291
await mockOriginSpokePoolClient.update(["V3FundsDeposited"]);
292292
const deposits = mockOriginSpokePoolClient.getDeposits();
293293

@@ -329,7 +329,7 @@ describe("Dataworker: Load bundle data: Pre-fill and Pre-Slow-Fill request logic
329329
});
330330

331331
it("Does not refund fill to msg.sender if fill is not in-memory and repayment address and msg.sender are invalid for repayment chain", async function () {
332-
generateV3Deposit({ outputToken: randomAddress() });
332+
generateV3Deposit();
333333
await mockOriginSpokePoolClient.update(["V3FundsDeposited"]);
334334
const deposits = mockOriginSpokePoolClient.getDeposits();
335335

@@ -373,7 +373,7 @@ describe("Dataworker: Load bundle data: Pre-fill and Pre-Slow-Fill request logic
373373
});
374374

375375
it("Does not refund fill to msg.sender if fill is in-memory and repayment address and msg.sender are invalid for repayment chain", async function () {
376-
generateV3Deposit({ outputToken: randomAddress() });
376+
generateV3Deposit();
377377
await mockOriginSpokePoolClient.update(["V3FundsDeposited"]);
378378
const deposits = mockOriginSpokePoolClient.getDeposits();
379379

@@ -404,7 +404,7 @@ describe("Dataworker: Load bundle data: Pre-fill and Pre-Slow-Fill request logic
404404
});
405405

406406
it("Does not refund lite chain fill to msg.sender if fill is not in-memory and repayment address and msg.sender are invalid for origin chain", async function () {
407-
generateV3Deposit({ outputToken: randomAddress(), fromLiteChain: true });
407+
generateV3Deposit({ fromLiteChain: true });
408408
await mockOriginSpokePoolClient.update(["V3FundsDeposited"]);
409409
const deposits = mockOriginSpokePoolClient.getDeposits();
410410

@@ -447,7 +447,7 @@ describe("Dataworker: Load bundle data: Pre-fill and Pre-Slow-Fill request logic
447447
});
448448

449449
it("Does not refund lite chain fill to msg.sender if fill is in-memory and repayment address and msg.sender are invalid for origin chain", async function () {
450-
generateV3Deposit({ outputToken: randomAddress(), fromLiteChain: true });
450+
generateV3Deposit({ fromLiteChain: true });
451451
await mockOriginSpokePoolClient.update(["V3FundsDeposited"]);
452452
const deposits = mockOriginSpokePoolClient.getDeposits();
453453

@@ -478,7 +478,7 @@ describe("Dataworker: Load bundle data: Pre-fill and Pre-Slow-Fill request logic
478478
});
479479

480480
it("Refunds deposit as a duplicate if fill is not in-memory and is a slow fill", async function () {
481-
generateV3Deposit({ outputToken: randomAddress() });
481+
generateV3Deposit();
482482
await mockOriginSpokePoolClient.update(["V3FundsDeposited"]);
483483
const deposits = mockOriginSpokePoolClient.getDeposits();
484484

test/InventoryClient.RefundChain.ts

+3-99
Original file line numberDiff line numberDiff line change
@@ -367,11 +367,6 @@ describe("InventoryClient: Refund chain selection", async function () {
367367
hubPoolClient.setEnableAllL2Tokens(true);
368368
expect(await inventoryClient.determineRefundChainId(sampleDepositData)).to.deep.equal([MAINNET]);
369369
expect(lastSpyLogIncludes(spy, 'expectedPostRelayAllocation":"166666666666666666"')).to.be.true;
370-
371-
// If we set this to false in this test, the destination chain will be default used since the refund data
372-
// will be ignored.
373-
hubPoolClient.setEnableAllL2Tokens(false);
374-
expect(await inventoryClient.determineRefundChainId(sampleDepositData)).to.deep.equal([OPTIMISM, MAINNET]);
375370
});
376371

377372
it("Normalizes upcoming refunds to correct precision", async function () {
@@ -394,19 +389,16 @@ describe("InventoryClient: Refund chain selection", async function () {
394389
hubPoolClient.setEnableAllL2Tokens(true);
395390
expect(await inventoryClient.determineRefundChainId(sampleDepositData)).to.deep.equal([MAINNET]);
396391
expect(lastSpyLogIncludes(spy, 'expectedPostRelayAllocation":"166666666666666666"')).to.be.true;
397-
398-
// If we set this to false in this test, the destination chain will be default used since the refund data
399-
// will be ignored.
400-
hubPoolClient.setEnableAllL2Tokens(false);
401-
expect(await inventoryClient.determineRefundChainId(sampleDepositData)).to.deep.equal([OPTIMISM, MAINNET]);
402392
});
403393

404394
it("Correctly throws when Deposit tokens are not equivalent", async function () {
405395
sampleDepositData.inputAmount = toWei(5);
406396
sampleDepositData.outputAmount = await computeOutputAmount(sampleDepositData);
407397
expect(await inventoryClient.determineRefundChainId(sampleDepositData)).to.deep.equal([1]);
408398

409-
sampleDepositData.outputToken = ZERO_ADDRESS;
399+
// In this test, output token has a valid pool rebalance mapping but its not the equivalent token as the
400+
// input token
401+
sampleDepositData.outputToken = l2TokensForUsdc[OPTIMISM];
410402
const srcChain = getNetworkName(sampleDepositData.originChainId);
411403
const dstChain = getNetworkName(sampleDepositData.destinationChainId);
412404
await assertPromiseError(
@@ -711,92 +703,4 @@ describe("InventoryClient: Refund chain selection", async function () {
711703
expect(possibleRepaymentChains.length).to.equal(4);
712704
});
713705
});
714-
715-
describe("In-protocol swap", async function () {
716-
const nativeUSDC = TOKEN_SYMBOLS_MAP.USDC.addresses;
717-
const bridgedUSDC = { ...TOKEN_SYMBOLS_MAP["USDC.e"].addresses, ...TOKEN_SYMBOLS_MAP["USDbC"].addresses };
718-
719-
beforeEach(async function () {
720-
hubPoolClient.mapTokenInfo(nativeUSDC[ARBITRUM], "USDC", 6);
721-
722-
// Sub in a nested USDC config for the existing USDC config.
723-
const usdcConfig = {
724-
[nativeUSDC[OPTIMISM]]: {
725-
[OPTIMISM]: { targetPct: toWei(0.12), thresholdPct: toWei(0.1), targetOverageBuffer },
726-
},
727-
[nativeUSDC[POLYGON]]: {
728-
[POLYGON]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer },
729-
},
730-
[nativeUSDC[ARBITRUM]]: {
731-
[ARBITRUM]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer },
732-
},
733-
[bridgedUSDC[OPTIMISM]]: {
734-
[OPTIMISM]: { targetPct: toWei(0.12), thresholdPct: toWei(0.1), targetOverageBuffer },
735-
},
736-
[bridgedUSDC[POLYGON]]: {
737-
[POLYGON]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer },
738-
},
739-
[bridgedUSDC[ARBITRUM]]: {
740-
[ARBITRUM]: { targetPct: toWei(0.07), thresholdPct: toWei(0.05), targetOverageBuffer },
741-
},
742-
};
743-
inventoryConfig.tokenConfig[mainnetUsdc] = usdcConfig;
744-
745-
const inputAmount = toMegaWei(100);
746-
sampleDepositData = {
747-
depositId: bnZero,
748-
fromLiteChain: false,
749-
toLiteChain: false,
750-
originChainId: ARBITRUM,
751-
destinationChainId: OPTIMISM,
752-
depositor: owner.address,
753-
recipient: owner.address,
754-
inputToken: nativeUSDC[ARBITRUM],
755-
inputAmount,
756-
outputToken: bridgedUSDC[OPTIMISM],
757-
outputAmount: inputAmount,
758-
message: "0x",
759-
messageHash: "0x",
760-
quoteTimestamp: hubPoolClient.currentTime!,
761-
fillDeadline: 0,
762-
exclusivityDeadline: 0,
763-
exclusiveRelayer: ZERO_ADDRESS,
764-
};
765-
});
766-
767-
it("outputToken is not supported as a repaymentToken", async function () {
768-
// Verify that there is no native USDC anywhere. The relayer is responsible for ensuring that it can make the fill.
769-
enabledChainIds
770-
.filter((chainId) => chainId !== MAINNET)
771-
.forEach((chainId) => expect(tokenClient.getBalance(chainId, nativeUSDC[chainId]).eq(bnZero)).to.be.true);
772-
773-
// All chains are at target balance; cumulative balance is unchanged but repaymentToken balances on origin chain increases.
774-
expect(await inventoryClient.determineRefundChainId(sampleDepositData, mainnetUsdc)).to.deep.equal([MAINNET]);
775-
expect(lastSpyLogIncludes(spy, 'expectedPostRelayAllocation":"78571428571428571"')).to.be.true; // (1,000+100)/(14,000)
776-
777-
// Even when the output amount is equal to the destination's entire balance, take repayment on mainnet.
778-
// This should be th same calculation as above.
779-
sampleDepositData.outputAmount = inventoryClient.getBalanceOnChain(OPTIMISM, mainnetUsdc);
780-
expect(await inventoryClient.determineRefundChainId(sampleDepositData, mainnetUsdc)).to.deep.equal([MAINNET]);
781-
expect(lastSpyLogIncludes(spy, 'expectedPostRelayAllocation":"78571428571428571"')).to.be.true; // (1,000+100)/(14,000)
782-
783-
// Drop the relayer's repaymentToken balance on Optimism. Repayment chain should now be Optimism.
784-
let balance = tokenClient.getBalance(OPTIMISM, bridgedUSDC[OPTIMISM]);
785-
tokenClient.setTokenData(OPTIMISM, bridgedUSDC[OPTIMISM], bnZero);
786-
expect(await inventoryClient.determineRefundChainId(sampleDepositData, mainnetUsdc)).to.deep.equal([
787-
OPTIMISM,
788-
MAINNET,
789-
]);
790-
791-
// Restore the Optimism balance and drop the Arbitrum balance. Repayment chain should now be Arbitrum.
792-
tokenClient.setTokenData(OPTIMISM, bridgedUSDC[OPTIMISM], balance);
793-
794-
balance = tokenClient.getBalance(ARBITRUM, bridgedUSDC[ARBITRUM]);
795-
tokenClient.setTokenData(ARBITRUM, bridgedUSDC[ARBITRUM], bnZero);
796-
expect(await inventoryClient.determineRefundChainId(sampleDepositData, mainnetUsdc)).to.deep.equal([
797-
ARBITRUM,
798-
MAINNET,
799-
]);
800-
});
801-
});
802706
});

0 commit comments

Comments
 (0)