Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(finalizer): Handle 1:many withdrawal:calldata #953

Merged
merged 5 commits into from
Oct 2, 2023
Merged
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
104 changes: 67 additions & 37 deletions src/finalizer/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import assert from "assert";
import { typeguards, utils as sdkUtils } from "@across-protocol/sdk-v2";
import { providers } from "ethers";
import { groupBy } from "lodash";
import {
Wallet,
Expand Down Expand Up @@ -27,7 +28,11 @@ import {
FINALIZER_TOKENBRIDGE_LOOKBACK,
Multicall2Call,
} from "../common";
import { ChainFinalizer, Withdrawal } from "./types";
import { ChainFinalizer, Withdrawal as _Withdrawal } from "./types";

type TransactionReceipt = providers.TransactionReceipt;

type Withdrawal = _Withdrawal & { txns: Multicall2Call[] };

const { isError, isEthersError } = typeguards;

Expand Down Expand Up @@ -70,10 +75,7 @@ export async function finalize(
// Note: Could move this into a client in the future to manage # of calls and chunk calls based on
// input byte length.
const multicall2 = getMultisender(hubChainId, hubSigner);
const finalizationsToBatch: {
callData: Multicall2Call;
withdrawal: Withdrawal;
}[] = [];
const finalizationsToBatch: Withdrawal[] = [];

// For each chain, delegate to a handler to look up any TokensBridged events and attempt finalization.
for (const chainId of configuredChainIds) {
Expand All @@ -100,75 +102,103 @@ export async function finalize(

const network = getNetworkName(chainId);
logger.debug({ at: "finalize", message: `Spawning ${network} finalizer.`, latestBlockToFinalize });
const { callData, withdrawals } = await chainFinalizer(
const { callData: txns, withdrawals: _withdrawals } = await chainFinalizer(
logger,
hubSigner,
hubPoolClient,
client,
latestBlockToFinalize
);
logger.debug({ at: "finalize", message: `Found ${callData.length} ${network} withdrawals for finalization.` });
logger.debug({
at: "finalize",
message: `Found ${_withdrawals.length} ${network} withdrawals for finalization.`,
});

const txns = callData.map((callData, i) => {
return { callData, withdrawal: withdrawals[i] };
if (_withdrawals.length === 0) {
continue;
}

if (![1, 2].includes(txns.length / _withdrawals.length)) {
pxrl marked this conversation as resolved.
Show resolved Hide resolved
logger.warn({
at: "finalize",
message: `Unexpected ${network} txn/withdrawal ratio (${txns.length / _withdrawals.length}).`,
txns,
withdrawals: _withdrawals,
});
continue;
}

// Normalise withdawals, such that 1 withdrawal has an array of calldata (usually only 1 call), but can be more.
// @todo: Refactor the underlying adapters so they return in this data structure.
const withdrawals: Withdrawal[] = _withdrawals.map((withdrawal) => {
return { ...withdrawal, txns: [] };
});

finalizationsToBatch.push(...txns);
// Append calldata. If multiple calls are needed per withdrawal (i.e. Polygon),
// require that the 2nd batch is appended to the first.
txns.forEach((txn, i) => withdrawals[i % withdrawals.length].txns.push(txn));

finalizationsToBatch.push(...withdrawals);
}

// Ensure each transaction would succeed in isolation.
const finalizations = await sdkUtils.filterAsync(finalizationsToBatch, async (finalization) => {
const finalizations = await sdkUtils.filterAsync(finalizationsToBatch, async (withdrawal) => {
const { txns } = withdrawal;
try {
const { target: to, callData: data } = finalization.callData;
await multicall2.provider.estimateGas({ to, data });
const txn = await multicall2.populateTransaction.aggregate(txns);
await multicall2.provider.estimateGas(txn);
return true;
} catch (err) {
const { l2ChainId, type, l1TokenSymbol, amount } = finalization.withdrawal;
const { l2ChainId, type, l1TokenSymbol, amount } = withdrawal;
const network = getNetworkName(l2ChainId);
logger.info({
at: "finalizer",
message: `Failed to estimate gas for ${network} ${amount} ${l1TokenSymbol} ${type}.`,
txns,
reason: isEthersError(err) ? err.reason : isError(err) ? err.message : "unknown error",
});
return false;
}
});

if (finalizations.length > 0) {
let txn: TransactionReceipt;
try {
// Note: If the sum of finalizations approaches the gas limit, consider slicing them up.
const callData = finalizations.map(({ callData }) => callData);
const txn = await (await multicall2.aggregate(callData)).wait();

const { withdrawals = [], proofs = [] } = groupBy(
finalizations.map(({ withdrawal }) => withdrawal),
({ type }) => (type === "withdrawal" ? "withdrawals" : "proofs")
);
proofs.forEach(({ l2ChainId, amount, l1TokenSymbol: symbol }) => {
const spokeChain = getNetworkName(l2ChainId);
logger.info({
at: "Finalizer",
message: `Submitted proof on chain ${hubChain} to initiate ${spokeChain} withdrawal of ${amount} ${symbol} 🔜`,
transactionHash: blockExplorerLink(txn.transactionHash, hubChainId),
});
});
withdrawals.forEach(({ l2ChainId, amount, l1TokenSymbol: symbol }) => {
const spokeChain = getNetworkName(l2ChainId);
logger.info({
at: "Finalizer",
message: `Finalized ${spokeChain} withdrawal for ${amount} ${symbol} 🪃`,
transactionHash: blockExplorerLink(txn.transactionHash, hubChainId),
});
});
const txns = finalizations.map(({ txns }) => txns).flat();
txn = await (await multicall2.aggregate(txns)).wait();
} catch (_error) {
const error = _error as Error;
logger.warn({
at: "Finalizer",
message: "Error creating aggregateTx",
reason: error.stack || error.message || error.toString(),
notificationPath: "across-error",
finalizations,
});

return;
}

const { withdrawals = [], proofs = [] } = groupBy(finalizations, ({ type }) =>
type === "withdrawal" ? "withdrawals" : "proofs"
);
proofs.forEach(({ l2ChainId, amount, l1TokenSymbol: symbol }) => {
const spokeChain = getNetworkName(l2ChainId);
logger.info({
at: "Finalizer",
message: `Submitted proof on chain ${hubChain} to initiate ${spokeChain} withdrawal of ${amount} ${symbol} 🔜`,
transactionHash: blockExplorerLink(txn.transactionHash, hubChainId),
});
});
withdrawals.forEach(({ l2ChainId, amount, l1TokenSymbol: symbol }) => {
const spokeChain = getNetworkName(l2ChainId);
logger.info({
at: "Finalizer",
message: `Finalized ${spokeChain} withdrawal for ${amount} ${symbol} 🪃`,
transactionHash: blockExplorerLink(txn.transactionHash, hubChainId),
});
});
}
}

Expand Down