diff --git a/src/error.ts b/src/error.ts index 7ae4bc30..7ca2410e 100644 --- a/src/error.ts +++ b/src/error.ts @@ -1,6 +1,20 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +import { BigNumber } from "ethers"; +import { ViemClient } from "./types"; +// @ts-ignore +import { abi as obAbi } from "../test/abis/OrderBook.json"; +// @ts-ignore +import { abi as rp4Abi } from "../test/abis/RouteProcessor4.json"; +// @ts-ignore +import { abi as arbRp4Abi } from "../test/abis/RouteProcessorOrderBookV4ArbOrderTaker.json"; +// @ts-ignore +import { abi as genericArbAbi } from "../test/abis/GenericPoolOrderBookV4ArbOrderTaker.json"; import { + isHex, BaseError, + isAddress, RpcRequestError, + decodeErrorResult, ExecutionRevertedError, InsufficientFundsError, // InvalidInputRpcError, @@ -16,6 +30,22 @@ export enum ErrorSeverity { HIGH = "HIGH", } +export type DecodedError = { + name: string; + args: string[]; +}; + +export type RawError = { + code: number; + message: string; + data?: string; +}; + +export type TxRevertError = { + raw: RawError; + decoded?: DecodedError; +}; + /** * Get error with snapshot */ @@ -24,7 +54,20 @@ export function errorSnapshot(header: string, err: any): string { if (err instanceof BaseError) { if (err.shortMessage) message.push("Reason: " + err.shortMessage); if (err.name) message.push("Error: " + err.name); - if (err.details) message.push("Details: " + err.details); + if (err.details) { + message.push("Details: " + err.details); + if (err.details.includes("unknown reason")) { + const { raw, decoded } = parseRevertError(err); + if (decoded) { + message.push("Error Name: " + decoded.name); + if (decoded.args.length) { + message.push("Error Args: " + JSON.stringify(decoded.args)); + } + } else { + if (raw.data) message.push("Error Raw Data: " + raw.data); + } + } + } } else if (err instanceof Error) { if ("reason" in err) message.push("Reason: " + err.reason); else message.push("Reason: " + err.message); @@ -58,3 +101,105 @@ export function containsNodeError(err: BaseError): boolean { return false; } } + +export async function handleRevert( + viemClient: ViemClient, + hash: `0x${string}`, +): Promise<{ err: any; nodeError: boolean } | undefined> { + try { + const tx = await viemClient.getTransaction({ hash }); + await viemClient.call({ + account: tx.from, + to: tx.to, + data: tx.input, + gas: tx.gas, + gasPrice: tx.gasPrice, + blockNumber: tx.blockNumber, + }); + return undefined; + } catch (err) { + if (err instanceof BaseError) { + const { raw, decoded } = parseRevertError(err); + if (decoded || raw.data) return { err, nodeError: true }; + } + return { err, nodeError: false }; + } +} + +export function parseRevertError(error: BaseError): TxRevertError { + if ("cause" in error) { + return parseRevertError(error.cause as any); + } else { + let decoded: DecodedError | undefined; + const raw: RawError = { + code: (error as any).code ?? NaN, + message: error.message, + data: (error as any).data ?? undefined, + }; + if ("data" in error && isHex(error.data)) { + decoded = tryDecodeError(error.data); + } + return { raw, decoded }; + } +} + +export function tryDecodeError(data: `0x${string}`): DecodedError | undefined { + const handleArgs = (args: readonly unknown[]): string[] => { + return ( + args?.map((arg) => { + if (typeof arg === "string") { + return arg; + } + if (typeof arg === "bigint") { + const str = BigNumber.from(arg).toHexString(); + if (isAddress(str)) { + return str; + } else { + return arg.toString(); + } + } + if (typeof arg === "number") { + return arg.toString(); + } + try { + return arg!.toString(); + } catch (error) { + return ""; + } + }) ?? [] + ); + }; + try { + const result = decodeErrorResult({ data, abi: rp4Abi }); + return { + name: result.errorName, + args: handleArgs(result.args ?? []), + }; + } catch { + try { + const result = decodeErrorResult({ data, abi: obAbi }); + return { + name: result.errorName, + args: handleArgs(result.args ?? []), + }; + } catch { + try { + const result = decodeErrorResult({ data, abi: arbRp4Abi }); + return { + name: result.errorName, + args: handleArgs(result.args ?? []), + }; + } catch { + try { + const result = decodeErrorResult({ data, abi: genericArbAbi }); + return { + name: result.errorName, + args: handleArgs(result.args ?? []), + }; + } catch { + return undefined; + } + } + } + } +} diff --git a/src/modes/interOrderbook.ts b/src/modes/interOrderbook.ts index 3293ebbb..0fcc52ac 100644 --- a/src/modes/interOrderbook.ts +++ b/src/modes/interOrderbook.ts @@ -109,6 +109,15 @@ export async function dryrun({ try { blockNumber = Number(await viemClient.getBlockNumber()); spanAttributes["blockNumber"] = blockNumber; + try { + gasPrice = ethers.BigNumber.from(await viemClient.getGasPrice()) + .mul(config.gasPriceMultiplier) + .div("100") + .toBigInt(); + rawtx.gasPrice = gasPrice; + } catch { + /**/ + } gasLimit = ethers.BigNumber.from(await signer.estimateGas(rawtx)) .mul(config.gasLimitMultiplier) .div(100); @@ -153,6 +162,15 @@ export async function dryrun({ try { blockNumber = Number(await viemClient.getBlockNumber()); spanAttributes["blockNumber"] = blockNumber; + try { + gasPrice = ethers.BigNumber.from(await viemClient.getGasPrice()) + .mul(config.gasPriceMultiplier) + .div("100") + .toBigInt(); + rawtx.gasPrice = gasPrice; + } catch { + /**/ + } gasLimit = ethers.BigNumber.from(await signer.estimateGas(rawtx)) .mul(config.gasLimitMultiplier) .div(100); diff --git a/src/modes/intraOrderbook.ts b/src/modes/intraOrderbook.ts index 498e4880..b0f6132b 100644 --- a/src/modes/intraOrderbook.ts +++ b/src/modes/intraOrderbook.ts @@ -106,6 +106,15 @@ export async function dryrun({ try { blockNumber = Number(await viemClient.getBlockNumber()); spanAttributes["blockNumber"] = blockNumber; + try { + gasPrice = ethers.BigNumber.from(await viemClient.getGasPrice()) + .mul(config.gasPriceMultiplier) + .div("100") + .toBigInt(); + rawtx.gasPrice = gasPrice; + } catch { + /**/ + } gasLimit = ethers.BigNumber.from(await signer.estimateGas(rawtx)) .mul(config.gasLimitMultiplier) .div(100); @@ -160,6 +169,15 @@ export async function dryrun({ try { blockNumber = Number(await viemClient.getBlockNumber()); spanAttributes["blockNumber"] = blockNumber; + try { + gasPrice = ethers.BigNumber.from(await viemClient.getGasPrice()) + .mul(config.gasPriceMultiplier) + .div("100") + .toBigInt(); + rawtx.gasPrice = gasPrice; + } catch { + /**/ + } gasLimit = ethers.BigNumber.from(await signer.estimateGas(rawtx)) .mul(config.gasLimitMultiplier) .div(100); diff --git a/src/modes/routeProcessor.ts b/src/modes/routeProcessor.ts index 29311ea8..50dc9e0a 100644 --- a/src/modes/routeProcessor.ts +++ b/src/modes/routeProcessor.ts @@ -174,6 +174,15 @@ export async function dryrun({ try { blockNumber = Number(await viemClient.getBlockNumber()); spanAttributes["blockNumber"] = blockNumber; + try { + gasPrice = ethers.BigNumber.from(await viemClient.getGasPrice()) + .mul(config.gasPriceMultiplier) + .div("100") + .toBigInt(); + rawtx.gasPrice = gasPrice; + } catch { + /**/ + } gasLimit = ethers.BigNumber.from(await signer.estimateGas(rawtx)) .mul(config.gasLimitMultiplier) .div(100); @@ -220,6 +229,15 @@ export async function dryrun({ try { blockNumber = Number(await viemClient.getBlockNumber()); spanAttributes["blockNumber"] = blockNumber; + try { + gasPrice = ethers.BigNumber.from(await viemClient.getGasPrice()) + .mul(config.gasPriceMultiplier) + .div("100") + .toBigInt(); + rawtx.gasPrice = gasPrice; + } catch { + /**/ + } gasLimit = ethers.BigNumber.from(await signer.estimateGas(rawtx)) .mul(config.gasLimitMultiplier) .div(100); diff --git a/src/processOrders.ts b/src/processOrders.ts index f61fbb65..bae240ee 100644 --- a/src/processOrders.ts +++ b/src/processOrders.ts @@ -9,7 +9,7 @@ import { BigNumber, Contract, ethers } from "ethers"; import { Tracer } from "@opentelemetry/sdk-trace-base"; import { Context, SpanStatusCode } from "@opentelemetry/api"; import { fundOwnedOrders, getNonce, rotateAccounts } from "./account"; -import { containsNodeError, ErrorSeverity, errorSnapshot } from "./error"; +import { containsNodeError, ErrorSeverity, errorSnapshot, handleRevert } from "./error"; import { Report, BotConfig, @@ -326,11 +326,13 @@ export const processOrders = async ( } else if (e.reason === ProcessPairHaltReason.TxReverted) { // Tx reverted onchain, this can happen for example // because of mev front running or false positive opportunities, etc + let message = "transaction reverted onchain"; + if (e.error) { + message = errorSnapshot(message, e.error); + span.setAttribute("errorDetails", message); + } span.setAttribute("severity", ErrorSeverity.HIGH); - span.setStatus({ - code: SpanStatusCode.ERROR, - message: "transaction reverted onchain", - }); + span.setStatus({ code: SpanStatusCode.ERROR, message }); span.setAttribute("unsuccessfulClear", true); span.setAttribute("txReverted", true); } else if (e.reason === ProcessPairHaltReason.TxMineFailed) { @@ -777,6 +779,11 @@ export async function processPair(args: { return result; } else { // keep track of gas consumption of the account + const simulation = await handleRevert(config.viemClient as any, txhash); + if (simulation) { + result.error = simulation.err; + spanAttributes["txNoneNodeError"] = !simulation.nodeError; + } result.report = { status: ProcessPairReportStatus.FoundOpportunity, txUrl, diff --git a/test/abis/RouteProcessor4.json b/test/abis/RouteProcessor4.json new file mode 100644 index 00000000..68ecdc7f --- /dev/null +++ b/test/abis/RouteProcessor4.json @@ -0,0 +1 @@ +{"abi":[{"inputs":[{"internalType":"address","name":"_bentoBox","type":"address"},{"internalType":"address[]","name":"priviledgedUserList","type":"address[]"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"}],"name":"MinimalOutputBalanceViolation","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":false,"internalType":"address","name":"to","type":"address"},{"indexed":true,"internalType":"address","name":"tokenIn","type":"address"},{"indexed":true,"internalType":"address","name":"tokenOut","type":"address"},{"indexed":false,"internalType":"uint256","name":"amountIn","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"amountOutMin","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"amountOut","type":"uint256"}],"name":"Route","type":"event"},{"inputs":[{"internalType":"int256","name":"amount0Delta","type":"int256"},{"internalType":"int256","name":"amount1Delta","type":"int256"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"algebraSwapCallback","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"bentoBox","outputs":[{"internalType":"contract IBentoBoxMinimal","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"int256","name":"amount0Delta","type":"int256"},{"internalType":"int256","name":"amount1Delta","type":"int256"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"pancakeV3SwapCallback","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"pause","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"priviledgedUsers","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"tokenIn","type":"address"},{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"address","name":"tokenOut","type":"address"},{"internalType":"uint256","name":"amountOutMin","type":"uint256"},{"internalType":"address","name":"to","type":"address"},{"internalType":"bytes","name":"route","type":"bytes"}],"name":"processRoute","outputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"resume","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"user","type":"address"},{"internalType":"bool","name":"priviledge","type":"bool"}],"name":"setPriviledge","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address payable","name":"transferValueTo","type":"address"},{"internalType":"uint256","name":"amountValueTransfer","type":"uint256"},{"internalType":"address","name":"tokenIn","type":"address"},{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"address","name":"tokenOut","type":"address"},{"internalType":"uint256","name":"amountOutMin","type":"uint256"},{"internalType":"address","name":"to","type":"address"},{"internalType":"bytes","name":"route","type":"bytes"}],"name":"transferValueAndprocessRoute","outputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"int256","name":"amount0Delta","type":"int256"},{"internalType":"int256","name":"amount1Delta","type":"int256"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"uniswapV3SwapCallback","outputs":[],"stateMutability":"nonpayable","type":"function"},{"stateMutability":"payable","type":"receive"}]} \ No newline at end of file