Skip to content

Commit 4af064d

Browse files
authored
fix(HubPoolClient): Handle disabled pool rebalance routes (#1182)
1 parent ff1419f commit 4af064d

File tree

9 files changed

+231
-220
lines changed

9 files changed

+231
-220
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.44",
4+
"version": "4.3.45",
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: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
count2DDictionaryValues,
1919
count3DDictionaryValues,
2020
toAddressType,
21+
isDefined,
2122
} from "../../../utils";
2223
import {
2324
addLastRunningBalance,
@@ -153,21 +154,15 @@ export function _buildPoolRebalanceRoot(
153154
([l2TokenAddress, { realizedLpFees: totalRealizedLpFee, totalRefundAmount }]) => {
154155
// If the repayment token and repayment chain ID do not map to a PoolRebalanceRoute graph, then
155156
// there are no relevant L1 running balances.
156-
if (
157-
!clients.hubPoolClient.l2TokenHasPoolRebalanceRoute(
158-
toAddressType(l2TokenAddress, repaymentChainId),
159-
repaymentChainId,
160-
mainnetBundleEndBlock
161-
)
162-
) {
163-
chainWithRefundsOnly.add(repaymentChainId);
164-
return;
165-
}
166157
const l1Token = clients.hubPoolClient.getL1TokenForL2TokenAtBlock(
167158
toAddressType(l2TokenAddress, repaymentChainId),
168159
repaymentChainId,
169160
mainnetBundleEndBlock
170161
);
162+
if (!l1Token) {
163+
chainWithRefundsOnly.add(repaymentChainId);
164+
return;
165+
}
171166
const l1TokenAddr = l1Token.toNative();
172167
assert(l1Token.isEVM(), `Expected an EVM address: ${l1TokenAddr}`);
173168

@@ -193,6 +188,9 @@ export function _buildPoolRebalanceRoot(
193188
destinationChainId,
194189
mainnetBundleEndBlock
195190
);
191+
192+
assert(isDefined(l1TokenCounterpart), "getRefundInformationFromFill: l1TokenCounterpart is undefined");
193+
196194
const lpFee = deposit.lpFeePct.mul(deposit.inputAmount).div(fixedPointAdjustment);
197195
updateRunningBalance(
198196
runningBalances,
@@ -222,6 +220,8 @@ export function _buildPoolRebalanceRoot(
222220
destinationChainId,
223221
mainnetBundleEndBlock
224222
);
223+
assert(isDefined(l1TokenCounterpart), "getRefundInformationFromFill: l1TokenCounterpart is undefined");
224+
225225
const lpFee = deposit.lpFeePct.mul(deposit.inputAmount).div(fixedPointAdjustment);
226226
updateRunningBalance(
227227
runningBalances,
@@ -277,21 +277,15 @@ export function _buildPoolRebalanceRoot(
277277
deposits.forEach((deposit) => {
278278
// If the repayment token and repayment chain ID do not map to a PoolRebalanceRoute graph, then
279279
// there are no relevant L1 running balances.
280-
if (
281-
!clients.hubPoolClient.l2TokenHasPoolRebalanceRoute(
282-
deposit.inputToken,
283-
deposit.originChainId,
284-
mainnetBundleEndBlock
285-
)
286-
) {
287-
chainWithRefundsOnly.add(deposit.originChainId);
288-
return;
289-
}
290280
const l1TokenCounterpart = clients.hubPoolClient.getL1TokenForL2TokenAtBlock(
291281
toAddressType(inputToken, originChainId),
292282
originChainId,
293283
mainnetBundleEndBlock
294284
);
285+
if (!l1TokenCounterpart) {
286+
chainWithRefundsOnly.add(deposit.originChainId);
287+
return;
288+
}
295289
updateRunningBalance(runningBalances, originChainId, l1TokenCounterpart.toEvmAddress(), deposit.inputAmount);
296290
});
297291
});

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
@@ -202,6 +202,8 @@ export function updateRunningBalanceForDeposit(
202202
deposit.originChainId,
203203
mainnetBundleEndBlock
204204
);
205+
assert(isDefined(l1TokenCounterpart), "updateRunningBalanceForDeposit: l1TokenCounterpart is undefined");
206+
205207
updateRunningBalance(runningBalances, deposit.originChainId, l1TokenCounterpart.toEvmAddress(), updateAmount);
206208
}
207209

src/clients/HubPoolClient.ts

Lines changed: 50 additions & 79 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.toEvmAddress()]?.[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.toEvmAddress()][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.toEvmAddress()
195+
][destinationChainId].find((mapping: DestinationTokenWithBlock) => mapping.blockNumber <= latestHubBlock);
196+
197+
return !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.toEvmAddress()]?.[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.toEvmAddress()]?.[destinationChainId] ?? []
260240
).find((mapping: DestinationTokenWithBlock) => mapping.blockNumber <= hubBlockNumber);
261-
return l2Token !== undefined;
241+
return l2Token !== undefined && !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 l1Token !== undefined;
276247
}
277248

