From 40965cbf9cfd53a9e66b9c46bff44200a463fec7 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Tue, 3 Dec 2024 20:40:01 +0000 Subject: [PATCH 1/4] init --- src/error.ts | 73 ++++++++-- src/processOrders.ts | 295 +++++++++++++++++++++++++-------------- test/processPair.test.js | 7 +- 3 files changed, 254 insertions(+), 121 deletions(-) diff --git a/src/error.ts b/src/error.ts index 40d02208..81a1b5c4 100644 --- a/src/error.ts +++ b/src/error.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ -import { ViemClient } from "./types"; +import { BigNumber } from "ethers"; +import { RawTx, ViemClient } from "./types"; // @ts-ignore import { abi as obAbi } from "../test/abis/OrderBook.json"; // @ts-ignore @@ -15,6 +16,7 @@ import { RpcRequestError, FeeCapTooLowError, decodeErrorResult, + TransactionReceipt, ExecutionRevertedError, InsufficientFundsError, TransactionNotFoundError, @@ -61,22 +63,39 @@ export type TxRevertError = { /** * Get error with snapshot */ -export function errorSnapshot(header: string, err: any): string { +export function errorSnapshot( + header: string, + err: any, + data?: { + receipt: TransactionReceipt; + rawtx: RawTx; + signerBalance: BigNumber; + }, +): string { const message = [header]; 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 (message.some((v) => v.includes("unknown reason"))) { + if ( + message.some( + (v) => v.includes("unknown reason") || v.includes("execution reverted"), + ) + ) { 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 (raw.data) { + message.push("Error Raw Data: " + raw.data); + } else if (data) { + const gasErr = checkGasIssue(data.receipt, data.rawtx, data.signerBalance); + if (gasErr) { + message.push("Gas Error: " + gasErr); + } } } } @@ -143,8 +162,16 @@ export function isTimeout(err: BaseError): boolean { export async function handleRevert( viemClient: ViemClient, hash: `0x${string}`, -): Promise<{ err: any; nodeError: boolean } | undefined> { + receipt: TransactionReceipt, + rawtx: RawTx, + signerBalance: BigNumber, +): Promise<{ err: any; nodeError: boolean; snapshot: string }> { + const header = "transaction reverted onchain"; try { + const gasErr = checkGasIssue(receipt, rawtx, signerBalance); + if (gasErr) { + return { err: header + gasErr, nodeError: false, snapshot: header + gasErr }; + } const tx = await viemClient.getTransaction({ hash }); await viemClient.call({ account: tx.from, @@ -154,13 +181,18 @@ export async function handleRevert( 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 }; + return { + err: "transaction reverted onchain, but simulation indicates that it should have been successful", + nodeError: false, + snapshot: + "transaction reverted onchain, but simulation indicates that it should have been successful", + }; + } catch (err: any) { + return { + err, + nodeError: containsNodeError(err), + snapshot: errorSnapshot(header, err, { receipt, rawtx, signerBalance }), + }; } } @@ -237,3 +269,18 @@ export function tryDecodeError(data: `0x${string}`): DecodedError | undefined { } } } + +/** + * Check if a mined transaction contains gas issue or not + */ +export function checkGasIssue(receipt: TransactionReceipt, rawtx: RawTx, signerBalance: BigNumber) { + const txGasCost = receipt.effectiveGasPrice * receipt.gasUsed; + if (signerBalance.lt(txGasCost)) { + return "account ran out of gas for transaction gas cost"; + } + if (typeof rawtx.gas === "bigint") { + const percentage = (receipt.gasUsed * 100n) / rawtx.gas; + if (percentage >= 98n) return "transaction ran out of specified gas"; + } + return undefined; +} diff --git a/src/processOrders.ts b/src/processOrders.ts index 33849abc..7c861b57 100644 --- a/src/processOrders.ts +++ b/src/processOrders.ts @@ -2,15 +2,16 @@ import { ChainId } from "sushi"; import { findOpp } from "./modes"; import { Token } from "sushi/currency"; import { createViemClient } from "./config"; -import { BaseError, PublicClient } from "viem"; import { arbAbis, orderbookAbi } from "./abis"; import { privateKeyToAccount } from "viem/accounts"; import { BigNumber, Contract, ethers } from "ethers"; import { Tracer } from "@opentelemetry/sdk-trace-base"; import { Context, SpanStatusCode } from "@opentelemetry/api"; +import { BaseError, PublicClient, TransactionReceipt } from "viem"; import { addWatchedToken, fundOwnedOrders, getNonce, rotateAccounts } from "./account"; import { containsNodeError, ErrorSeverity, errorSnapshot, handleRevert, isTimeout } from "./error"; import { + RawTx, Report, BotConfig, SpanAttrs, @@ -327,9 +328,16 @@ 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"; + let message = ""; if (e.error) { - message = errorSnapshot(message, e.error); + if ("snapshot" in e.error) { + message = e.error.snapshot; + } else { + message = errorSnapshot( + "transaction reverted onchain", + e.error.err, + ); + } span.setAttribute("errorDetails", message); } if (e.spanAttributes["txNoneNodeError"]) { @@ -666,8 +674,14 @@ export async function processPair(args: { } txhash = writeSigner !== undefined - ? await writeSigner.sendTransaction(rawtx) - : await signer.sendTransaction(rawtx); + ? await writeSigner.sendTransaction({ + ...rawtx, + type: "legacy", + }) + : await signer.sendTransaction({ + ...rawtx, + type: "legacy", + }); txUrl = config.chain.blockExplorers?.default.url + "/tx/" + txhash; // eslint-disable-next-line no-console @@ -693,111 +707,50 @@ export async function processPair(args: { const receipt = await viemClient.waitForTransactionReceipt({ hash: txhash, confirmations: 1, - timeout: 200_000, + timeout: 120_000, }); - - const actualGasCost = ethers.BigNumber.from(receipt.effectiveGasPrice).mul(receipt.gasUsed); - signer.BALANCE = signer.BALANCE.sub(actualGasCost); - if (receipt.status === "success") { - spanAttributes["didClear"] = true; - - const clearActualAmount = getActualClearAmount(rawtx.to, orderbook.address, receipt); - - const inputTokenIncome = getIncome( - signer.account.address, - receipt, - orderPairObject.buyToken, - ); - const outputTokenIncome = getIncome( - signer.account.address, - receipt, - orderPairObject.sellToken, - ); - const income = getTotalIncome( - inputTokenIncome, - outputTokenIncome, - inputToEthPrice, - outputToEthPrice, - orderPairObject.buyTokenDecimals, - orderPairObject.sellTokenDecimals, - ); - const netProfit = income ? income.sub(actualGasCost) : undefined; - - if (income) { - spanAttributes["details.income"] = toNumber(income); - spanAttributes["details.netProfit"] = toNumber(netProfit!); - spanAttributes["details.actualGasCost"] = toNumber(actualGasCost); - } - if (inputTokenIncome) { - spanAttributes["details.inputTokenIncome"] = ethers.utils.formatUnits( - inputTokenIncome, - orderPairObject.buyTokenDecimals, - ); - } - if (outputTokenIncome) { - spanAttributes["details.outputTokenIncome"] = ethers.utils.formatUnits( - outputTokenIncome, - orderPairObject.sellTokenDecimals, + return handleReceipt( + txhash, + receipt, + signer, + viemClient as any as ViemClient, + spanAttributes, + rawtx, + orderbook, + orderPairObject, + inputToEthPrice, + outputToEthPrice, + result, + txUrl, + pair, + toToken, + fromToken, + config, + ); + } catch (e: any) { + try { + const newReceipt = await viemClient.getTransactionReceipt({ hash: txhash }); + if (newReceipt) { + return handleReceipt( + txhash, + newReceipt, + signer, + viemClient as any as ViemClient, + spanAttributes, + rawtx, + orderbook, + orderPairObject, + inputToEthPrice, + outputToEthPrice, + result, + txUrl, + pair, + toToken, + fromToken, + config, ); } - - result.report = { - status: ProcessPairReportStatus.FoundOpportunity, - txUrl, - tokenPair: pair, - buyToken: orderPairObject.buyToken, - sellToken: orderPairObject.sellToken, - clearedAmount: clearActualAmount?.toString(), - actualGasCost: ethers.utils.formatUnits(actualGasCost), - income, - inputTokenIncome: inputTokenIncome - ? ethers.utils.formatUnits(inputTokenIncome, toToken.decimals) - : undefined, - outputTokenIncome: outputTokenIncome - ? ethers.utils.formatUnits(outputTokenIncome, fromToken.decimals) - : undefined, - netProfit, - clearedOrders: orderPairObject.takeOrders.map((v) => v.id), - }; - - // keep track of gas consumption of the account and bounty token - result.gasCost = actualGasCost; - if (inputTokenIncome && inputTokenIncome.gt(0)) { - const tkn = { - address: orderPairObject.buyToken.toLowerCase(), - decimals: orderPairObject.buyTokenDecimals, - symbol: orderPairObject.buyTokenSymbol, - }; - addWatchedToken(tkn, config.watchedTokens ?? [], signer); - } - if (outputTokenIncome && outputTokenIncome.gt(0)) { - const tkn = { - address: orderPairObject.sellToken.toLowerCase(), - decimals: orderPairObject.sellTokenDecimals, - symbol: orderPairObject.sellTokenSymbol, - }; - addWatchedToken(tkn, config.watchedTokens ?? [], signer); - } - 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, - tokenPair: pair, - buyToken: orderPairObject.buyToken, - sellToken: orderPairObject.sellToken, - actualGasCost: ethers.utils.formatUnits(actualGasCost), - }; - result.reason = ProcessPairHaltReason.TxReverted; - return Promise.reject(result); - } - } catch (e: any) { + } catch {} // keep track of gas consumption of the account let actualGasCost; try { @@ -831,3 +784,131 @@ export async function processPair(args: { throw result; } } + +/** + * Handles the tx receipt + */ +export async function handleReceipt( + txhash: string, + receipt: TransactionReceipt, + signer: ViemClient, + viemClient: ViemClient, + spanAttributes: any, + rawtx: RawTx, + orderbook: Contract, + orderPairObject: BundledOrders, + inputToEthPrice: string, + outputToEthPrice: string, + result: any, + txUrl: string, + pair: string, + toToken: Token, + fromToken: Token, + config: BotConfig, +) { + const actualGasCost = ethers.BigNumber.from(receipt.effectiveGasPrice).mul(receipt.gasUsed); + const signerBalance = signer.BALANCE; + signer.BALANCE = signer.BALANCE.sub(actualGasCost); + if (receipt.status === "success") { + spanAttributes["didClear"] = true; + + const clearActualAmount = getActualClearAmount(rawtx.to, orderbook.address, receipt); + + const inputTokenIncome = getIncome( + signer.account.address, + receipt, + orderPairObject.buyToken, + ); + const outputTokenIncome = getIncome( + signer.account.address, + receipt, + orderPairObject.sellToken, + ); + const income = getTotalIncome( + inputTokenIncome, + outputTokenIncome, + inputToEthPrice, + outputToEthPrice, + orderPairObject.buyTokenDecimals, + orderPairObject.sellTokenDecimals, + ); + const netProfit = income ? income.sub(actualGasCost) : undefined; + + if (income) { + spanAttributes["details.income"] = toNumber(income); + spanAttributes["details.netProfit"] = toNumber(netProfit!); + spanAttributes["details.actualGasCost"] = toNumber(actualGasCost); + } + if (inputTokenIncome) { + spanAttributes["details.inputTokenIncome"] = ethers.utils.formatUnits( + inputTokenIncome, + orderPairObject.buyTokenDecimals, + ); + } + if (outputTokenIncome) { + spanAttributes["details.outputTokenIncome"] = ethers.utils.formatUnits( + outputTokenIncome, + orderPairObject.sellTokenDecimals, + ); + } + + result.report = { + status: ProcessPairReportStatus.FoundOpportunity, + txUrl, + tokenPair: pair, + buyToken: orderPairObject.buyToken, + sellToken: orderPairObject.sellToken, + clearedAmount: clearActualAmount?.toString(), + actualGasCost: ethers.utils.formatUnits(actualGasCost), + income, + inputTokenIncome: inputTokenIncome + ? ethers.utils.formatUnits(inputTokenIncome, toToken.decimals) + : undefined, + outputTokenIncome: outputTokenIncome + ? ethers.utils.formatUnits(outputTokenIncome, fromToken.decimals) + : undefined, + netProfit, + clearedOrders: orderPairObject.takeOrders.map((v) => v.id), + }; + + // keep track of gas consumption of the account and bounty token + result.gasCost = actualGasCost; + if (inputTokenIncome && inputTokenIncome.gt(0)) { + const tkn = { + address: orderPairObject.buyToken.toLowerCase(), + decimals: orderPairObject.buyTokenDecimals, + symbol: orderPairObject.buyTokenSymbol, + }; + addWatchedToken(tkn, config.watchedTokens ?? [], signer); + } + if (outputTokenIncome && outputTokenIncome.gt(0)) { + const tkn = { + address: orderPairObject.sellToken.toLowerCase(), + decimals: orderPairObject.sellTokenDecimals, + symbol: orderPairObject.sellTokenSymbol, + }; + addWatchedToken(tkn, config.watchedTokens ?? [], signer); + } + return result; + } else { + const simulation = await handleRevert( + viemClient as any, + txhash as `0x${string}`, + receipt, + rawtx, + signerBalance, + ); + result.error = simulation; + spanAttributes["txNoneNodeError"] = !simulation.nodeError; + result.report = { + status: ProcessPairReportStatus.FoundOpportunity, + txUrl, + tokenPair: pair, + buyToken: orderPairObject.buyToken, + sellToken: orderPairObject.sellToken, + actualGasCost: ethers.utils.formatUnits(actualGasCost), + }; + result.reason = ProcessPairHaltReason.TxReverted; + return Promise.reject(result); + } +} diff --git a/test/processPair.test.js b/test/processPair.test.js index 9a22c100..aaf74cce 100644 --- a/test/processPair.test.js +++ b/test/processPair.test.js @@ -612,7 +612,12 @@ describe("Test process pair", async function () { actualGasCost: formatUnits(effectiveGasPrice.mul(gasUsed)), }, reason: ProcessPairHaltReason.TxReverted, - error: "out of gas", + error: { + err: "transaction reverted onchainaccount ran out of gas for transaction gas cost", + nodeError: false, + snapshot: + "transaction reverted onchainaccount ran out of gas for transaction gas cost", + }, gasCost: undefined, spanAttributes: { "details.pair": pair, From d6f869fca7047d6be98eb7f14eae83e5aad0d8e7 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Tue, 3 Dec 2024 21:33:54 +0000 Subject: [PATCH 2/4] update --- src/error.ts | 15 ++++++++------- src/processOrders.ts | 6 ++++-- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/error.ts b/src/error.ts index 81a1b5c4..b567b1a6 100644 --- a/src/error.ts +++ b/src/error.ts @@ -165,7 +165,7 @@ export async function handleRevert( receipt: TransactionReceipt, rawtx: RawTx, signerBalance: BigNumber, -): Promise<{ err: any; nodeError: boolean; snapshot: string }> { +): Promise<{ err: any; nodeError: boolean; snapshot: string } | undefined> { const header = "transaction reverted onchain"; try { const gasErr = checkGasIssue(receipt, rawtx, signerBalance); @@ -181,12 +181,13 @@ export async function handleRevert( gasPrice: tx.gasPrice, blockNumber: tx.blockNumber, }); - return { - err: "transaction reverted onchain, but simulation indicates that it should have been successful", - nodeError: false, - snapshot: - "transaction reverted onchain, but simulation indicates that it should have been successful", - }; + // return { + // err: "transaction reverted onchain, but simulation indicates that it should have been successful", + // nodeError: false, + // snapshot: + // "transaction reverted onchain, but simulation indicates that it should have been successful", + // }; + return undefined; } catch (err: any) { return { err, diff --git a/src/processOrders.ts b/src/processOrders.ts index 7c861b57..87bf906d 100644 --- a/src/processOrders.ts +++ b/src/processOrders.ts @@ -898,8 +898,10 @@ export async function handleReceipt( rawtx, signerBalance, ); - result.error = simulation; - spanAttributes["txNoneNodeError"] = !simulation.nodeError; + if (simulation) { + result.error = simulation; + spanAttributes["txNoneNodeError"] = !simulation.nodeError; + } result.report = { status: ProcessPairReportStatus.FoundOpportunity, txUrl, From fae15fac73bcc37bda6ab077c23bd96f8046c8c2 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Wed, 4 Dec 2024 01:27:21 +0000 Subject: [PATCH 3/4] update --- src/error.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/error.ts b/src/error.ts index b567b1a6..712dee45 100644 --- a/src/error.ts +++ b/src/error.ts @@ -181,12 +181,12 @@ export async function handleRevert( gasPrice: tx.gasPrice, blockNumber: tx.blockNumber, }); - // return { - // err: "transaction reverted onchain, but simulation indicates that it should have been successful", - // nodeError: false, - // snapshot: - // "transaction reverted onchain, but simulation indicates that it should have been successful", - // }; + return { + err: "transaction reverted onchain and simulation failed to find out what was the revert reason, please try to simulate the tx manualy for more details", + nodeError: false, + snapshot: + "transaction reverted onchain and simulation failed to find out what was the revert reason, please try to simulate the tx manualy for more details", + }; return undefined; } catch (err: any) { return { From abc12d08aa3dd7232c69db36deb528406040c1fb Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Wed, 4 Dec 2024 03:03:02 +0000 Subject: [PATCH 4/4] fix clear2 bounty task scale bug --- src/modes/intraOrderbook.ts | 4 ++-- test/findOpp.test.js | 2 +- test/mode-intraOrderbook.test.js | 5 +++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/modes/intraOrderbook.ts b/src/modes/intraOrderbook.ts index 9c262dbe..9f69724a 100644 --- a/src/modes/intraOrderbook.ts +++ b/src/modes/intraOrderbook.ts @@ -299,7 +299,7 @@ export async function findOpp({ ]) as `0x${string}`, }) ).data, - ); + ).mul("1" + "0".repeat(18 - orderPairObject.buyTokenDecimals)); const outputBalance = ethers.BigNumber.from( ( await viemClient.call({ @@ -309,7 +309,7 @@ export async function findOpp({ ]) as `0x${string}`, }) ).data, - ); + ).mul("1" + "0".repeat(18 - orderPairObject.sellTokenDecimals)); for (let i = 0; i < opposingOrders.length; i++) { try { return await dryrun({ diff --git a/test/findOpp.test.js b/test/findOpp.test.js index 8d074993..45100f4d 100644 --- a/test/findOpp.test.js +++ b/test/findOpp.test.js @@ -229,7 +229,7 @@ describe("Test find opp", async function () { }; const orderbooksOrdersTemp = clone(orderbooksOrders); orderbooksOrdersTemp[0][0].orderbook = orderPairObject.orderbook; - const inputBalance = ethers.BigNumber.from("1000000000000000000"); + const inputBalance = ethers.BigNumber.from("1000000000000000000000000000000"); const outputBalance = ethers.BigNumber.from("1000000000000000000"); const result = await findOpp({ orderPairObject, diff --git a/test/mode-intraOrderbook.test.js b/test/mode-intraOrderbook.test.js index 0718e23d..2eb00dbe 100644 --- a/test/mode-intraOrderbook.test.js +++ b/test/mode-intraOrderbook.test.js @@ -171,6 +171,7 @@ describe("Test intra-orderbook find opp", async function () { }; }); const balance = ethers.BigNumber.from("1000000000000000000"); + const balance2 = ethers.BigNumber.from("1000000000000000000000000000000"); it("should find opp", async function () { const result = await findOpp({ @@ -191,7 +192,7 @@ describe("Test intra-orderbook find opp", async function () { signer.account.address, orderPairObject.buyToken, orderPairObject.sellToken, - balance, + balance2, balance, ethers.utils.parseUnits(inputToEthPrice), ethers.utils.parseUnits(outputToEthPrice), @@ -293,7 +294,7 @@ describe("Test intra-orderbook find opp", async function () { signer.account.address, orderPairObject.buyToken, orderPairObject.sellToken, - balance, + balance2, balance, ethers.utils.parseUnits(inputToEthPrice), ethers.utils.parseUnits(outputToEthPrice),