Skip to content

Commit 14d4941

Browse files
authored
feat(HubPoolClient): Handle disabled pool rebalance routes (#1198)
1 parent 79cce74 commit 14d4941

File tree

9 files changed

+229
-221
lines changed

9 files changed

+229
-221
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@across-protocol/sdk",
33
"author": "UMA Team",
4-
"version": "4.3.58",
4+
"version": "4.3.59",
55
"license": "AGPL-3.0",
66
"homepage": "https://docs.across.to/reference/sdk",
77
"files": [
@@ -34,7 +34,7 @@
3434
"bump-version:minor": "yarn version --minor --no-git-tag-version --no-commit-hooks && git commit -m 'chore: bump version' ./package.json --no-verify",
3535
"bump-version:patch": "yarn version --patch --no-git-tag-version --no-commit-hooks && git commit -m 'chore: bump version' ./package.json --no-verify",
3636
"typechain": "typechain --target ethers-v5 --out-dir src/utils/abi/typechain 'src/utils/abi/contracts/*.json' && eslint --fix src/utils/abi/typechain && yarn prettier --write \"src/utils/abi/typechain/**/*.ts\"",
37-
"yalc:watch": "nodemon --watch src --ext ts,tsx,json,js,jsx --exec 'yalc push'"
37+
"yalc:watch": "nodemon --watch src --ext ts,tsx,json,js,jsx --ignore src/utils/abi/ --exec 'yalc publish --push --changed'"
3838
},
3939
"lint-staged": {
4040
"*.ts": "yarn lint"

src/clients/BundleDataClient/utils/DataworkerUtils.ts

Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -399,21 +399,15 @@ export function _getMarginalRunningBalances(
399399
([l2TokenAddress, { realizedLpFees: totalRealizedLpFee, totalRefundAmount }]) => {
400400
// If the repayment token and repayment chain ID do not map to a PoolRebalanceRoute graph, then
401401
// there are no relevant L1 running balances.
402-
if (
403-
!clients.hubPoolClient.l2TokenHasPoolRebalanceRoute(
404-
toAddressType(l2TokenAddress, repaymentChainId),
405-
repaymentChainId,
406-
mainnetBundleEndBlock
407-
)
408-
) {
409-
chainWithRefundsOnly.add(repaymentChainId);
410-
return;
411-
}
412402
const l1Token = clients.hubPoolClient.getL1TokenForL2TokenAtBlock(
413403
toAddressType(l2TokenAddress, repaymentChainId),
414404
repaymentChainId,
415405
mainnetBundleEndBlock
416406
);
407+
if (!l1Token) {
408+
chainWithRefundsOnly.add(repaymentChainId);
409+
return;
410+
}
417411
const l1TokenAddr = l1Token.toNative();
418412
assert(l1Token.isEVM(), `Expected an EVM address: ${l1TokenAddr}`);
419413

@@ -439,6 +433,9 @@ export function _getMarginalRunningBalances(
439433
destinationChainId,
440434
mainnetBundleEndBlock
441435
);
436+
437+
assert(isDefined(l1TokenCounterpart), "getRefundInformationFromFill: l1TokenCounterpart is undefined");
438+
442439
const lpFee = deposit.lpFeePct.mul(deposit.inputAmount).div(fixedPointAdjustment);
443440
updateRunningBalance(
444441
runningBalances,
@@ -468,6 +465,8 @@ export function _getMarginalRunningBalances(
468465
destinationChainId,
469466
mainnetBundleEndBlock
470467
);
468+
assert(isDefined(l1TokenCounterpart), "getRefundInformationFromFill: l1TokenCounterpart is undefined");
469+
471470
const lpFee = deposit.lpFeePct.mul(deposit.inputAmount).div(fixedPointAdjustment);
472471
updateRunningBalance(
473472
runningBalances,
@@ -523,21 +522,15 @@ export function _getMarginalRunningBalances(
523522
deposits.forEach((deposit) => {
524523
// If the repayment token and repayment chain ID do not map to a PoolRebalanceRoute graph, then
525524
// there are no relevant L1 running balances.
526-
if (
527-
!clients.hubPoolClient.l2TokenHasPoolRebalanceRoute(
528-
deposit.inputToken,
529-
deposit.originChainId,
530-
mainnetBundleEndBlock
531-
)
532-
) {
533-
chainWithRefundsOnly.add(deposit.originChainId);
534-
return;
535-
}
536525
const l1TokenCounterpart = clients.hubPoolClient.getL1TokenForL2TokenAtBlock(
537526
toAddressType(inputToken, originChainId),
538527
originChainId,
539528
mainnetBundleEndBlock
540529
);
530+
if (!l1TokenCounterpart) {
531+
chainWithRefundsOnly.add(deposit.originChainId);
532+
return;
533+
}
541534
updateRunningBalance(runningBalances, originChainId, l1TokenCounterpart.toEvmAddress(), deposit.inputAmount);
542535
});
543536
});

src/clients/BundleDataClient/utils/FillUtils.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,18 +37,21 @@ export function getRefundInformationFromFill(
3737

3838
// Now figure out the equivalent L2 token for the repayment token. If the inputToken doesn't have a
3939
// PoolRebalanceRoute, then the repayment chain would have been the originChainId after the getRepaymentChainId()
40-
// call and we would have returned already, so the following call should always succeed.
40+
// call and we would have returned already, so the following call should always return a valid L1 token.
4141
const l1TokenCounterpart = hubPoolClient.getL1TokenForL2TokenAtBlock(
4242
relayData.inputToken,
4343
relayData.originChainId,
4444
bundleEndBlockForMainnet
4545
);
4646

47+
assert(isDefined(l1TokenCounterpart), "getRefundInformationFromFill: l1TokenCounterpart is undefined");
48+
4749
const repaymentToken = hubPoolClient.getL2TokenForL1TokenAtBlock(
4850
l1TokenCounterpart,
4951
chainToSendRefundTo,
5052
bundleEndBlockForMainnet
5153
);
54+
assert(isDefined(repaymentToken), "getRefundInformationFromFill: repaymentToken is undefined");
5255

5356
return {
5457
chainToSendRefundTo,
@@ -183,6 +186,9 @@ function _repaymentChainTokenIsValid(
183186
relayData.originChainId,
184187
bundleEndBlockForMainnet
185188
);
189+
if (!l1TokenCounterpart) {
190+
return false;
191+
}
186192
if (
187193
!hubPoolClient.l2TokenEnabledForL1TokenAtBlock(
188194
l1TokenCounterpart,

src/clients/BundleDataClient/utils/PoolRebalanceUtils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,8 @@ export function updateRunningBalanceForDeposit(
206206
deposit.originChainId,
207207
mainnetBundleEndBlock
208208
);
209+
assert(isDefined(l1TokenCounterpart), "updateRunningBalanceForDeposit: l1TokenCounterpart is undefined");
210+
209211
updateRunningBalance(runningBalances, deposit.originChainId, l1TokenCounterpart.toEvmAddress(), updateAmount);
210212
}
211213

src/clients/HubPoolClient.ts

Lines changed: 49 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ import {
3434
fetchTokenInfo,
3535
getCachedBlockForTimestamp,
3636
getCurrentTime,
37-
getNetworkName,
3837
isDefined,
3938
mapAsync,
4039
paginatedEventQuery,
@@ -75,17 +74,11 @@ type HubPoolEvent =
7574
| "RootBundleExecuted"
7675
| "CrossChainContractsSet";
7776

78-
type L1TokensToDestinationTokens = {
79-
[l1Token: string]: { [destinationChainId: number]: Address };
80-
};
81-
8277
export type LpFeeRequest = Pick<Deposit, "originChainId" | "inputToken" | "inputAmount" | "quoteTimestamp"> & {
8378
paymentChainId?: number;
8479
};
8580

8681
export class HubPoolClient extends BaseAbstractClient {
87-
// L1Token -> destinationChainId -> destinationToken
88-
protected l1TokensToDestinationTokens: L1TokensToDestinationTokens = {};
8982
protected l1Tokens: L1TokenInfo[] = []; // L1Tokens and their associated info.
9083
// @dev `token` here is a 20-byte hex sting
9184
protected lpTokens: { [token: string]: LpToken } = {};
@@ -192,87 +185,65 @@ export class HubPoolClient extends BaseAbstractClient {
192185
l1Token: EvmAddress,
193186
destinationChainId: number,
194187
latestHubBlock = Number.MAX_SAFE_INTEGER
195-
): Address {
188+
): Address | undefined {
196189
if (!this.l1TokensToDestinationTokensWithBlock?.[l1Token.toNative()]?.[destinationChainId]) {
197-
const chain = getNetworkName(destinationChainId);
198-
const { symbol } = this.l1Tokens.find(({ address }) => address.eq(l1Token)) ?? { symbol: l1Token.toString() };
199-
throw new Error(`Could not find SpokePool mapping for ${symbol} on ${chain} and L1 token ${l1Token}`);
190+
return undefined;
200191
}
201192
// Find the last mapping published before the target block.
202-
const l2Token: DestinationTokenWithBlock | undefined = sortEventsDescending(
203-
this.l1TokensToDestinationTokensWithBlock[l1Token.toNative()][destinationChainId]
204-
).find((mapping: DestinationTokenWithBlock) => mapping.blockNumber <= latestHubBlock);
205-
if (!l2Token) {
206-
const chain = getNetworkName(destinationChainId);
207-
const { symbol } = this.l1Tokens.find(({ address }) => address.eq(l1Token)) ?? { symbol: l1Token.toString() };
208-
throw new Error(
209-
`Could not find SpokePool mapping for ${symbol} on ${chain} at or before HubPool block ${latestHubBlock}!`
210-
);
211-
}
212-
return l2Token.l2Token;
193+
const l2Token: DestinationTokenWithBlock | undefined = this.l1TokensToDestinationTokensWithBlock[
194+
l1Token.toNative()
195+
][destinationChainId].find((mapping: DestinationTokenWithBlock) => mapping.blockNumber <= latestHubBlock);
196+
197+
return !isDefined(l2Token) || l2Token.l2Token.isZeroAddress() ? undefined : l2Token.l2Token;
213198
}
214199

215200
// Returns the latest L1 token to use for an L2 token as of the input hub block.
216201
getL1TokenForL2TokenAtBlock(
217202
l2Token: Address,
218203
destinationChainId: number,
219204
latestHubBlock = Number.MAX_SAFE_INTEGER
220-
): EvmAddress {
221-
const l2Tokens = Object.keys(this.l1TokensToDestinationTokensWithBlock)
222-
.filter((l1Token) => this.l2TokenEnabledForL1Token(EvmAddress.from(l1Token), destinationChainId))
223-
.map((l1Token) => {
224-
// Return all matching L2 token mappings that are equal to or earlier than the target block.
225-
// @dev Since tokens on L2s (like Solana) can have 32 byte addresses, filter on the lower 20 bytes of the token only.
226-
return this.l1TokensToDestinationTokensWithBlock[l1Token][destinationChainId].filter(
227-
(dstTokenWithBlock) =>
228-
dstTokenWithBlock.l2Token.truncateToBytes20() === l2Token.truncateToBytes20() &&
229-
dstTokenWithBlock.blockNumber <= latestHubBlock
230-
);
231-
})
232-
.flat();
233-
if (l2Tokens.length === 0) {
234-
const chain = getNetworkName(destinationChainId);
235-
throw new Error(
236-
`Could not find HubPool mapping for ${l2Token} on ${chain} at or before HubPool block ${latestHubBlock}!`
205+
): EvmAddress | undefined {
206+
const l2Tokens = Object.keys(this.l1TokensToDestinationTokensWithBlock).flatMap((l1Token) => {
207+
// Get the latest L2 token mapping for the given L1 token.
208+
// @dev Since tokens on L2s (like Solana) can have 32 byte addresses, filter on the lower 20 bytes of the token only.
209+
const sortedL2Tokens = sortEventsDescending(
210+
(this.l1TokensToDestinationTokensWithBlock[l1Token][destinationChainId] ?? []).filter(
211+
(dstTokenWithBlock) => dstTokenWithBlock.blockNumber <= latestHubBlock
212+
)
237213
);
238-
}
239-
// Find the last mapping published before the target block.
240-
return sortEventsDescending(l2Tokens)[0].l1Token;
214+
// If the latest L2 token mapping is equal to the target L2 token, return it.
215+
return sortedL2Tokens.length > 0 && sortedL2Tokens[0].l2Token.truncateToBytes20() === l2Token.truncateToBytes20()
216+
? sortedL2Tokens[0]
217+
: [];
218+
});
219+
220+
return l2Tokens.length === 0 ? undefined : sortEventsDescending(l2Tokens)[0].l1Token;
241221
}
242222

243223
protected getL1TokenForDeposit(
244224
deposit: Pick<DepositWithBlock, "originChainId" | "inputToken" | "quoteBlockNumber">
245-
): EvmAddress {
225+
): EvmAddress | undefined {
246226
// L1-->L2 token mappings are set via PoolRebalanceRoutes which occur on mainnet,
247227
// so we use the latest token mapping. This way if a very old deposit is filled, the relayer can use the
248228
// latest L2 token mapping to find the L1 token counterpart.
249229
return this.getL1TokenForL2TokenAtBlock(deposit.inputToken, deposit.originChainId, deposit.quoteBlockNumber);
250230
}
251231

252232
l2TokenEnabledForL1Token(l1Token: EvmAddress, destinationChainId: number): boolean {
253-
return this.l1TokensToDestinationTokens?.[l1Token.toNative()]?.[destinationChainId] != undefined;
233+
return this.l2TokenEnabledForL1TokenAtBlock(l1Token, destinationChainId, Number.MAX_SAFE_INTEGER);
254234
}
255235

256236
l2TokenEnabledForL1TokenAtBlock(l1Token: EvmAddress, destinationChainId: number, hubBlockNumber: number): boolean {
257237
// Find the last mapping published before the target block.
258238
const l2Token: DestinationTokenWithBlock | undefined = sortEventsDescending(
259239
this.l1TokensToDestinationTokensWithBlock?.[l1Token.toNative()]?.[destinationChainId] ?? []
260240
).find((mapping: DestinationTokenWithBlock) => mapping.blockNumber <= hubBlockNumber);
261-
return l2Token !== undefined;
241+
return isDefined(l2Token) && !l2Token.l2Token.isZeroAddress();
262242
}
263243

264244
l2TokenHasPoolRebalanceRoute(l2Token: Address, l2ChainId: number, hubPoolBlock = this.latestHeightSearched): boolean {
265-
return Object.values(this.l1TokensToDestinationTokensWithBlock).some((destinationTokenMapping) => {
266-
return Object.entries(destinationTokenMapping).some(([_l2ChainId, setPoolRebalanceRouteEvents]) => {
267-
return setPoolRebalanceRouteEvents.some((e) => {
268-
return (
269-
e.blockNumber <= hubPoolBlock &&
270-
e.l2Token.truncateToBytes20() === l2Token.truncateToBytes20() &&
271-
Number(_l2ChainId) === l2ChainId
272-
);
273-
});
274-
});
275-
});
245+
const l1Token = this.getL1TokenForL2TokenAtBlock(l2Token, l2ChainId, hubPoolBlock);
246+
return isDefined(l1Token);
276247
}
277248

278249
/**
@@ -401,11 +372,11 @@ export class HubPoolClient extends BaseAbstractClient {
401372
const hubPoolTokens: { [k: string]: EvmAddress } = {};
402373
const getHubPoolToken = (deposit: LpFeeRequest, quoteBlockNumber: number): EvmAddress | undefined => {
403374
const tokenKey = `${deposit.originChainId}-${deposit.inputToken}`;
404-
if (this.l2TokenHasPoolRebalanceRoute(deposit.inputToken, deposit.originChainId, quoteBlockNumber)) {
405-
return (hubPoolTokens[tokenKey] ??= this.getL1TokenForDeposit({ ...deposit, quoteBlockNumber }));
375+
const l1Token = this.getL1TokenForDeposit({ ...deposit, quoteBlockNumber });
376+
if (!isDefined(l1Token)) {
377+
return undefined;
406378
}
407-
408-
return undefined;
379+
return (hubPoolTokens[tokenKey] ??= l1Token);
409380
};
410381

411382
// Filter hubPoolTokens for duplicates by reverting to their native string
@@ -553,14 +524,14 @@ export class HubPoolClient extends BaseAbstractClient {
553524
// Resolve both SpokePool tokens back to their respective HubPool tokens and verify that they match.
554525
const l1TokenA = this.getL1TokenForL2TokenAtBlock(tokenA, chainIdA, hubPoolBlock);
555526
const l1TokenB = this.getL1TokenForL2TokenAtBlock(tokenB, chainIdB, hubPoolBlock);
556-
if (!l1TokenA.eq(l1TokenB)) {
527+
if (!isDefined(l1TokenA) || !isDefined(l1TokenB) || !l1TokenA.eq(l1TokenB)) {
557528
return false;
558529
}
559530

560531
// Resolve both HubPool tokens back to a current SpokePool token and verify that they match.
561532
const _tokenA = this.getL2TokenForL1TokenAtBlock(l1TokenA, chainIdA, hubPoolBlock);
562533
const _tokenB = this.getL2TokenForL1TokenAtBlock(l1TokenB, chainIdB, hubPoolBlock);
563-
return tokenA.eq(_tokenA) && tokenB.eq(_tokenB);
534+
return isDefined(_tokenA) && isDefined(_tokenB) && tokenA.eq(_tokenA) && tokenB.eq(_tokenB);
564535
}
565536

566537
getSpokeActivationBlockForChain(chainId: number): number {
@@ -1001,24 +972,22 @@ export class HubPoolClient extends BaseAbstractClient {
1001972
destinationToken = svmUsdc;
1002973
}
1003974

1004-
// If the destination token is set to the zero address in an event, then this means Across should no longer
1005-
// rebalance to this chain.
1006-
if (!destinationToken.isZeroAddress()) {
1007-
assign(this.l1TokensToDestinationTokens, [args.l1Token, args.destinationChainId], destinationToken);
1008-
assign(
1009-
this.l1TokensToDestinationTokensWithBlock,
1010-
[args.l1Token, args.destinationChainId],
1011-
[
1012-
{
1013-
l1Token: EvmAddress.from(args.l1Token),
1014-
l2Token: destinationToken,
1015-
blockNumber: args.blockNumber,
1016-
txnIndex: args.txnIndex,
1017-
logIndex: args.logIndex,
1018-
txnRef: args.txnRef,
1019-
},
1020-
]
1021-
);
975+
const newRoute: DestinationTokenWithBlock = {
976+
l1Token: EvmAddress.from(args.l1Token),
977+
l2Token: destinationToken,
978+
blockNumber: args.blockNumber,
979+
txnIndex: args.txnIndex,
980+
logIndex: args.logIndex,
981+
txnRef: args.txnRef,
982+
};
983+
if (this.l1TokensToDestinationTokensWithBlock[args.l1Token]?.[args.destinationChainId]) {
984+
// Events are most likely coming in descending orders already but just in case we sort them again.
985+
this.l1TokensToDestinationTokensWithBlock[args.l1Token][args.destinationChainId] = sortEventsDescending([
986+
...this.l1TokensToDestinationTokensWithBlock[args.l1Token][args.destinationChainId],
987+
newRoute,
988+
]);
989+
} else {
990+
assign(this.l1TokensToDestinationTokensWithBlock, [args.l1Token, args.destinationChainId], [newRoute]);
1022991
}
1023992
}
1024993
}

src/clients/SpokePoolClient/SpokePoolClient.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -474,18 +474,22 @@ export abstract class SpokePoolClient extends BaseAbstractClient {
474474
)
475475
) {
476476
return false;
477-
} else {
478-
const l1Token = this.hubPoolClient?.getL1TokenForL2TokenAtBlock(
479-
deposit.inputToken,
480-
deposit.originChainId,
481-
deposit.quoteBlockNumber
482-
);
483-
return this.hubPoolClient.l2TokenEnabledForL1TokenAtBlock(
484-
l1Token,
485-
deposit.destinationChainId,
486-
deposit.quoteBlockNumber
487-
);
488477
}
478+
479+
const l1Token = this.hubPoolClient?.getL1TokenForL2TokenAtBlock(
480+
deposit.inputToken,
481+
deposit.originChainId,
482+
deposit.quoteBlockNumber
483+
);
484+
if (!l1Token) {
485+
return false;
486+
}
487+
488+
return this.hubPoolClient.l2TokenEnabledForL1TokenAtBlock(
489+
l1Token,
490+
deposit.destinationChainId,
491+
deposit.quoteBlockNumber
492+
);
489493
}
490494

491495
/**

0 commit comments

Comments
 (0)