Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@across-protocol/sdk",
"author": "UMA Team",
"version": "4.3.44",
"version": "4.3.45",
"license": "AGPL-3.0",
"homepage": "https://docs.across.to/reference/sdk",
"files": [
Expand Down Expand Up @@ -34,7 +34,7 @@
"bump-version:minor": "yarn version --minor --no-git-tag-version --no-commit-hooks && git commit -m 'chore: bump version' ./package.json --no-verify",
"bump-version:patch": "yarn version --patch --no-git-tag-version --no-commit-hooks && git commit -m 'chore: bump version' ./package.json --no-verify",
"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\"",
"yalc:watch": "nodemon --watch src --ext ts,tsx,json,js,jsx --exec 'yalc push'"
"yalc:watch": "nodemon --watch src --ext ts,tsx,json,js,jsx --ignore src/utils/abi/ --exec 'yalc publish --push --changed'"
},
"lint-staged": {
"*.ts": "yarn lint"
Expand Down
34 changes: 14 additions & 20 deletions src/clients/BundleDataClient/utils/DataworkerUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
count2DDictionaryValues,
count3DDictionaryValues,
toAddressType,
isDefined,
} from "../../../utils";
import {
addLastRunningBalance,
Expand Down Expand Up @@ -153,21 +154,15 @@ export function _buildPoolRebalanceRoot(
([l2TokenAddress, { realizedLpFees: totalRealizedLpFee, totalRefundAmount }]) => {
// If the repayment token and repayment chain ID do not map to a PoolRebalanceRoute graph, then
// there are no relevant L1 running balances.
if (
!clients.hubPoolClient.l2TokenHasPoolRebalanceRoute(
toAddressType(l2TokenAddress, repaymentChainId),
repaymentChainId,
mainnetBundleEndBlock
)
) {
chainWithRefundsOnly.add(repaymentChainId);
return;
}
const l1Token = clients.hubPoolClient.getL1TokenForL2TokenAtBlock(
toAddressType(l2TokenAddress, repaymentChainId),
repaymentChainId,
mainnetBundleEndBlock
);
if (!l1Token) {
chainWithRefundsOnly.add(repaymentChainId);
return;
}
const l1TokenAddr = l1Token.toNative();
assert(l1Token.isEVM(), `Expected an EVM address: ${l1TokenAddr}`);

Expand All @@ -193,6 +188,9 @@ export function _buildPoolRebalanceRoot(
destinationChainId,
mainnetBundleEndBlock
);

assert(isDefined(l1TokenCounterpart), "getRefundInformationFromFill: l1TokenCounterpart is undefined");

const lpFee = deposit.lpFeePct.mul(deposit.inputAmount).div(fixedPointAdjustment);
updateRunningBalance(
runningBalances,
Expand Down Expand Up @@ -222,6 +220,8 @@ export function _buildPoolRebalanceRoot(
destinationChainId,
mainnetBundleEndBlock
);
assert(isDefined(l1TokenCounterpart), "getRefundInformationFromFill: l1TokenCounterpart is undefined");

const lpFee = deposit.lpFeePct.mul(deposit.inputAmount).div(fixedPointAdjustment);
updateRunningBalance(
runningBalances,
Expand Down Expand Up @@ -277,21 +277,15 @@ export function _buildPoolRebalanceRoot(
deposits.forEach((deposit) => {
// If the repayment token and repayment chain ID do not map to a PoolRebalanceRoute graph, then
// there are no relevant L1 running balances.
if (
!clients.hubPoolClient.l2TokenHasPoolRebalanceRoute(
deposit.inputToken,
deposit.originChainId,
mainnetBundleEndBlock
)
) {
chainWithRefundsOnly.add(deposit.originChainId);
return;
}
const l1TokenCounterpart = clients.hubPoolClient.getL1TokenForL2TokenAtBlock(
toAddressType(inputToken, originChainId),
originChainId,
mainnetBundleEndBlock
);
if (!l1TokenCounterpart) {
chainWithRefundsOnly.add(deposit.originChainId);
return;
}
updateRunningBalance(runningBalances, originChainId, l1TokenCounterpart.toEvmAddress(), deposit.inputAmount);
});
});
Expand Down
8 changes: 7 additions & 1 deletion src/clients/BundleDataClient/utils/FillUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,21 @@ export function getRefundInformationFromFill(

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

assert(isDefined(l1TokenCounterpart), "getRefundInformationFromFill: l1TokenCounterpart is undefined");

const repaymentToken = hubPoolClient.getL2TokenForL1TokenAtBlock(
l1TokenCounterpart,
chainToSendRefundTo,
bundleEndBlockForMainnet
);
assert(isDefined(repaymentToken), "getRefundInformationFromFill: repaymentToken is undefined");

return {
chainToSendRefundTo,
Expand Down Expand Up @@ -183,6 +186,9 @@ function _repaymentChainTokenIsValid(
relayData.originChainId,
bundleEndBlockForMainnet
);
if (!l1TokenCounterpart) {
return false;
}
if (
!hubPoolClient.l2TokenEnabledForL1TokenAtBlock(
l1TokenCounterpart,
Expand Down
2 changes: 2 additions & 0 deletions src/clients/BundleDataClient/utils/PoolRebalanceUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,8 @@ export function updateRunningBalanceForDeposit(
deposit.originChainId,
mainnetBundleEndBlock
);
assert(isDefined(l1TokenCounterpart), "updateRunningBalanceForDeposit: l1TokenCounterpart is undefined");

updateRunningBalance(runningBalances, deposit.originChainId, l1TokenCounterpart.toEvmAddress(), updateAmount);
}

Expand Down
129 changes: 50 additions & 79 deletions src/clients/HubPoolClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ import {
fetchTokenInfo,
getCachedBlockForTimestamp,
getCurrentTime,
getNetworkName,
isDefined,
mapAsync,
paginatedEventQuery,
Expand Down Expand Up @@ -75,17 +74,11 @@ type HubPoolEvent =
| "RootBundleExecuted"
| "CrossChainContractsSet";

type L1TokensToDestinationTokens = {
[l1Token: string]: { [destinationChainId: number]: Address };
};

export type LpFeeRequest = Pick<Deposit, "originChainId" | "inputToken" | "inputAmount" | "quoteTimestamp"> & {
paymentChainId?: number;
};

export class HubPoolClient extends BaseAbstractClient {
// L1Token -> destinationChainId -> destinationToken
protected l1TokensToDestinationTokens: L1TokensToDestinationTokens = {};
protected l1Tokens: L1TokenInfo[] = []; // L1Tokens and their associated info.
// @dev `token` here is a 20-byte hex sting
protected lpTokens: { [token: string]: LpToken } = {};
Expand Down Expand Up @@ -192,87 +185,65 @@ export class HubPoolClient extends BaseAbstractClient {
l1Token: EvmAddress,
destinationChainId: number,
latestHubBlock = Number.MAX_SAFE_INTEGER
): Address {
): Address | undefined {
if (!this.l1TokensToDestinationTokensWithBlock?.[l1Token.toEvmAddress()]?.[destinationChainId]) {
const chain = getNetworkName(destinationChainId);
const { symbol } = this.l1Tokens.find(({ address }) => address.eq(l1Token)) ?? { symbol: l1Token.toString() };
throw new Error(`Could not find SpokePool mapping for ${symbol} on ${chain} and L1 token ${l1Token}`);
return undefined;
}
// Find the last mapping published before the target block.
const l2Token: DestinationTokenWithBlock | undefined = sortEventsDescending(
this.l1TokensToDestinationTokensWithBlock[l1Token.toEvmAddress()][destinationChainId]
).find((mapping: DestinationTokenWithBlock) => mapping.blockNumber <= latestHubBlock);
if (!l2Token) {
const chain = getNetworkName(destinationChainId);
const { symbol } = this.l1Tokens.find(({ address }) => address.eq(l1Token)) ?? { symbol: l1Token.toString() };
throw new Error(
`Could not find SpokePool mapping for ${symbol} on ${chain} at or before HubPool block ${latestHubBlock}!`
);
}
return l2Token.l2Token;
const l2Token: DestinationTokenWithBlock | undefined = this.l1TokensToDestinationTokensWithBlock[
l1Token.toEvmAddress()
][destinationChainId].find((mapping: DestinationTokenWithBlock) => mapping.blockNumber <= latestHubBlock);

return !l2Token || l2Token.l2Token.isZeroAddress() ? undefined : l2Token.l2Token;
}

// Returns the latest L1 token to use for an L2 token as of the input hub block.
getL1TokenForL2TokenAtBlock(
l2Token: Address,
destinationChainId: number,
latestHubBlock = Number.MAX_SAFE_INTEGER
): EvmAddress {
const l2Tokens = Object.keys(this.l1TokensToDestinationTokensWithBlock)
.filter((l1Token) => this.l2TokenEnabledForL1Token(EvmAddress.from(l1Token), destinationChainId))
.map((l1Token) => {
// Return all matching L2 token mappings that are equal to or earlier than the target block.
// @dev Since tokens on L2s (like Solana) can have 32 byte addresses, filter on the lower 20 bytes of the token only.
return this.l1TokensToDestinationTokensWithBlock[l1Token][destinationChainId].filter(
(dstTokenWithBlock) =>
dstTokenWithBlock.l2Token.truncateToBytes20() === l2Token.truncateToBytes20() &&
dstTokenWithBlock.blockNumber <= latestHubBlock
);
})
.flat();
if (l2Tokens.length === 0) {
const chain = getNetworkName(destinationChainId);
throw new Error(
`Could not find HubPool mapping for ${l2Token} on ${chain} at or before HubPool block ${latestHubBlock}!`
): EvmAddress | undefined {
const l2Tokens = Object.keys(this.l1TokensToDestinationTokensWithBlock).flatMap((l1Token) => {
// Get the latest L2 token mapping for the given L1 token.
// @dev Since tokens on L2s (like Solana) can have 32 byte addresses, filter on the lower 20 bytes of the token only.
const sortedL2Tokens = sortEventsDescending(
(this.l1TokensToDestinationTokensWithBlock[l1Token][destinationChainId] ?? []).filter(
(dstTokenWithBlock) => dstTokenWithBlock.blockNumber <= latestHubBlock
)
);
}
// Find the last mapping published before the target block.
return sortEventsDescending(l2Tokens)[0].l1Token;
// If the latest L2 token mapping is equal to the target L2 token, return it.
return sortedL2Tokens.length > 0 && sortedL2Tokens[0].l2Token.truncateToBytes20() === l2Token.truncateToBytes20()
? sortedL2Tokens[0]
: [];
});

return l2Tokens.length === 0 ? undefined : sortEventsDescending(l2Tokens)[0].l1Token;
}

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

l2TokenEnabledForL1Token(l1Token: EvmAddress, destinationChainId: number): boolean {
return this.l1TokensToDestinationTokens?.[l1Token.toEvmAddress()]?.[destinationChainId] != undefined;
return this.l2TokenEnabledForL1TokenAtBlock(l1Token, destinationChainId, Number.MAX_SAFE_INTEGER);
}

l2TokenEnabledForL1TokenAtBlock(l1Token: EvmAddress, destinationChainId: number, hubBlockNumber: number): boolean {
// Find the last mapping published before the target block.
const l2Token: DestinationTokenWithBlock | undefined = sortEventsDescending(
this.l1TokensToDestinationTokensWithBlock?.[l1Token.toEvmAddress()]?.[destinationChainId] ?? []
).find((mapping: DestinationTokenWithBlock) => mapping.blockNumber <= hubBlockNumber);
return l2Token !== undefined;
return l2Token !== undefined && !l2Token.l2Token.isZeroAddress();
}

l2TokenHasPoolRebalanceRoute(l2Token: Address, l2ChainId: number, hubPoolBlock = this.latestHeightSearched): boolean {
return Object.values(this.l1TokensToDestinationTokensWithBlock).some((destinationTokenMapping) => {
return Object.entries(destinationTokenMapping).some(([_l2ChainId, setPoolRebalanceRouteEvents]) => {
return setPoolRebalanceRouteEvents.some((e) => {
return (
e.blockNumber <= hubPoolBlock &&
e.l2Token.truncateToBytes20() === l2Token.truncateToBytes20() &&
Number(_l2ChainId) === l2ChainId
);
});
});
});
const l1Token = this.getL1TokenForL2TokenAtBlock(l2Token, l2ChainId, hubPoolBlock);
return l1Token !== undefined;
}

/**
Expand Down Expand Up @@ -407,9 +378,11 @@ export class HubPoolClient extends BaseAbstractClient {
const hubPoolTokens: { [k: string]: EvmAddress } = {};
const getHubPoolToken = (deposit: LpFeeRequest, quoteBlockNumber: number): EvmAddress | undefined => {
const tokenKey = `${deposit.originChainId}-${deposit.inputToken}`;
if (this.l2TokenHasPoolRebalanceRoute(deposit.inputToken, deposit.originChainId, quoteBlockNumber)) {
return (hubPoolTokens[tokenKey] ??= this.getL1TokenForDeposit({ ...deposit, quoteBlockNumber }));
} else return undefined;
const l1Token = this.getL1TokenForDeposit({ ...deposit, quoteBlockNumber });
if (!l1Token) {
return undefined;
}
return (hubPoolTokens[tokenKey] ??= l1Token);
};
const getHubPoolTokens = (): EvmAddress[] => dedupArray(Object.values(hubPoolTokens).filter(isDefined));

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

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

getSpokeActivationBlockForChain(chainId: number): number {
Expand Down Expand Up @@ -995,24 +968,22 @@ export class HubPoolClient extends BaseAbstractClient {
destinationToken = svmUsdc;
}

// If the destination token is set to the zero address in an event, then this means Across should no longer
// rebalance to this chain.
if (!destinationToken.isZeroAddress()) {
assign(this.l1TokensToDestinationTokens, [args.l1Token, args.destinationChainId], destinationToken);
assign(
this.l1TokensToDestinationTokensWithBlock,
[args.l1Token, args.destinationChainId],
[
{
l1Token: EvmAddress.from(args.l1Token),
l2Token: destinationToken,
blockNumber: args.blockNumber,
txnIndex: args.txnIndex,
logIndex: args.logIndex,
txnRef: args.txnRef,
},
]
);
const newRoute: DestinationTokenWithBlock = {
l1Token: EvmAddress.from(args.l1Token),
l2Token: destinationToken,
blockNumber: args.blockNumber,
txnIndex: args.txnIndex,
logIndex: args.logIndex,
txnRef: args.txnRef,
};
if (this.l1TokensToDestinationTokensWithBlock[args.l1Token]?.[args.destinationChainId]) {
// Events are most likely coming in descending orders already but just in case we sort them again.
this.l1TokensToDestinationTokensWithBlock[args.l1Token][args.destinationChainId] = sortEventsDescending([
...this.l1TokensToDestinationTokensWithBlock[args.l1Token][args.destinationChainId],
newRoute,
]);
} else {
assign(this.l1TokensToDestinationTokensWithBlock, [args.l1Token, args.destinationChainId], [newRoute]);
}
}
}
Expand Down
26 changes: 15 additions & 11 deletions src/clients/SpokePoolClient/SpokePoolClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -472,18 +472,22 @@ export abstract class SpokePoolClient extends BaseAbstractClient {
)
) {
return false;
} else {
const l1Token = this.hubPoolClient?.getL1TokenForL2TokenAtBlock(
deposit.inputToken,
deposit.originChainId,
deposit.quoteBlockNumber
);
return this.hubPoolClient.l2TokenEnabledForL1TokenAtBlock(
l1Token,
deposit.destinationChainId,
deposit.quoteBlockNumber
);
}

const l1Token = this.hubPoolClient?.getL1TokenForL2TokenAtBlock(
deposit.inputToken,
deposit.originChainId,
deposit.quoteBlockNumber
);
if (!l1Token) {
return false;
}

return this.hubPoolClient.l2TokenEnabledForL1TokenAtBlock(
l1Token,
deposit.destinationChainId,
deposit.quoteBlockNumber
);
}

/**
Expand Down
Loading