Skip to content

Commit

Permalink
Merge branch 'master' into bz/mode
Browse files Browse the repository at this point in the history
  • Loading branch information
bmzig authored May 21, 2024
2 parents ccd24d2 + c67b9e7 commit abdd013
Show file tree
Hide file tree
Showing 17 changed files with 424 additions and 199 deletions.
49 changes: 27 additions & 22 deletions src/clients/InventoryClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -383,26 +383,29 @@ export class InventoryClient {
return isInputTokenUSDC && isOutputTokenBridgedUSDC;
}

// Work out where a relay should be refunded to optimally manage the bots inventory. If the inventory management logic
// not enabled then return funds on the chain the deposit was filled on Else, use the following algorithm for each
// of the origin and destination chain:
// a) Find the chain virtual balance (current balance + pending relays + pending refunds) minus current shortfall.
// b) Find the cumulative virtual balance, including the total refunds on all chains and excluding current shortfall.
// c) Consider the size of a and b post relay (i.e after the relay is paid and all current transfers are settled what
// will the balances be on the target chain and the overall cumulative balance).
// d) Use c to compute what the post relay post current in-flight transactions allocation would be. Compare this
// number to the target threshold and:
// If this number is less than the target for the destination chain + rebalance then select destination chain. We
// slightly prefer destination to origin chain to support relayer capital efficiency.
// Else, if this number is less than the target for the origin chain + rebalance then select origin
// chain.
// Else, take repayment on the Hub chain for ease of transferring out of L1 to any L2.
async determineRefundChainId(deposit: V3Deposit, l1Token?: string): Promise<number> {
/*
* Return all eligible repayment chains for a deposit. If inventory management is enabled, then this function will
* only choose chains where the post-relay balance allocation for a potential repayment chain is under the maximum
* allowed allocation on that chain. Origin, Destination, and HubChains are always evaluated as potential
* repayment chains in addition to "Slow Withdrawal chains" such as Base, Optimism and Arbitrum for which
* taking repayment would reduce HubPool utilization. Post-relay allocation percentages take into
* account pending cross-chain inventory-management transfers, upcoming bundle refunds, token shortfalls
* needed to cover other unfilled deposits in addition to current token balances. Slow withdrawal chains are only
* selected if the SpokePool's running balance for that chain is over the system's desired target.
* @dev The HubChain is always evaluated as a fallback option if the inventory management is enabled and all other
* chains are over-allocated.
* @dev If inventory management is disabled, then destinationChain is used as a default.
* @param deposit Deposit to determine repayment chains for.
* @param l1Token L1Token linked with deposited inputToken and repayement chain refund token.
* @returns list of chain IDs that are possible repayment chains for the deposit, sorted from highest
* to lowest priority.
*/
async determineRefundChainId(deposit: V3Deposit, l1Token?: string): Promise<number[]> {
const { originChainId, destinationChainId, inputToken, outputToken, outputAmount, inputAmount } = deposit;
const hubChainId = this.hubPoolClient.chainId;

if (!this.isInventoryManagementEnabled()) {
return destinationChainId;
return [destinationChainId];
}

// The InventoryClient assumes 1:1 equivalency between input and output tokens. At the moment there is no support
Expand Down Expand Up @@ -469,6 +472,7 @@ export class InventoryClient {
chainsToEvaluate.push(originChainId);
}

const eligibleRefundChains: number[] = [];
// At this point, all chains to evaluate have defined token configs and are sorted in order of
// highest priority to take repayment on, assuming the chain is under-allocated.
for (const _chain of chainsToEvaluate) {
Expand Down Expand Up @@ -535,15 +539,16 @@ export class InventoryClient {
}
);
if (expectedPostRelayAllocation.lte(thresholdPct)) {
return _chain;
eligibleRefundChains.push(_chain);
}
}

// None of the chain allocation percentages are lower than their target so take
// repayment on the hub chain by default. The caller has also set a token config so they are not expecting
// repayments to default to destination chain. If caller wanted repayments to default to destination
// chain, then they should not set a token config.
return hubChainId;
// Always add hubChain as a fallback option if inventory management is enabled. If none of the chainsToEvaluate
// were selected, then this function will return just the hub chain as a fallback option.
if (!eligibleRefundChains.includes(hubChainId)) {
eligibleRefundChains.push(hubChainId);
}
return eligibleRefundChains;
}

