Skip to content

Commit

Permalink
Merge pull request #261 from rainlanguage/2024-11-19-revert-simulation
Browse files Browse the repository at this point in the history
revert simulation
  • Loading branch information
rouzwelt authored Nov 20, 2024
2 parents d54a7e3 + 1ec3e1b commit 4231781
Show file tree
Hide file tree
Showing 8 changed files with 239 additions and 8 deletions.
157 changes: 156 additions & 1 deletion src/error.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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
*/
Expand All @@ -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);
Expand Down Expand Up @@ -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;
}
}
}
}
}
9 changes: 9 additions & 0 deletions src/modes/interOrderbook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
9 changes: 9 additions & 0 deletions src/modes/intraOrderbook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
9 changes: 9 additions & 0 deletions src/modes/routeProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
23 changes: 17 additions & 6 deletions src/processOrders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions test/abis/RouteProcessor4.json
Original file line number Diff line number Diff line change
@@ -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"}]}
34 changes: 34 additions & 0 deletions test/error.test.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
5 changes: 4 additions & 1 deletion test/processPair.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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),
Expand Down

0 comments on commit 4231781

Please sign in to comment.