diff --git a/src/error.ts b/src/error.ts index 7ae4bc30..628efade 100644 --- a/src/error.ts +++ b/src/error.ts @@ -1,6 +1,18 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +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, RpcRequestError, + decodeErrorResult, ExecutionRevertedError, InsufficientFundsError, // InvalidInputRpcError, @@ -16,6 +28,31 @@ export enum ErrorSeverity { HIGH = "HIGH", } +/** + * Specifies a decoded contract error + */ +export type DecodedError = { + name: string; + args: string[]; +}; + +/** + * Raw error returned from rpc call + */ +export type RawError = { + code: number; + message: string; + data?: string; +}; + +/** + * Represents a revert error that happened for a transaction + */ +export type TxRevertError = { + raw: RawError; + decoded?: DecodedError; +}; + /** * Get error with snapshot */ @@ -24,7 +61,24 @@ 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.name.includes("unknown reason") || + err.details.includes("unknown reason") || + err.shortMessage.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 +112,104 @@ export function containsNodeError(err: BaseError): boolean { return false; } } + +/** + * Handles a reverted transaction by simulating it and returning the revert error + */ +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 }; + } +} + +/** + * Parses a revert error to TxRevertError type + */ +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 }; + } +} + +/** + * Tries to decode an error data with known contract error selectors + */ +export function tryDecodeError(data: `0x${string}`): DecodedError | undefined { + const handleArgs = (args: readonly unknown[]): string[] => { + return ( + args?.map((arg) => { + if (typeof arg === "string") { + return arg; + } else { + 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..b5be7a2b 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); diff --git a/src/modes/intraOrderbook.ts b/src/modes/intraOrderbook.ts index 498e4880..7cb2c52f 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); diff --git a/src/modes/routeProcessor.ts b/src/modes/routeProcessor.ts index 29311ea8..452bbda3 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); diff --git a/src/processOrders.ts b/src/processOrders.ts index f61fbb65..e2654e67 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,17 @@ 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 - span.setAttribute("severity", ErrorSeverity.HIGH); - span.setStatus({ - code: SpanStatusCode.ERROR, - message: "transaction reverted onchain", - }); + let message = "transaction reverted onchain"; + if (e.error) { + message = errorSnapshot(message, e.error); + span.setAttribute("errorDetails", message); + } + if (e.spanAttributes["txNoneNodeError"]) { + span.setAttribute("severity", ErrorSeverity.HIGH); + span.setStatus({ code: SpanStatusCode.ERROR, message }); + } else { + span.setStatus({ code: SpanStatusCode.OK, message }); + } span.setAttribute("unsuccessfulClear", true); span.setAttribute("txReverted", true); } else if (e.reason === ProcessPairHaltReason.TxMineFailed) { @@ -777,6 +783,11 @@ export async function processPair(args: { return result; } else { // keep track of gas consumption of the account + const simulation = await handleRevert(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 diff --git a/test/error.test.js b/test/error.test.js new file mode 100644 index 00000000..633c0c34 --- /dev/null +++ b/test/error.test.js @@ -0,0 +1,34 @@ +const { assert } = require("chai"); +const { BaseError } = require("viem"); +const { tryDecodeError, parseRevertError } = require("../src/error"); + +describe("Test error", async function () { + const data = "0x963b34a500000000000000000000000000000000000000000000000340bda9d7e155feb0"; + + it("should decode the error data", async function () { + const result = tryDecodeError(data); + const expected = { + name: "MinimalOutputBalanceViolation", + args: ["60005303754817928880"], + }; + assert.deepEqual(result, expected); + }); + + it("should parse viem revert error", async function () { + const rawError = { + code: -3, + message: "some msg", + data, + }; + const error = new BaseError("some msg", { cause: rawError }); + const result = parseRevertError(error); + const expected = { + raw: rawError, + decoded: { + name: "MinimalOutputBalanceViolation", + args: ["60005303754817928880"], + }, + }; + assert.deepEqual(result, expected); + }); +}); diff --git a/test/processPair.test.js b/test/processPair.test.js index a65de918..9a22c100 100644 --- a/test/processPair.test.js +++ b/test/processPair.test.js @@ -583,6 +583,8 @@ describe("Test process pair", async function () { }; signer.sendTransaction = async () => txHash; viemClient.waitForTransactionReceipt = async () => errorReceipt; + viemClient.getTransaction = async () => ({}); + viemClient.call = async () => Promise.reject("out of gas"); try { await processPair({ config, @@ -610,7 +612,7 @@ describe("Test process pair", async function () { actualGasCost: formatUnits(effectiveGasPrice.mul(gasUsed)), }, reason: ProcessPairHaltReason.TxReverted, - error: undefined, + error: "out of gas", gasCost: undefined, spanAttributes: { "details.pair": pair, @@ -630,6 +632,7 @@ describe("Test process pair", async function () { "details.marketQuote.num": 0.99699, "details.marketQuote.str": "0.99699", "details.clearModePick": "rp4", + txNoneNodeError: true, "details.quote": JSON.stringify({ maxOutput: formatUnits(vaultBalance), ratio: formatUnits(ethers.constants.Zero),