/**
Expand Down
25 changes: 16 additions & 9 deletions src/clients/ProfitClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -433,17 +433,22 @@ export class ProfitClient {
return fillAmount.mul(tokenPriceInUsd).div(bn10.pow(l1TokenInfo.decimals));
}

async getFillProfitability(deposit: V3Deposit, lpFeePct: BigNumber, l1Token: L1Token): Promise<FillProfit> {
async getFillProfitability(
deposit: V3Deposit,
lpFeePct: BigNumber,
l1Token: L1Token,
repaymentChainId: number
): Promise<FillProfit> {
const minRelayerFeePct = this.minRelayerFeePct(l1Token.symbol, deposit.originChainId, deposit.destinationChainId);

const fill = await this.calculateFillProfitability(deposit, lpFeePct, minRelayerFeePct);
if (!fill.profitable || this.debugProfitability) {
const { depositId, originChainId } = deposit;
const { depositId } = deposit;
const profitable = fill.profitable ? "profitable" : "unprofitable";

this.logger.debug({
at: "ProfitClient#getFillProfitability",
message: `${l1Token.symbol} v3 deposit ${depositId} on chain ${originChainId} is ${profitable}`,
message: `${l1Token.symbol} v3 deposit ${depositId} with repayment on ${repaymentChainId} is ${profitable}`,
deposit,
inputTokenPriceUsd: formatEther(fill.inputTokenPriceUsd),
inputTokenAmountUsd: formatEther(fill.inputAmountUsd),
Expand Down Expand Up @@ -472,17 +477,19 @@ export class ProfitClient {
async isFillProfitable(
deposit: V3Deposit,
lpFeePct: BigNumber,
l1Token: L1Token
): Promise<Pick<FillProfit, "profitable" | "nativeGasCost" | "tokenGasCost" | "grossRelayerFeePct">> {
l1Token: L1Token,
repaymentChainId: number
): Promise<Pick<FillProfit, "profitable" | "nativeGasCost" | "tokenGasCost" | "netRelayerFeePct">> {
let profitable = false;
let grossRelayerFeePct = bnZero;
let netRelayerFeePct = bnZero;
let nativeGasCost = uint256Max;
let tokenGasCost = uint256Max;
try {
({ profitable, grossRelayerFeePct, nativeGasCost, tokenGasCost } = await this.getFillProfitability(
({ profitable, netRelayerFeePct, nativeGasCost, tokenGasCost } = await this.getFillProfitability(
deposit,
lpFeePct,
l1Token
l1Token,
repaymentChainId
));
} catch (err) {
this.logger.debug({
Expand All @@ -497,7 +504,7 @@ export class ProfitClient {
profitable: profitable || (this.isTestnet && nativeGasCost.lt(uint256Max)),
nativeGasCost,
tokenGasCost,
grossRelayerFeePct,
netRelayerFeePct,
};
}

Expand Down
31 changes: 20 additions & 11 deletions src/finalizer/utils/arbitrum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {
getRedisCache,
getBlockForTimestamp,
getL1TokenInfo,
compareAddressesSimple,
TOKEN_SYMBOLS_MAP,
} from "../../utils";
import { TokensBridged } from "../../interfaces";
import { HubPoolClient, SpokePoolClient } from "../../clients";
Expand All @@ -30,7 +32,7 @@ export async function arbitrumOneFinalizer(
// Arbitrum takes 7 days to finalize withdrawals, so don't look up events younger than that.
const redis = await getRedisCache(logger);
const [fromBlock, toBlock] = await Promise.all([
getBlockForTimestamp(chainId, getCurrentTime() - 9 * 60 * 60 * 24, undefined, redis),
getBlockForTimestamp(chainId, getCurrentTime() - 14 * 60 * 60 * 24, undefined, redis),
getBlockForTimestamp(chainId, getCurrentTime() - 7 * 60 * 60 * 24, undefined, redis),
]);
logger.debug({
Expand Down Expand Up @@ -139,17 +141,24 @@ async function getAllMessageStatuses(
// This is important for bridge transactions containing multiple events.
const logIndexesForMessage = getUniqueLogIndex(tokensBridged);
return (
await Promise.all(
tokensBridged.map((e, i) => getMessageOutboxStatusAndProof(logger, e, mainnetSigner, logIndexesForMessage[i]))
(
await Promise.all(
tokensBridged.map((e, i) => getMessageOutboxStatusAndProof(logger, e, mainnetSigner, logIndexesForMessage[i]))
)
)
)
.map((result, i) => {
return {
...result,
info: tokensBridged[i],
};
})
.filter((result) => result.message !== undefined);
.map((result, i) => {
return {
...result,
info: tokensBridged[i],
};
})
// USDC withdrawals for Arbitrum should be finalized via the CCTP Finalizer.
.filter(
(result) =>
result.message !== undefined &&
!compareAddressesSimple(result.info.l2TokenAddress, TOKEN_SYMBOLS_MAP["_USDC"].addresses[CHAIN_ID])
)
);
}

async function getMessageOutboxStatusAndProof(
Expand Down
4 changes: 1 addition & 3 deletions src/finalizer/utils/cctp/l1ToL2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,7 @@ export async function cctpL1toL2Finalizer(
spokePoolClient: SpokePoolClient,
l1ToL2AddressesToFinalize: string[]
): Promise<FinalizerPromise> {
// Let's just assume for now CCTP transfers don't take longer than 1 day and can
// happen very quickly.
const lookback = getCurrentTime() - 60 * 60 * 24;
const lookback = getCurrentTime() - 60 * 60 * 24 * 7;
const redis = await getRedisCache(logger);
const fromBlock = await getBlockForTimestamp(hubPoolClient.chainId, lookback, undefined, redis);
logger.debug({
Expand Down
29 changes: 20 additions & 9 deletions src/finalizer/utils/cctp/l2ToL1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import { CONTRACT_ADDRESSES, Multicall2Call, chainIdsToCctpDomains } from "../..
import {
Contract,
Signer,
TOKEN_SYMBOLS_MAP,
assert,
compareAddressesSimple,
getBlockForTimestamp,
getCurrentTime,
getNetworkName,
Expand All @@ -24,11 +26,9 @@ export async function cctpL2toL1Finalizer(
hubPoolClient: HubPoolClient,
spokePoolClient: SpokePoolClient
): Promise<FinalizerPromise> {
// Let's just assume for now CCTP transfers don't take longer than 1 day and can
// happen very quickly.
const lookback = getCurrentTime() - 60 * 60 * 24;
const lookback = getCurrentTime() - 60 * 60 * 24 * 7;
const redis = await getRedisCache(logger);
const fromBlock = await getBlockForTimestamp(hubPoolClient.chainId, lookback, undefined, redis);
const fromBlock = await getBlockForTimestamp(spokePoolClient.chainId, lookback, undefined, redis);
logger.debug({
at: `Finalizer#CCTPL2ToL1Finalizer:${spokePoolClient.chainId}`,
message: `MessageSent event filter for ${getNetworkName(spokePoolClient.chainId)} to L1`,
Expand Down Expand Up @@ -67,14 +67,25 @@ async function resolveRelatedTxnReceipts(
targetDestinationChainId: number,
latestBlockToFinalize: number
): Promise<DecodedCCTPMessage[]> {
const sourceChainId = client.chainId;
// Dedup the txnReceipt list because there might be multiple tokens bridged events in the same txn hash.

const uniqueTxnHashes = new Set<string>();
client
.getTokensBridged()
.filter(
(bridgeEvent) =>
bridgeEvent.blockNumber >= latestBlockToFinalize &&
compareAddressesSimple(bridgeEvent.l2TokenAddress, TOKEN_SYMBOLS_MAP._USDC.addresses[sourceChainId])
)
.forEach((bridgeEvent) => uniqueTxnHashes.add(bridgeEvent.transactionHash));

// Resolve the receipts to all collected txns
const txnReceipts = await Promise.all(
client
.getTokensBridged()
.filter((bridgeEvent) => bridgeEvent.blockNumber >= latestBlockToFinalize)
.map((bridgeEvent) => client.spokePool.provider.getTransactionReceipt(bridgeEvent.transactionHash))
Array.from(uniqueTxnHashes).map((hash) => client.spokePool.provider.getTransactionReceipt(hash))
);
return resolveCCTPRelatedTxns(txnReceipts, client.chainId, targetDestinationChainId);

return resolveCCTPRelatedTxns(txnReceipts, sourceChainId, targetDestinationChainId);
}

/**
Expand Down
4 changes: 1 addition & 3 deletions src/finalizer/utils/linea/l1ToL2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,7 @@ export async function lineaL1ToL2Finalizer(
);

// Optimize block range for querying Linea's MessageSent events on L1.
// We want to conservatively query for events that are between 0 and 24 hours old
// because Linea L1->L2 messages are claimable after ~20 mins.
const { fromBlock, toBlock } = await getBlockRangeByHoursOffsets(l1ChainId, 24, 0);
const { fromBlock, toBlock } = await getBlockRangeByHoursOffsets(l1ChainId, 24 * 7, 0);
logger.debug({
at: "Finalizer#LineaL1ToL2Finalizer",
message: "Linea MessageSent event filter",
Expand Down
5 changes: 2 additions & 3 deletions src/finalizer/utils/linea/l2ToL1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,8 @@ export async function lineaL2ToL1Finalizer(
const getMessagesWithStatusByTxHash = makeGetMessagesWithStatusByTxHash(l2Contract, l1ClaimingService);

// Optimize block range for querying relevant source events on L2.
// We want to conservatively query for events that are between 8 and 72 hours old
// because Linea L2->L1 messages are claimable after 6 - 32 hours
const { fromBlock, toBlock } = await getBlockRangeByHoursOffsets(l2ChainId, 72, 8);
// Linea L2->L1 messages are claimable after 6 - 32 hours
const { fromBlock, toBlock } = await getBlockRangeByHoursOffsets(l2ChainId, 24 * 8, 6);
logger.debug({
at: "Finalizer#LineaL2ToL1Finalizer",
message: "Linea TokensBridged event filter",
Expand Down
42 changes: 27 additions & 15 deletions src/finalizer/utils/opStack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { HubPoolClient, SpokePoolClient } from "../../clients";
import { TokensBridged } from "../../interfaces";
import {
BigNumber,
CHAIN_IDs,
chainIsOPStack,
compareAddressesSimple,
convertFromWei,
getBlockForTimestamp,
getCachedProvider,
Expand All @@ -16,6 +18,7 @@ import {
getUniqueLogIndex,
groupObjectCountsByProp,
Signer,
TOKEN_SYMBOLS_MAP,
winston,
} from "../../utils";
import { Multicall2Call } from "../../common";
Expand Down Expand Up @@ -53,8 +56,8 @@ export async function opStackFinalizer(
// - Don't try to withdraw tokens that are not past the 7 day challenge period
const redis = await getRedisCache(logger);
const [earliestBlockToFinalize, latestBlockToProve] = await Promise.all([
getBlockForTimestamp(chainId, getCurrentTime() - 14 * 60 * 60 * 24, undefined, redis),
getBlockForTimestamp(chainId, getCurrentTime() - 7 * 60 * 60 * 24, undefined, redis),
getBlockForTimestamp(chainId, getCurrentTime() - 60 * 60 * 24, undefined, redis),
]);
const { recentTokensBridgedEvents = [], olderTokensBridgedEvents = [] } = groupBy(
spokePoolClient.getTokensBridged(),
Expand Down Expand Up @@ -129,22 +132,31 @@ async function getCrossChainMessages(
const logIndexesForMessage = getUniqueLogIndex(tokensBridged);

return (
await Promise.all(
tokensBridged.map(
async (l2Event, i) =>
(
await crossChainMessenger.getMessagesByTransaction(l2Event.transactionHash, {
direction: optimismSDK.MessageDirection.L2_TO_L1,
})
)[logIndexesForMessage[i]]
(
await Promise.all(
tokensBridged.map(
async (l2Event, i) =>
(
await crossChainMessenger.getMessagesByTransaction(l2Event.transactionHash, {
direction: optimismSDK.MessageDirection.L2_TO_L1,
})
)[logIndexesForMessage[i]]
)
)
)
).map((message, i) => {
return {
message,
event: tokensBridged[i],
};
});
.map((message, i) => {
return {
message,
event: tokensBridged[i],
};
})
// USDC withdrawals for Base and Optimism should be finalized via the CCTP Finalizer.
.filter(
(e) =>
!compareAddressesSimple(e.event.l2TokenAddress, TOKEN_SYMBOLS_MAP["_USDC"].addresses[_chainId]) ||
!(_chainId === CHAIN_IDs.BASE || _chainId === CHAIN_IDs.OPTIMISM)
)
);
}

async function getMessageStatuses(
Expand Down
10 changes: 9 additions & 1 deletion src/finalizer/utils/polygon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
getRedisCache,
getBlockForTimestamp,
getL1TokenInfo,
compareAddressesSimple,
TOKEN_SYMBOLS_MAP,
} from "../../utils";
import { EthersError, TokensBridged } from "../../interfaces";
import { HubPoolClient, SpokePoolClient } from "../../clients";
Expand Down Expand Up @@ -46,7 +48,7 @@ export async function polygonFinalizer(
const { chainId } = spokePoolClient;

const posClient = await getPosClient(signer);
const lookback = getCurrentTime() - 60 * 60 * 24;
const lookback = getCurrentTime() - 60 * 60 * 24 * 7;
const redis = await getRedisCache(logger);
const fromBlock = await getBlockForTimestamp(chainId, lookback, undefined, redis);

Expand Down Expand Up @@ -110,6 +112,12 @@ async function getFinalizableTransactions(
const exitStatus = await Promise.all(
checkpointedTokensBridged.map(async (_, i) => {
const payload = payloads[i];
const { chainId, l2TokenAddress } = tokensBridged[i];

if (compareAddressesSimple(l2TokenAddress, TOKEN_SYMBOLS_MAP._USDC.addresses[chainId])) {
return { status: "NON_CANONICAL_BRIDGE" };
}

try {
// If we can estimate gas for exit transaction call, then we can exit the burn tx, otherwise its likely
// been processed. Note this will capture mislabel some exit txns that fail for other reasons as "exit
Expand Down
2 changes: 1 addition & 1 deletion src/finalizer/utils/zkSync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export async function zkSyncFinalizer(
// older than 2 days and earlier than 1 day.
const redis = await getRedisCache(logger);
const [fromBlock, toBlock] = await Promise.all([
getBlockForTimestamp(l2ChainId, getCurrentTime() - 2 * 60 * 60 * 24, undefined, redis),
getBlockForTimestamp(l2ChainId, getCurrentTime() - 8 * 60 * 60 * 24, undefined, redis),
getBlockForTimestamp(l2ChainId, getCurrentTime() - 1 * 60 * 60 * 24, undefined, redis),
]);
logger.debug({
Expand Down
Loading

0 comments on commit abdd013

Please sign in to comment.