278249
/**
@@ -407,9 +378,11 @@ export class HubPoolClient extends BaseAbstractClient {
407378
const hubPoolTokens: { [k: string]: EvmAddress } = {};
408379
const getHubPoolToken = (deposit: LpFeeRequest, quoteBlockNumber: number): EvmAddress | undefined => {
409380
const tokenKey = `${deposit.originChainId}-${deposit.inputToken}`;
410-
if (this.l2TokenHasPoolRebalanceRoute(deposit.inputToken, deposit.originChainId, quoteBlockNumber)) {
411-
return (hubPoolTokens[tokenKey] ??= this.getL1TokenForDeposit({ ...deposit, quoteBlockNumber }));
412-
} else return undefined;
381+
const l1Token = this.getL1TokenForDeposit({ ...deposit, quoteBlockNumber });
382+
if (!l1Token) {
383+
return undefined;
384+
}
385+
return (hubPoolTokens[tokenKey] ??= l1Token);
413386
};
414387
const getHubPoolTokens = (): EvmAddress[] => dedupArray(Object.values(hubPoolTokens).filter(isDefined));
415388

@@ -548,14 +521,14 @@ export class HubPoolClient extends BaseAbstractClient {
548521
// Resolve both SpokePool tokens back to their respective HubPool tokens and verify that they match.
549522
const l1TokenA = this.getL1TokenForL2TokenAtBlock(tokenA, chainIdA, hubPoolBlock);
550523
const l1TokenB = this.getL1TokenForL2TokenAtBlock(tokenB, chainIdB, hubPoolBlock);
551-
if (!l1TokenA.eq(l1TokenB)) {
524+
if (!l1TokenA || !l1TokenB || !l1TokenA.eq(l1TokenB)) {
552525
return false;
553526
}
554527

555528
// Resolve both HubPool tokens back to a current SpokePool token and verify that they match.
556529
const _tokenA = this.getL2TokenForL1TokenAtBlock(l1TokenA, chainIdA, hubPoolBlock);
557530
const _tokenB = this.getL2TokenForL1TokenAtBlock(l1TokenB, chainIdB, hubPoolBlock);
558-
return tokenA.eq(_tokenA) && tokenB.eq(_tokenB);
531+
return _tokenA !== undefined && _tokenB !== undefined && tokenA.eq(_tokenA) && tokenB.eq(_tokenB);
559532
}
560533

561534
getSpokeActivationBlockForChain(chainId: number): number {
@@ -995,24 +968,22 @@ export class HubPoolClient extends BaseAbstractClient {
995968
destinationToken = svmUsdc;
996969
}
997970

998-
// If the destination token is set to the zero address in an event, then this means Across should no longer
999-
// rebalance to this chain.
1000-
if (!destinationToken.isZeroAddress()) {
1001-
assign(this.l1TokensToDestinationTokens, [args.l1Token, args.destinationChainId], destinationToken);
1002-
assign(
1003-
this.l1TokensToDestinationTokensWithBlock,
1004-
[args.l1Token, args.destinationChainId],
1005-
[
1006-
{
1007-
l1Token: EvmAddress.from(args.l1Token),
1008-
l2Token: destinationToken,
1009-
blockNumber: args.blockNumber,
1010-
txnIndex: args.txnIndex,
1011-
logIndex: args.logIndex,
1012-
txnRef: args.txnRef,
1013-
},
1014-
]
1015-
);
971+
const newRoute: DestinationTokenWithBlock = {
972+
l1Token: EvmAddress.from(args.l1Token),
973+
l2Token: destinationToken,
974+
blockNumber: args.blockNumber,
975+
txnIndex: args.txnIndex,
976+
logIndex: args.logIndex,
977+
txnRef: args.txnRef,
978+
};
979+
if (this.l1TokensToDestinationTokensWithBlock[args.l1Token]?.[args.destinationChainId]) {
980+
// Events are most likely coming in descending orders already but just in case we sort them again.
981+
this.l1TokensToDestinationTokensWithBlock[args.l1Token][args.destinationChainId] = sortEventsDescending([
982+
...this.l1TokensToDestinationTokensWithBlock[args.l1Token][args.destinationChainId],
983+
newRoute,
984+
]);
985+
} else {
986+
assign(this.l1TokensToDestinationTokensWithBlock, [args.l1Token, args.destinationChainId], [newRoute]);
1016987
}
1017988
}
1018989
}

src/clients/SpokePoolClient/SpokePoolClient.ts

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

489493
/**

0 commit comments

Comments
 (0)