From bddf02ed12dbbdd24547a51614e9f70c907bf123 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Sat, 16 Nov 2024 23:24:10 +0000 Subject: [PATCH 01/18] init --- README.md | 4 ++ example.env | 3 + src/cli.ts | 66 +++++++++++++++--- src/index.ts | 1 + src/modes/index.ts | 6 ++ src/modes/interOrderbook.ts | 20 +++++- src/modes/intraOrderbook.ts | 57 ++++++++++------ src/modes/routeProcessor.ts | 20 +++++- src/processOrders.ts | 56 +++++++++++++--- src/types.ts | 6 ++ test/cli.test.js | 8 ++- test/data.js | 1 + test/e2e/e2e.test.js | 9 ++- test/findOpp.test.js | 3 + test/mode-interOrderbook.test.js | 6 +- test/mode-intraOrderbook.test.js | 22 +++++- test/mode-routeProcessor.test.js | 18 ++++- test/processPair.test.js | 112 ++++++++++++++++++++++++++++++- 18 files changed, 363 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index 974d6cb5..1f7e163e 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,7 @@ Other optional arguments are: - `-w` or `--wallet-count`, Number of wallet to submit transactions with, requirs `--mnemonic`. Will override the 'WALLET_COUNT' in env variables - `-t` or `--topup-amount`, The initial topup amount of excess wallets, requirs `--mnemonic`. Will override the 'TOPUP_AMOUNT' in env variables - `--gas-price-multiplier`, Option to multiply the gas price fetched from the rpc as percentage, default is 107, ie +7%. Will override the 'GAS_PRICE_MULTIPLIER' in env variables +- `--gas-limit-multiplier`, Option to multiply the gas limit estimation from the rpc as percentage, default is 105, ie +5%. Will override the 'GAS_LIMIT_MULTIPLIER' in env variables - `-V` or `--version`, output the version number - `-h` or `--help`, output usage information @@ -256,6 +257,9 @@ ROUTE="single" # Option to multiply the gas price fetched from the rpc as percentage, default is 107, ie +7% GAS_PRICE_MULTIPLIER= + +# Option to multiply the gas limit estimation from the rpc as percentage, default is 105, ie +5% +GAS_LIMIT_MULTIPLIER= ``` If both env variables and CLI argument are set, the CLI arguments will be prioritized and override the env variables. diff --git a/example.env b/example.env index f4f0a84c..94fc0c01 100644 --- a/example.env +++ b/example.env @@ -85,6 +85,9 @@ ROUTE="single" # Option to multiply the gas price fetched from the rpc as percentage, default is 107, ie +7% GAS_PRICE_MULTIPLIER= +# Option to multiply the gas limit estimation from the rpc as percentage, default is 105, ie +5% +GAS_LIMIT_MULTIPLIER= + # test rpcs vars TEST_POLYGON_RPC= diff --git a/src/cli.ts b/src/cli.ts index 6bd1810b..a33e3163 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -57,6 +57,8 @@ const ENV_OPTIONS = { botMinBalance: process?.env?.BOT_MIN_BALANCE, selfFundOrders: process?.env?.SELF_FUND_ORDERS, gasPriceMultiplier: process?.env?.GAS_PRICE_MULTIPLIER, + gasLimitMultiplier: process?.env?.GAS_LIMIT_MULTIPLIER, + txGas: process?.env?.TX_GAS, route: process?.env?.ROUTE, rpc: process?.env?.RPC_URL ? Array.from(process?.env?.RPC_URL.matchAll(/[^,\s]+/g)).map((v) => v[0]) @@ -168,6 +170,14 @@ const getOptions = async (argv: any, version?: string) => { "--gas-price-multiplier ", "Option to multiply the gas price fetched from the rpc as percentage, default is 107, ie +7%. Will override the 'GAS_PRICE_MULTIPLIER' in env variables", ) + .option( + "--gas-limit-multiplier ", + "Option to multiply the gas limit estimation from the rpc as percentage, default is 105, ie +5%. Will override the 'GAS_LIMIT_MULTIPLIER' in env variables", + ) + .option( + "--tx-gas ", + "Option to set a static gas limit for all submitting txs. Will override the 'TX_GAS' in env variables", + ) .description( [ "A NodeJS app to find and take arbitrage trades for Rain Orderbook orders against some DeFi liquidity providers, requires NodeJS v18 or higher.", @@ -203,6 +213,8 @@ const getOptions = async (argv: any, version?: string) => { cmdOptions.topupAmount = cmdOptions.topupAmount || ENV_OPTIONS.topupAmount; cmdOptions.selfFundOrders = cmdOptions.selfFundOrders || ENV_OPTIONS.selfFundOrders; cmdOptions.gasPriceMultiplier = cmdOptions.gasPriceMultiplier || ENV_OPTIONS.gasPriceMultiplier; + cmdOptions.gasLimitMultiplier = cmdOptions.gasLimitMultiplier || ENV_OPTIONS.gasLimitMultiplier; + cmdOptions.txGas = cmdOptions.txGas || ENV_OPTIONS.txGas; cmdOptions.botMinBalance = cmdOptions.botMinBalance || ENV_OPTIONS.botMinBalance; cmdOptions.route = cmdOptions.route || ENV_OPTIONS.route; cmdOptions.bundle = cmdOptions.bundle ? ENV_OPTIONS.bundle : false; @@ -251,7 +263,7 @@ export const arbRound = async ( if (!ordersDetails.length) { span.setStatus({ code: SpanStatusCode.OK, message: "found no orders" }); span.end(); - return { txs: [], foundOpp: false, avgGasCost: undefined }; + return { txs: [], foundOpp: false, didClear: false, avgGasCost: undefined }; } } catch (e: any) { const snapshot = errorSnapshot("", e); @@ -260,12 +272,13 @@ export const arbRound = async ( span.setAttribute("didClear", false); span.setAttribute("foundOpp", false); span.end(); - return { txs: [], foundOpp: false, avgGasCost: undefined }; + return { txs: [], foundOpp: false, didClear: false, avgGasCost: undefined }; } try { let txs; let foundOpp = false; + let didClear = false; const { reports = [], avgGasCost = undefined } = await clear( config, ordersDetails, @@ -277,7 +290,6 @@ export const arbRound = async ( if (txs.length) { foundOpp = true; span.setAttribute("txUrls", txs); - span.setAttribute("didClear", true); span.setAttribute("foundOpp", true); } else if ( reports.some((v) => v.status === ProcessPairReportStatus.FoundOpportunity) @@ -285,6 +297,15 @@ export const arbRound = async ( foundOpp = true; span.setAttribute("foundOpp", true); } + if ( + reports.some( + (v) => + v.status === ProcessPairReportStatus.FoundOpportunity && !v.reason, + ) + ) { + didClear = true; + span.setAttribute("didClear", true); + } } else { span.setAttribute("didClear", false); } @@ -293,7 +314,7 @@ export const arbRound = async ( } span.setStatus({ code: SpanStatusCode.OK }); span.end(); - return { txs, foundOpp, avgGasCost }; + return { txs, foundOpp, didClear, avgGasCost }; } catch (e: any) { if (e?.startsWith?.("Failed to batch quote orders")) { span.setAttribute("severity", ErrorSeverity.LOW); @@ -307,7 +328,7 @@ export const arbRound = async ( span.setAttribute("didClear", false); span.setAttribute("foundOpp", false); span.end(); - return { txs: [], foundOpp: false, avgGasCost: undefined }; + return { txs: [], foundOpp: false, didClear: false, avgGasCost: undefined }; } } catch (e: any) { const snapshot = errorSnapshot("Unexpected error occured", e); @@ -317,7 +338,7 @@ export const arbRound = async ( span.setAttribute("didClear", false); span.setAttribute("foundOpp", false); span.end(); - return { txs: [], foundOpp: false, avgGasCost: undefined }; + return { txs: [], foundOpp: false, didClear: false, avgGasCost: undefined }; } }); }; @@ -389,6 +410,32 @@ export async function startup(argv: any, version?: string, tracer?: Tracer, ctx? } else { options.gasPriceMultiplier = 107; } + if (options.gasLimitMultiplier) { + if (typeof options.gasLimitMultiplier === "number") { + if (options.gasLimitMultiplier <= 0 || !Number.isInteger(options.gasLimitMultiplier)) + throw "invalid gasLimitMultiplier value, must be an integer greater than zero"; + } else if ( + typeof options.gasLimitMultiplier === "string" && + /^[0-9]+$/.test(options.gasLimitMultiplier) + ) { + options.gasLimitMultiplier = Number(options.gasLimitMultiplier); + if (options.gasLimitMultiplier <= 0) + throw "invalid gasLimitMultiplier value, must be an integer greater than zero"; + } else throw "invalid gasLimitMultiplier value, must be an integer greater than zero"; + } else { + options.gasLimitMultiplier = 105; + } + if (options.txGas) { + if (typeof options.txGas === "number") { + if (options.txGas <= 0 || !Number.isInteger(options.txGas)) + throw "invalid txGas value, must be an integer greater than zero"; + else options.txGas = BigInt(options.txGas); + } else if (typeof options.txGas === "string" && /^[0-9]+$/.test(options.txGas)) { + options.txGas = BigInt(options.txGas); + if (options.txGas <= 0n) + throw "invalid txGas value, must be an integer greater than zero"; + } else throw "invalid txGas value, must be an integer greater than zero"; + } const poolUpdateInterval = _poolUpdateInterval * 60 * 1000; let ordersDetails: any[] = []; if (!process?.env?.TEST) @@ -559,16 +606,19 @@ export const main = async (argv: any, version?: string) => { try { await rotateProviders(config, update); const roundResult = await arbRound(tracer, roundCtx, options, config); - let txs, foundOpp, roundAvgGasCost; + let txs, foundOpp, didClear, roundAvgGasCost; if (roundResult) { txs = roundResult.txs; foundOpp = roundResult.foundOpp; + didClear = roundResult.didClear; roundAvgGasCost = roundResult.avgGasCost; } if (txs && txs.length) { roundSpan.setAttribute("txUrls", txs); - roundSpan.setAttribute("didClear", true); roundSpan.setAttribute("foundOpp", true); + } else if (didClear) { + roundSpan.setAttribute("foundOpp", true); + roundSpan.setAttribute("didClear", true); } else if (foundOpp) { roundSpan.setAttribute("foundOpp", true); roundSpan.setAttribute("didClear", false); diff --git a/src/index.ts b/src/index.ts index a189aa02..2e08ca27 100644 --- a/src/index.ts +++ b/src/index.ts @@ -211,6 +211,7 @@ export async function getConfig( config.route = route; config.rpcRecords = rpcRecords; config.gasPriceMultiplier = options.gasPriceMultiplier; + config.gasLimitMultiplier = options.gasLimitMultiplier; // init accounts const { mainAccount, accounts } = await initAccounts(walletKey, config, options, tracer, ctx); diff --git a/src/modes/index.ts b/src/modes/index.ts index a1c61b95..3d066895 100644 --- a/src/modes/index.ts +++ b/src/modes/index.ts @@ -82,6 +82,12 @@ export async function findOpp({ if (allResults.some((v) => v.status === "fulfilled")) { // pick and return the highest profit + allResults.forEach((v, i) => { + if (v.status === "fulfilled") { + v.value.spanAttributes["clearModePick"] = + i === 0 ? "rp4" : i === 1 ? "intra" : "inter"; + } + }); const res = allResults.filter( (v) => v.status === "fulfilled", ) as PromiseFulfilledResult[]; diff --git a/src/modes/interOrderbook.ts b/src/modes/interOrderbook.ts index 602567da..3293ebbb 100644 --- a/src/modes/interOrderbook.ts +++ b/src/modes/interOrderbook.ts @@ -82,7 +82,14 @@ export async function dryrun({ evaluable: { interpreter: orderPairObject.takeOrders[0].takeOrder.order.evaluable.interpreter, store: orderPairObject.takeOrders[0].takeOrder.order.evaluable.store, - bytecode: "0x", + bytecode: + config.gasCoveragePercentage === "0" + ? "0x" + : getBountyEnsureBytecode( + ethers.utils.parseUnits(inputToEthPrice), + ethers.utils.parseUnits(outputToEthPrice), + ethers.constants.Zero, + ), }, signedContext: [], }; @@ -102,7 +109,9 @@ export async function dryrun({ try { blockNumber = Number(await viemClient.getBlockNumber()); spanAttributes["blockNumber"] = blockNumber; - gasLimit = ethers.BigNumber.from(await signer.estimateGas(rawtx)); + gasLimit = ethers.BigNumber.from(await signer.estimateGas(rawtx)) + .mul(config.gasLimitMultiplier) + .div(100); } catch (e) { const isNodeError = containsNodeError(e as BaseError); const errMsg = errorSnapshot("", e); @@ -144,7 +153,9 @@ export async function dryrun({ try { blockNumber = Number(await viemClient.getBlockNumber()); spanAttributes["blockNumber"] = blockNumber; - gasLimit = ethers.BigNumber.from(await signer.estimateGas(rawtx)); + gasLimit = ethers.BigNumber.from(await signer.estimateGas(rawtx)) + .mul(config.gasLimitMultiplier) + .div(100); rawtx.gas = gasLimit.toBigInt(); gasCost = gasLimit.mul(gasPrice); task.evaluable.bytecode = getBountyEnsureBytecode( @@ -179,6 +190,9 @@ export async function dryrun({ } } rawtx.gas = gasLimit.toBigInt(); + if (typeof config.txGas === "bigint") { + rawtx.gas = config.txGas; + } // if reached here, it means there was a success and found opp spanAttributes["oppBlockNumber"] = blockNumber; diff --git a/src/modes/intraOrderbook.ts b/src/modes/intraOrderbook.ts index d70a7349..498e4880 100644 --- a/src/modes/intraOrderbook.ts +++ b/src/modes/intraOrderbook.ts @@ -49,6 +49,23 @@ export async function dryrun({ const inputBountyVaultId = "1"; const outputBountyVaultId = "1"; const obInterface = new ethers.utils.Interface(orderbookAbi); + const task = { + evaluable: { + interpreter: orderPairObject.takeOrders[0].takeOrder.order.evaluable.interpreter, + store: orderPairObject.takeOrders[0].takeOrder.order.evaluable.store, + bytecode: getWithdrawEnsureBytecode( + signer.account.address, + orderPairObject.buyToken, + orderPairObject.sellToken, + inputBalance, + outputBalance, + ethers.utils.parseUnits(inputToEthPrice), + ethers.utils.parseUnits(outputToEthPrice), + ethers.constants.Zero, + ), + }, + signedContext: [], + }; const withdrawInputCalldata = obInterface.encodeFunctionData("withdraw2", [ orderPairObject.buyToken, inputBountyVaultId, @@ -59,7 +76,7 @@ export async function dryrun({ orderPairObject.sellToken, outputBountyVaultId, ethers.constants.MaxUint256, - [], + config.gasCoveragePercentage === "0" ? [] : [task], ]); const clear2Calldata = obInterface.encodeFunctionData("clear2", [ orderPairObject.takeOrders[0].takeOrder.order, @@ -89,7 +106,9 @@ export async function dryrun({ try { blockNumber = Number(await viemClient.getBlockNumber()); spanAttributes["blockNumber"] = blockNumber; - gasLimit = ethers.BigNumber.from(await signer.estimateGas(rawtx)); + gasLimit = ethers.BigNumber.from(await signer.estimateGas(rawtx)) + .mul(config.gasLimitMultiplier) + .div(100); } catch (e) { // reason, code, method, transaction, error, stack, message const isNodeError = containsNodeError(e as BaseError); @@ -118,23 +137,16 @@ export async function dryrun({ // sender output which is already called above if (config.gasCoveragePercentage !== "0") { const headroom = (Number(config.gasCoveragePercentage) * 1.03).toFixed(); - const task = { - evaluable: { - interpreter: orderPairObject.takeOrders[0].takeOrder.order.evaluable.interpreter, - store: orderPairObject.takeOrders[0].takeOrder.order.evaluable.store, - bytecode: getWithdrawEnsureBytecode( - signer.account.address, - orderPairObject.buyToken, - orderPairObject.sellToken, - inputBalance, - outputBalance, - ethers.utils.parseUnits(inputToEthPrice), - ethers.utils.parseUnits(outputToEthPrice), - gasCost.mul(headroom).div("100"), - ), - }, - signedContext: [], - }; + task.evaluable.bytecode = getWithdrawEnsureBytecode( + signer.account.address, + orderPairObject.buyToken, + orderPairObject.sellToken, + inputBalance, + outputBalance, + ethers.utils.parseUnits(inputToEthPrice), + ethers.utils.parseUnits(outputToEthPrice), + gasCost.mul(headroom).div("100"), + ); withdrawOutputCalldata = obInterface.encodeFunctionData("withdraw2", [ orderPairObject.sellToken, outputBountyVaultId, @@ -148,7 +160,9 @@ export async function dryrun({ try { blockNumber = Number(await viemClient.getBlockNumber()); spanAttributes["blockNumber"] = blockNumber; - gasLimit = ethers.BigNumber.from(await signer.estimateGas(rawtx)); + gasLimit = ethers.BigNumber.from(await signer.estimateGas(rawtx)) + .mul(config.gasLimitMultiplier) + .div(100); rawtx.gas = gasLimit.toBigInt(); gasCost = gasLimit.mul(gasPrice); task.evaluable.bytecode = getWithdrawEnsureBytecode( @@ -192,6 +206,9 @@ export async function dryrun({ } } rawtx.gas = gasLimit.toBigInt(); + if (typeof config.txGas === "bigint") { + rawtx.gas = config.txGas; + } // if reached here, it means there was a success and found opp spanAttributes["oppBlockNumber"] = blockNumber; diff --git a/src/modes/routeProcessor.ts b/src/modes/routeProcessor.ts index a4419f0a..29311ea8 100644 --- a/src/modes/routeProcessor.ts +++ b/src/modes/routeProcessor.ts @@ -147,7 +147,14 @@ export async function dryrun({ evaluable: { interpreter: orderPairObject.takeOrders[0].takeOrder.order.evaluable.interpreter, store: orderPairObject.takeOrders[0].takeOrder.order.evaluable.store, - bytecode: "0x", + bytecode: + config.gasCoveragePercentage === "0" + ? "0x" + : getBountyEnsureBytecode( + ethers.utils.parseUnits(ethPrice), + ethers.constants.Zero, + ethers.constants.Zero, + ), }, signedContext: [], }; @@ -167,7 +174,9 @@ export async function dryrun({ try { blockNumber = Number(await viemClient.getBlockNumber()); spanAttributes["blockNumber"] = blockNumber; - gasLimit = ethers.BigNumber.from(await signer.estimateGas(rawtx)); + gasLimit = ethers.BigNumber.from(await signer.estimateGas(rawtx)) + .mul(config.gasLimitMultiplier) + .div(100); } catch (e) { // reason, code, method, transaction, error, stack, message const isNodeError = containsNodeError(e as BaseError); @@ -211,7 +220,9 @@ export async function dryrun({ try { blockNumber = Number(await viemClient.getBlockNumber()); spanAttributes["blockNumber"] = blockNumber; - gasLimit = ethers.BigNumber.from(await signer.estimateGas(rawtx)); + gasLimit = ethers.BigNumber.from(await signer.estimateGas(rawtx)) + .mul(config.gasLimitMultiplier) + .div(100); rawtx.gas = gasLimit.toBigInt(); gasCost = gasLimit.mul(gasPrice); task.evaluable.bytecode = getBountyEnsureBytecode( @@ -247,6 +258,9 @@ export async function dryrun({ } } rawtx.gas = gasLimit.toBigInt(); + if (typeof config.txGas === "bigint") { + rawtx.gas = config.txGas; + } // if reached here, it means there was a success and found opp // rest of span attr are not needed since they are present in the result.data diff --git a/src/processOrders.ts b/src/processOrders.ts index 2b1b1544..439a3537 100644 --- a/src/processOrders.ts +++ b/src/processOrders.ts @@ -47,7 +47,8 @@ export enum ProcessPairHaltReason { FailedToGetPools = 4, TxFailed = 5, TxMineFailed = 6, - UnexpectedError = 7, + TxReverted = 7, + UnexpectedError = 8, } /** @@ -310,21 +311,49 @@ export const processOrders = async ( span.setAttribute("errorDetails", message); } span.setStatus({ code: SpanStatusCode.OK, message }); - } else { - // set the otel span status as OK as an unsuccessfull clear, this can happen for example + } else if (e.reason === ProcessPairHaltReason.TxFailed) { + // failed to submit the tx to mempool, this can happen for example when rpc rejects + // the tx for example because of low gas or invalid parameters, etc + let message = "failed to submit the transaction"; + if (e.error) { + message = errorSnapshot(message, e.error); + span.setAttribute("errorDetails", message); + } + span.setAttribute("severity", ErrorSeverity.MEDIUM); + span.setStatus({ code: SpanStatusCode.ERROR, message }); + span.setAttribute("unsuccessfulClear", true); + span.setAttribute("txSendFailed", true); + } else if (e.reason === ProcessPairHaltReason.TxReverted) { + // set the severity to LOW, this can happen for example // because of mev front running or false positive opportunities, etc - let code = SpanStatusCode.OK; + span.setAttribute("severity", ErrorSeverity.LOW); + span.setStatus({ + code: SpanStatusCode.ERROR, + message: "transaction reverted onchain", + }); + span.setAttribute("unsuccessfulClear", true); + span.setAttribute("txReverted", true); + } else if (e.reason === ProcessPairHaltReason.TxMineFailed) { + // tx failed to get included onchain, this can happen as result of timeout, rpc dropping the tx, etc let message = "transaction failed"; if (e.error) { message = errorSnapshot(message, e.error); span.setAttribute("errorDetails", message); } - if (e.spanAttributes["txNoneNodeError"]) { - code = SpanStatusCode.ERROR; - span.setAttribute("severity", ErrorSeverity.MEDIUM); + span.setAttribute("severity", ErrorSeverity.MEDIUM); + span.setStatus({ code: SpanStatusCode.ERROR, message }); + span.setAttribute("unsuccessfulClear", true); + span.setAttribute("txMineFailed", true); + } else { + // record the error for the span + let message = pair + "unexpected error"; + if (e.error) { + message = errorSnapshot(message, e.error); + span.recordException(e.error); } - span.setStatus({ code, message }); - span.setAttribute("unsuccessfullClear", true); + // set the span status to unexpected error + span.setAttribute("severity", ErrorSeverity.HIGH); + span.setStatus({ code: SpanStatusCode.ERROR, message }); } } else { // record the error for the span @@ -756,7 +785,7 @@ export async function processPair(args: { sellToken: orderPairObject.sellToken, actualGasCost: ethers.utils.formatUnits(actualGasCost), }; - result.reason = ProcessPairHaltReason.TxMineFailed; + result.reason = ProcessPairHaltReason.TxReverted; return Promise.reject(result); } } catch (e: any) { @@ -781,6 +810,13 @@ export async function processPair(args: { result.report.actualGasCost = ethers.utils.formatUnits(actualGasCost); } result.error = e; + spanAttributes["details.rawTx"] = JSON.stringify( + { + ...rawtx, + from: signer.account.address, + }, + withBigintSerializer, + ); spanAttributes["txNoneNodeError"] = !containsNodeError(e); result.reason = ProcessPairHaltReason.TxMineFailed; throw result; diff --git a/src/types.ts b/src/types.ts index f351bebf..d8c1076f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -46,6 +46,8 @@ export type CliOptions = { tokens?: TokenDetails[]; route?: string; gasPriceMultiplier: number; + gasLimitMultiplier: number; + txGas?: bigint; }; export type TokenDetails = { @@ -145,6 +147,8 @@ export type BotConfig = { route?: "multi" | "single"; rpcRecords: Record; gasPriceMultiplier: number; + gasLimitMultiplier: number; + txGas?: bigint; onFetchRequest?: (request: Request) => void; onFetchResponse?: (request: Response) => void; }; @@ -162,6 +166,8 @@ export type Report = { clearedOrders?: string[]; income?: BigNumber; netProfit?: BigNumber; + reason?: ProcessPairHaltReason; + error?: any; }; export type RoundReport = { diff --git a/test/cli.test.js b/test/cli.test.js index 2689bc78..23a927fe 100644 --- a/test/cli.test.js +++ b/test/cli.test.js @@ -48,7 +48,7 @@ describe("Test cli", async function () { }; const response = await arbRound(tracer, ctx, options, { mainAccount: {} }); - const expected = { txs: [], foundOpp: false, avgGasCost: undefined }; + const expected = { txs: [], foundOpp: false, didClear: false, avgGasCost: undefined }; assert.deepEqual(response, expected); testSpan.end(); @@ -185,6 +185,8 @@ describe("Test cli", async function () { "0.123", "--gas-price-multiplier", "120", + "--gas-limit-multiplier", + "110", ]); const expected = { roundGap: 10000, @@ -204,10 +206,12 @@ describe("Test cli", async function () { }, }, gasPriceMultiplier: 120, + gasLimitMultiplier: 110, }, options: { botMinBalance: "0.123", gasPriceMultiplier: 120, + gasLimitMultiplier: 110, }, }; await sleep(1000); @@ -221,5 +225,7 @@ describe("Test cli", async function () { assert.equal(result.options.botMinBalance, expected.options.botMinBalance); assert.equal(result.options.gasPriceMultiplier, expected.options.gasPriceMultiplier); assert.equal(result.config.gasPriceMultiplier, expected.config.gasPriceMultiplier); + assert.equal(result.options.gasLimitMultiplier, expected.options.gasLimitMultiplier); + assert.equal(result.config.gasLimitMultiplier, expected.config.gasLimitMultiplier); }); }); diff --git a/test/data.js b/test/data.js index 17e3cfeb..e5bd1ea2 100644 --- a/test/data.js +++ b/test/data.js @@ -67,6 +67,7 @@ const config = { symbol: token2.symbol, }, gasPriceMultiplier: 107, + gasLimitMultiplier: 100, }; const vaultBalance1 = BigNumber.from("10000000000000000000"); diff --git a/test/e2e/e2e.test.js b/test/e2e/e2e.test.js index 81cd52ac..336a61fb 100644 --- a/test/e2e/e2e.test.js +++ b/test/e2e/e2e.test.js @@ -74,7 +74,7 @@ for (let i = 0; i < testData.length; i++) { for (let j = 0; j < rpVersions.length; j++) { const rpVersion = rpVersions[j]; - it.only(`should clear orders successfully using route processor v${rpVersion}`, async function () { + it(`should clear orders successfully using route processor v${rpVersion}`, async function () { config.rpc = [rpc]; const viemClient = await viem.getPublicClient(); const dataFetcher = await getDataFetcher(config, liquidityProviders, false); @@ -250,6 +250,7 @@ for (let i = 0; i < testData.length; i++) { config.mainAccount = bot; config.quoteRpc = [mockServer.url + "/rpc"]; config.gasPriceMultiplier = 107; + config.gasLimitMultiplier = 100; const { reports } = await clear(config, orders, tracer, ctx); // should have cleared correct number of orders @@ -330,7 +331,7 @@ for (let i = 0; i < testData.length; i++) { testSpan.end(); }); - it.only("should clear orders successfully using inter-orderbook", async function () { + it("should clear orders successfully using inter-orderbook", async function () { config.rpc = [rpc]; const viemClient = await viem.getPublicClient(); const dataFetcher = await getDataFetcher(config, liquidityProviders, false); @@ -581,6 +582,7 @@ for (let i = 0; i < testData.length; i++) { config.mainAccount = bot; config.quoteRpc = [mockServer.url + "/rpc"]; config.gasPriceMultiplier = 107; + config.gasLimitMultiplier = 100; const { reports } = await clear(config, orders, tracer, ctx); // should have cleared correct number of orders @@ -674,7 +676,7 @@ for (let i = 0; i < testData.length; i++) { testSpan.end(); }); - it.only("should clear orders successfully using intra-orderbook", async function () { + it("should clear orders successfully using intra-orderbook", async function () { config.rpc = [rpc]; const viemClient = await viem.getPublicClient(); const dataFetcher = await getDataFetcher(config, liquidityProviders, false); @@ -933,6 +935,7 @@ for (let i = 0; i < testData.length; i++) { config.mainAccount = bot; config.quoteRpc = [mockServer.url + "/rpc"]; config.gasPriceMultiplier = 107; + config.gasLimitMultiplier = 100; const { reports } = await clear(config, orders, tracer, ctx); // should have cleared correct number of orders diff --git a/test/findOpp.test.js b/test/findOpp.test.js index efea40c7..5d15c616 100644 --- a/test/findOpp.test.js +++ b/test/findOpp.test.js @@ -124,6 +124,7 @@ describe("Test find opp", async function () { amountOut: formatUnits(getAmountOut(vaultBalance), 6), marketPrice: formatUnits(getCurrentPrice(vaultBalance)), route: expectedRouteVisual, + clearModePick: "rp4", }, }; assert.deepEqual(result, expected); @@ -214,6 +215,7 @@ describe("Test find opp", async function () { oppBlockNumber, foundOpp: true, maxInput: vaultBalance.toString(), + clearModePick: "inter", }, }; assert.deepEqual(result, expected); @@ -309,6 +311,7 @@ describe("Test find opp", async function () { spanAttributes: { oppBlockNumber, foundOpp: true, + clearModePick: "intra", }, }; assert.deepEqual(result, expected); diff --git a/test/mode-interOrderbook.test.js b/test/mode-interOrderbook.test.js index 884bd652..b7ac2a7e 100644 --- a/test/mode-interOrderbook.test.js +++ b/test/mode-interOrderbook.test.js @@ -404,7 +404,11 @@ describe("Test inter-orderbook find opp", async function () { interpreter: orderPairObject.takeOrders[0].takeOrder.order.evaluable.interpreter, store: orderPairObject.takeOrders[0].takeOrder.order.evaluable.store, - bytecode: "0x", + bytecode: getBountyEnsureBytecode( + ethers.utils.parseUnits(inputToEthPrice), + ethers.utils.parseUnits(outputToEthPrice), + ethers.constants.Zero, + ), }, signedContext: [], }; diff --git a/test/mode-intraOrderbook.test.js b/test/mode-intraOrderbook.test.js index d34f6387..a7c84110 100644 --- a/test/mode-intraOrderbook.test.js +++ b/test/mode-intraOrderbook.test.js @@ -270,6 +270,7 @@ describe("Test intra-orderbook find opp", async function () { }); assert.fail("expected to reject, but resolved"); } catch (error) { + const balance = ethers.BigNumber.from("1000000000000000000"); const withdrawInputCalldata = orderbook.interface.encodeFunctionData("withdraw2", [ orderPairObject.buyToken, "1", @@ -280,7 +281,26 @@ describe("Test intra-orderbook find opp", async function () { orderPairObject.sellToken, "1", ethers.constants.MaxUint256, - [], + [ + { + evaluable: { + interpreter: + orderPairObject.takeOrders[0].takeOrder.order.evaluable.interpreter, + store: orderPairObject.takeOrders[0].takeOrder.order.evaluable.store, + bytecode: getWithdrawEnsureBytecode( + signer.account.address, + orderPairObject.buyToken, + orderPairObject.sellToken, + balance, + balance, + ethers.utils.parseUnits(inputToEthPrice), + ethers.utils.parseUnits(outputToEthPrice), + ethers.constants.Zero, + ), + }, + signedContext: [], + }, + ], ]); const clear2Calldata = orderbook.interface.encodeFunctionData("clear2", [ orderPairObject.takeOrders[0].takeOrder.order, diff --git a/test/mode-routeProcessor.test.js b/test/mode-routeProcessor.test.js index 3a671319..bd20ce12 100644 --- a/test/mode-routeProcessor.test.js +++ b/test/mode-routeProcessor.test.js @@ -199,7 +199,11 @@ describe("Test route processor dryrun", async function () { interpreter: orderPairObject.takeOrders[0].takeOrder.order.evaluable.interpreter, store: orderPairObject.takeOrders[0].takeOrder.order.evaluable.store, - bytecode: "0x", + bytecode: getBountyEnsureBytecode( + ethers.utils.parseUnits(ethPrice), + ethers.constants.Zero, + ethers.constants.Zero, + ), }, signedContext: [], }; @@ -440,7 +444,11 @@ describe("Test route processor find opp", async function () { interpreter: orderPairObject.takeOrders[0].takeOrder.order.evaluable.interpreter, store: orderPairObject.takeOrders[0].takeOrder.order.evaluable.store, - bytecode: "0x", + bytecode: getBountyEnsureBytecode( + ethers.utils.parseUnits(ethPrice), + ethers.constants.Zero, + ethers.constants.Zero, + ), }, signedContext: [], }; @@ -638,7 +646,11 @@ describe("Test find opp with retries", async function () { interpreter: orderPairObject.takeOrders[0].takeOrder.order.evaluable.interpreter, store: orderPairObject.takeOrders[0].takeOrder.order.evaluable.store, - bytecode: "0x", + bytecode: getBountyEnsureBytecode( + ethers.utils.parseUnits(ethPrice), + ethers.constants.Zero, + ethers.constants.Zero, + ), }, signedContext: [], }; diff --git a/test/processPair.test.js b/test/processPair.test.js index fa0833b9..a65de918 100644 --- a/test/processPair.test.js +++ b/test/processPair.test.js @@ -150,6 +150,7 @@ describe("Test process pair", async function () { didClear: true, "details.inputToEthPrice": formatUnits(getCurrentInputToEthPrice()), "details.outputToEthPrice": "1", + "details.clearModePick": "rp4", "details.quote": JSON.stringify({ maxOutput: formatUnits(vaultBalance), ratio: formatUnits(ethers.constants.Zero), @@ -226,6 +227,7 @@ describe("Test process pair", async function () { "details.marketQuote.str": "0.99699", "details.inputToEthPrice": formatUnits(getCurrentInputToEthPrice()), "details.outputToEthPrice": "1", + "details.clearModePick": "inter", "details.quote": JSON.stringify({ maxOutput: formatUnits(vaultBalance), ratio: formatUnits(ethers.constants.Zero), @@ -546,6 +548,7 @@ describe("Test process pair", async function () { "details.outputToEthPrice": "1", "details.marketQuote.num": 0.99699, "details.marketQuote.str": "0.99699", + "details.clearModePick": "rp4", txNoneNodeError: true, "details.quote": JSON.stringify({ maxOutput: formatUnits(vaultBalance), @@ -567,7 +570,7 @@ describe("Test process pair", async function () { } }); - it("should fail to mine tx", async function () { + it("should revert tx", async function () { await mockServer.forPost("/rpc").thenSendJsonRpcResult(quoteResponse); const errorReceipt = { status: "reverted", @@ -606,7 +609,7 @@ describe("Test process pair", async function () { txUrl: scannerUrl + "/tx/" + txHash, actualGasCost: formatUnits(effectiveGasPrice.mul(gasUsed)), }, - reason: ProcessPairHaltReason.TxMineFailed, + reason: ProcessPairHaltReason.TxReverted, error: undefined, gasCost: undefined, spanAttributes: { @@ -626,6 +629,111 @@ describe("Test process pair", async function () { "details.outputToEthPrice": "1", "details.marketQuote.num": 0.99699, "details.marketQuote.str": "0.99699", + "details.clearModePick": "rp4", + "details.quote": JSON.stringify({ + maxOutput: formatUnits(vaultBalance), + ratio: formatUnits(ethers.constants.Zero), + }), + "details.estimatedProfit": formatUnits( + estimateProfit( + orderPairObject, + getCurrentInputToEthPrice(), + ethers.utils.parseUnits("1"), + undefined, + getCurrentPrice(vaultBalance), + vaultBalance, + ), + ), + }, + }; + assert.deepEqual(error, expected); + } + }); + + it("should fail to mine tx", async function () { + await mockServer.forPost("/rpc").thenSendJsonRpcResult(quoteResponse); + const errorRejection = new Error("timeout"); + dataFetcher.getCurrentPoolCodeMap = () => { + return poolCodeMap; + }; + signer.sendTransaction = async () => txHash; + viemClient.waitForTransactionReceipt = async () => Promise.reject(errorRejection); + try { + await processPair({ + config, + orderPairObject, + viemClient, + dataFetcher, + signer, + flashbotSigner: undefined, + arb, + orderbook, + pair, + mainAccount: signer, + accounts: [signer], + fetchedPairPools: [], + }); + assert.fail("expected to reject, but resolved"); + } catch (error) { + const expectedTakeOrdersConfigStruct = { + minimumInput: ethers.constants.One, + maximumInput: vaultBalance, + maximumIORatio: ethers.constants.MaxUint256, + orders: [orderPairObject.takeOrders[0].takeOrder], + data: expectedRouteData, + }; + const task = { + evaluable: { + interpreter: + orderPairObject.takeOrders[0].takeOrder.order.evaluable.interpreter, + store: orderPairObject.takeOrders[0].takeOrder.order.evaluable.store, + bytecode: "0x", + }, + signedContext: [], + }; + const rawtx = { + data: arb.interface.encodeFunctionData("arb3", [ + orderPairObject.orderbook, + expectedTakeOrdersConfigStruct, + task, + ]), + to: arb.address, + gasPrice: gasPrice.mul(107).div(100).toString(), + gas: gasLimitEstimation.toString(), + nonce: 0, + from: signer.account.address, + }; + const expected = { + report: { + status: ProcessPairReportStatus.FoundOpportunity, + tokenPair: pair, + buyToken: orderPairObject.buyToken, + sellToken: orderPairObject.sellToken, + txUrl: scannerUrl + "/tx/" + txHash, + }, + reason: ProcessPairHaltReason.TxMineFailed, + error: errorRejection, + gasCost: undefined, + spanAttributes: { + "details.pair": pair, + "details.orders": [orderPairObject.takeOrders[0].id], + "details.gasPrice": gasPrice.mul(107).div(100).toString(), + "details.blockNumber": 123456, + "details.blockNumberDiff": 0, + "details.marketPrice": formatUnits(getCurrentPrice(vaultBalance)), + "details.amountIn": formatUnits(vaultBalance), + "details.amountOut": formatUnits(getAmountOut(vaultBalance), 6), + oppBlockNumber: 123456, + "details.route": expectedRouteVisual, + foundOpp: true, + "details.rawTx": JSON.stringify(rawtx), + "details.txUrl": scannerUrl + "/tx/" + txHash, + "details.inputToEthPrice": formatUnits(getCurrentInputToEthPrice()), + "details.outputToEthPrice": "1", + "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), From 5b5fc8df3593b95313236108e78a6f104a10e2bb Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Sun, 17 Nov 2024 01:04:21 +0000 Subject: [PATCH 02/18] fix --- README.md | 4 ++++ example.env | 3 +++ src/index.ts | 1 + src/processOrders.ts | 3 ++- test/cli.test.js | 6 ++++++ 5 files changed, 16 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1f7e163e..246416e9 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,7 @@ Other optional arguments are: - `-t` or `--topup-amount`, The initial topup amount of excess wallets, requirs `--mnemonic`. Will override the 'TOPUP_AMOUNT' in env variables - `--gas-price-multiplier`, Option to multiply the gas price fetched from the rpc as percentage, default is 107, ie +7%. Will override the 'GAS_PRICE_MULTIPLIER' in env variables - `--gas-limit-multiplier`, Option to multiply the gas limit estimation from the rpc as percentage, default is 105, ie +5%. Will override the 'GAS_LIMIT_MULTIPLIER' in env variables +- `--tx-gas`, Option to set a static gas limit for all submitting txs. Will override the 'TX_GAS' in env variables - `-V` or `--version`, output the version number - `-h` or `--help`, output usage information @@ -260,6 +261,9 @@ GAS_PRICE_MULTIPLIER= # Option to multiply the gas limit estimation from the rpc as percentage, default is 105, ie +5% GAS_LIMIT_MULTIPLIER= + +# Option to set a static gas limit for all submitting txs +TX_GAS= ``` If both env variables and CLI argument are set, the CLI arguments will be prioritized and override the env variables. diff --git a/example.env b/example.env index 94fc0c01..2ff656b5 100644 --- a/example.env +++ b/example.env @@ -88,6 +88,9 @@ GAS_PRICE_MULTIPLIER= # Option to multiply the gas limit estimation from the rpc as percentage, default is 105, ie +5% GAS_LIMIT_MULTIPLIER= +# Option to set a static gas limit for all submitting txs +TX_GAS= + # test rpcs vars TEST_POLYGON_RPC= diff --git a/src/index.ts b/src/index.ts index 2e08ca27..fdccbe3c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -212,6 +212,7 @@ export async function getConfig( config.rpcRecords = rpcRecords; config.gasPriceMultiplier = options.gasPriceMultiplier; config.gasLimitMultiplier = options.gasLimitMultiplier; + config.txGas = options.txGas; // init accounts const { mainAccount, accounts } = await initAccounts(walletKey, config, options, tracer, ctx); diff --git a/src/processOrders.ts b/src/processOrders.ts index 439a3537..45c1e7c6 100644 --- a/src/processOrders.ts +++ b/src/processOrders.ts @@ -324,8 +324,9 @@ export const processOrders = async ( span.setAttribute("unsuccessfulClear", true); span.setAttribute("txSendFailed", true); } else if (e.reason === ProcessPairHaltReason.TxReverted) { - // set the severity to LOW, this can happen for example + // Tx reverted onchain, this can happen for example // because of mev front running or false positive opportunities, etc + // set the severity to LOW span.setAttribute("severity", ErrorSeverity.LOW); span.setStatus({ code: SpanStatusCode.ERROR, diff --git a/test/cli.test.js b/test/cli.test.js index 23a927fe..4e3c6293 100644 --- a/test/cli.test.js +++ b/test/cli.test.js @@ -187,6 +187,8 @@ describe("Test cli", async function () { "120", "--gas-limit-multiplier", "110", + "--tx-gas", + "123456789", ]); const expected = { roundGap: 10000, @@ -207,11 +209,13 @@ describe("Test cli", async function () { }, gasPriceMultiplier: 120, gasLimitMultiplier: 110, + txGas: 123456789n, }, options: { botMinBalance: "0.123", gasPriceMultiplier: 120, gasLimitMultiplier: 110, + txGas: 123456789n, }, }; await sleep(1000); @@ -227,5 +231,7 @@ describe("Test cli", async function () { assert.equal(result.config.gasPriceMultiplier, expected.config.gasPriceMultiplier); assert.equal(result.options.gasLimitMultiplier, expected.options.gasLimitMultiplier); assert.equal(result.config.gasLimitMultiplier, expected.config.gasLimitMultiplier); + assert.equal(result.options.txGas, expected.options.txGas); + assert.equal(result.config.txGas, expected.config.txGas); }); }); From 64eb1efbd08c8394607b800c5357a302b81a75cf Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Sun, 17 Nov 2024 04:42:17 +0000 Subject: [PATCH 03/18] set severity to high for failed txs --- src/processOrders.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/processOrders.ts b/src/processOrders.ts index 45c1e7c6..f61fbb65 100644 --- a/src/processOrders.ts +++ b/src/processOrders.ts @@ -319,15 +319,14 @@ export const processOrders = async ( message = errorSnapshot(message, e.error); span.setAttribute("errorDetails", message); } - span.setAttribute("severity", ErrorSeverity.MEDIUM); + span.setAttribute("severity", ErrorSeverity.HIGH); span.setStatus({ code: SpanStatusCode.ERROR, message }); span.setAttribute("unsuccessfulClear", true); span.setAttribute("txSendFailed", true); } else if (e.reason === ProcessPairHaltReason.TxReverted) { // Tx reverted onchain, this can happen for example // because of mev front running or false positive opportunities, etc - // set the severity to LOW - span.setAttribute("severity", ErrorSeverity.LOW); + span.setAttribute("severity", ErrorSeverity.HIGH); span.setStatus({ code: SpanStatusCode.ERROR, message: "transaction reverted onchain", @@ -341,7 +340,7 @@ export const processOrders = async ( message = errorSnapshot(message, e.error); span.setAttribute("errorDetails", message); } - span.setAttribute("severity", ErrorSeverity.MEDIUM); + span.setAttribute("severity", ErrorSeverity.HIGH); span.setStatus({ code: SpanStatusCode.ERROR, message }); span.setAttribute("unsuccessfulClear", true); span.setAttribute("txMineFailed", true); From 47653c1ef813aaf9b76ed11b27ddee8b080bffbb Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Tue, 19 Nov 2024 05:26:32 +0000 Subject: [PATCH 04/18] init --- src/error.ts | 147 ++++++++++++++++++++++++++++++++- src/modes/interOrderbook.ts | 18 ++++ src/modes/intraOrderbook.ts | 18 ++++ src/modes/routeProcessor.ts | 18 ++++ src/processOrders.ts | 17 ++-- test/abis/RouteProcessor4.json | 1 + 6 files changed, 213 insertions(+), 6 deletions(-) create mode 100644 test/abis/RouteProcessor4.json 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 From cb9515bef9281d9b0c4437e059b9ca6880e2083a Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Tue, 19 Nov 2024 17:55:42 +0000 Subject: [PATCH 05/18] update --- src/error.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/error.ts b/src/error.ts index 7ca2410e..fa286be2 100644 --- a/src/error.ts +++ b/src/error.ts @@ -56,7 +56,11 @@ export function errorSnapshot(header: string, err: any): string { if (err.name) message.push("Error: " + err.name); if (err.details) { message.push("Details: " + err.details); - if (err.details.includes("unknown reason")) { + 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); From 6881b00c2870205cd51cae1c3b3d69729d08d374 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Tue, 19 Nov 2024 19:42:42 +0000 Subject: [PATCH 06/18] add tests --- src/error.ts | 40 +++++++++++++++++++++++----------------- src/processOrders.ts | 10 +++++++--- test/error.test.js | 34 ++++++++++++++++++++++++++++++++++ test/processPair.test.js | 5 ++++- 4 files changed, 68 insertions(+), 21 deletions(-) create mode 100644 test/error.test.js diff --git a/src/error.ts b/src/error.ts index fa286be2..628efade 100644 --- a/src/error.ts +++ b/src/error.ts @@ -1,5 +1,4 @@ /* 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"; @@ -12,7 +11,6 @@ import { abi as genericArbAbi } from "../test/abis/GenericPoolOrderBookV4ArbOrde import { isHex, BaseError, - isAddress, RpcRequestError, decodeErrorResult, ExecutionRevertedError, @@ -30,17 +28,26 @@ 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; @@ -106,6 +113,9 @@ export function containsNodeError(err: BaseError): boolean { } } +/** + * Handles a reverted transaction by simulating it and returning the revert error + */ export async function handleRevert( viemClient: ViemClient, hash: `0x${string}`, @@ -130,6 +140,9 @@ export async function handleRevert( } } +/** + * Parses a revert error to TxRevertError type + */ export function parseRevertError(error: BaseError): TxRevertError { if ("cause" in error) { return parseRevertError(error.cause as any); @@ -147,29 +160,22 @@ export function parseRevertError(error: BaseError): TxRevertError { } } +/** + * 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; - } - if (typeof arg === "bigint") { - const str = BigNumber.from(arg).toHexString(); - if (isAddress(str)) { - return str; - } else { - return arg.toString(); + } else { + try { + return arg!.toString(); + } catch (error) { + return ""; } } - if (typeof arg === "number") { - return arg.toString(); - } - try { - return arg!.toString(); - } catch (error) { - return ""; - } }) ?? [] ); }; diff --git a/src/processOrders.ts b/src/processOrders.ts index bae240ee..e2654e67 100644 --- a/src/processOrders.ts +++ b/src/processOrders.ts @@ -331,8 +331,12 @@ export const processOrders = async ( message = errorSnapshot(message, e.error); span.setAttribute("errorDetails", message); } - span.setAttribute("severity", ErrorSeverity.HIGH); - span.setStatus({ code: SpanStatusCode.ERROR, 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) { @@ -779,7 +783,7 @@ 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); + const simulation = await handleRevert(viemClient as any, txhash); if (simulation) { result.error = simulation.err; spanAttributes["txNoneNodeError"] = !simulation.nodeError; 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), From 1ec3e1b59583ea280a611b52aac3acbe4d6105fc Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Tue, 19 Nov 2024 19:47:13 +0000 Subject: [PATCH 07/18] update --- src/modes/interOrderbook.ts | 9 --------- src/modes/intraOrderbook.ts | 9 --------- src/modes/routeProcessor.ts | 9 --------- 3 files changed, 27 deletions(-) diff --git a/src/modes/interOrderbook.ts b/src/modes/interOrderbook.ts index 0fcc52ac..b5be7a2b 100644 --- a/src/modes/interOrderbook.ts +++ b/src/modes/interOrderbook.ts @@ -162,15 +162,6 @@ 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 b0f6132b..7cb2c52f 100644 --- a/src/modes/intraOrderbook.ts +++ b/src/modes/intraOrderbook.ts @@ -169,15 +169,6 @@ 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 50dc9e0a..452bbda3 100644 --- a/src/modes/routeProcessor.ts +++ b/src/modes/routeProcessor.ts @@ -229,15 +229,6 @@ 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); From 011b76786e8e923a718e2e47021a68b99bfeddc2 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Wed, 20 Nov 2024 02:50:45 +0000 Subject: [PATCH 08/18] Update processOrders.ts --- src/processOrders.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/processOrders.ts b/src/processOrders.ts index e2654e67..49741b53 100644 --- a/src/processOrders.ts +++ b/src/processOrders.ts @@ -333,10 +333,10 @@ export const processOrders = async ( } if (e.spanAttributes["txNoneNodeError"]) { span.setAttribute("severity", ErrorSeverity.HIGH); - span.setStatus({ code: SpanStatusCode.ERROR, message }); } else { - span.setStatus({ code: SpanStatusCode.OK, message }); + span.setAttribute("severity", ErrorSeverity.LOW); } + span.setStatus({ code: SpanStatusCode.ERROR, message }); span.setAttribute("unsuccessfulClear", true); span.setAttribute("txReverted", true); } else if (e.reason === ProcessPairHaltReason.TxMineFailed) { From 172aa0ca327b38d43fde9efa6d2c70e5d00b3477 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Wed, 20 Nov 2024 03:50:22 +0000 Subject: [PATCH 09/18] update --- src/modes/index.ts | 10 +++++++++- src/processOrders.ts | 2 -- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/modes/index.ts b/src/modes/index.ts index 3d066895..92cb2b82 100644 --- a/src/modes/index.ts +++ b/src/modes/index.ts @@ -1,4 +1,4 @@ -import { Contract } from "ethers"; +import { BigNumber, Contract } from "ethers"; import { PublicClient } from "viem"; import { DataFetcher } from "sushi"; import { Token } from "sushi/currency"; @@ -43,6 +43,14 @@ export async function findOpp({ toToken: Token; fromToken: Token; }): Promise { + try { + gasPrice = BigNumber.from(await viemClient.getGasPrice()) + .mul(config.gasPriceMultiplier) + .div("100") + .toBigInt(); + } catch { + /**/ + } const promises = [ findRpOpp({ orderPairObject, diff --git a/src/processOrders.ts b/src/processOrders.ts index 49741b53..3c3069e5 100644 --- a/src/processOrders.ts +++ b/src/processOrders.ts @@ -333,8 +333,6 @@ export const processOrders = async ( } if (e.spanAttributes["txNoneNodeError"]) { span.setAttribute("severity", ErrorSeverity.HIGH); - } else { - span.setAttribute("severity", ErrorSeverity.LOW); } span.setStatus({ code: SpanStatusCode.ERROR, message }); span.setAttribute("unsuccessfulClear", true); From e770df767c7f952c2fcd71f3d47c08a1643dc16c Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Wed, 20 Nov 2024 04:12:17 +0000 Subject: [PATCH 10/18] init --- src/modes/interOrderbook.ts | 4 ++-- src/modes/intraOrderbook.ts | 4 ++-- src/modes/routeProcessor.ts | 6 ++++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/modes/interOrderbook.ts b/src/modes/interOrderbook.ts index b5be7a2b..8dcff154 100644 --- a/src/modes/interOrderbook.ts +++ b/src/modes/interOrderbook.ts @@ -118,7 +118,7 @@ export async function dryrun({ } catch { /**/ } - gasLimit = ethers.BigNumber.from(await signer.estimateGas(rawtx)) + gasLimit = ethers.BigNumber.from(await signer.estimateGas({ ...rawtx, type: "legacy" })) .mul(config.gasLimitMultiplier) .div(100); } catch (e) { @@ -162,7 +162,7 @@ export async function dryrun({ try { blockNumber = Number(await viemClient.getBlockNumber()); spanAttributes["blockNumber"] = blockNumber; - gasLimit = ethers.BigNumber.from(await signer.estimateGas(rawtx)) + gasLimit = ethers.BigNumber.from(await signer.estimateGas({ ...rawtx, type: "legacy" })) .mul(config.gasLimitMultiplier) .div(100); rawtx.gas = gasLimit.toBigInt(); diff --git a/src/modes/intraOrderbook.ts b/src/modes/intraOrderbook.ts index 7cb2c52f..3750daa3 100644 --- a/src/modes/intraOrderbook.ts +++ b/src/modes/intraOrderbook.ts @@ -115,7 +115,7 @@ export async function dryrun({ } catch { /**/ } - gasLimit = ethers.BigNumber.from(await signer.estimateGas(rawtx)) + gasLimit = ethers.BigNumber.from(await signer.estimateGas({ ...rawtx, type: "legacy" })) .mul(config.gasLimitMultiplier) .div(100); } catch (e) { @@ -169,7 +169,7 @@ export async function dryrun({ try { blockNumber = Number(await viemClient.getBlockNumber()); spanAttributes["blockNumber"] = blockNumber; - gasLimit = ethers.BigNumber.from(await signer.estimateGas(rawtx)) + gasLimit = ethers.BigNumber.from(await signer.estimateGas({ ...rawtx, type: "legacy" })) .mul(config.gasLimitMultiplier) .div(100); rawtx.gas = gasLimit.toBigInt(); diff --git a/src/modes/routeProcessor.ts b/src/modes/routeProcessor.ts index 452bbda3..9960cef3 100644 --- a/src/modes/routeProcessor.ts +++ b/src/modes/routeProcessor.ts @@ -183,7 +183,7 @@ export async function dryrun({ } catch { /**/ } - gasLimit = ethers.BigNumber.from(await signer.estimateGas(rawtx)) + gasLimit = ethers.BigNumber.from(await signer.estimateGas({ ...rawtx, type: "legacy" })) .mul(config.gasLimitMultiplier) .div(100); } catch (e) { @@ -229,7 +229,9 @@ export async function dryrun({ try { blockNumber = Number(await viemClient.getBlockNumber()); spanAttributes["blockNumber"] = blockNumber; - gasLimit = ethers.BigNumber.from(await signer.estimateGas(rawtx)) + gasLimit = ethers.BigNumber.from( + await signer.estimateGas({ ...rawtx, type: "legacy" }), + ) .mul(config.gasLimitMultiplier) .div(100); rawtx.gas = gasLimit.toBigInt(); From 02950067ee740558827725b57c01c36314f03159 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Wed, 20 Nov 2024 04:29:01 +0000 Subject: [PATCH 11/18] update --- src/modes/interOrderbook.ts | 2 ++ src/modes/intraOrderbook.ts | 2 ++ src/modes/routeProcessor.ts | 2 ++ 3 files changed, 6 insertions(+) diff --git a/src/modes/interOrderbook.ts b/src/modes/interOrderbook.ts index 8dcff154..a8779750 100644 --- a/src/modes/interOrderbook.ts +++ b/src/modes/interOrderbook.ts @@ -124,6 +124,7 @@ export async function dryrun({ } catch (e) { const isNodeError = containsNodeError(e as BaseError); const errMsg = errorSnapshot("", e); + spanAttributes["stage"] = 1; spanAttributes["isNodeError"] = isNodeError; spanAttributes["error"] = errMsg; spanAttributes["rawtx"] = JSON.stringify( @@ -180,6 +181,7 @@ export async function dryrun({ } catch (e) { const isNodeError = containsNodeError(e as BaseError); const errMsg = errorSnapshot("", e); + spanAttributes["stage"] = 2; spanAttributes["isNodeError"] = isNodeError; spanAttributes["error"] = errMsg; spanAttributes["rawtx"] = JSON.stringify( diff --git a/src/modes/intraOrderbook.ts b/src/modes/intraOrderbook.ts index 3750daa3..a4ebeda0 100644 --- a/src/modes/intraOrderbook.ts +++ b/src/modes/intraOrderbook.ts @@ -122,6 +122,7 @@ export async function dryrun({ // reason, code, method, transaction, error, stack, message const isNodeError = containsNodeError(e as BaseError); const errMsg = errorSnapshot("", e); + spanAttributes["stage"] = 1; spanAttributes["isNodeError"] = isNodeError; spanAttributes["error"] = errMsg; spanAttributes["rawtx"] = JSON.stringify( @@ -196,6 +197,7 @@ export async function dryrun({ } catch (e) { const isNodeError = containsNodeError(e as BaseError); const errMsg = errorSnapshot("", e); + spanAttributes["stage"] = 2; spanAttributes["isNodeError"] = isNodeError; spanAttributes["error"] = errMsg; spanAttributes["rawtx"] = JSON.stringify( diff --git a/src/modes/routeProcessor.ts b/src/modes/routeProcessor.ts index 9960cef3..4f0739ff 100644 --- a/src/modes/routeProcessor.ts +++ b/src/modes/routeProcessor.ts @@ -190,6 +190,7 @@ export async function dryrun({ // reason, code, method, transaction, error, stack, message const isNodeError = containsNodeError(e as BaseError); const errMsg = errorSnapshot("", e); + spanAttributes["stage"] = 1; spanAttributes["isNodeError"] = isNodeError; spanAttributes["error"] = errMsg; spanAttributes["rawtx"] = JSON.stringify( @@ -249,6 +250,7 @@ export async function dryrun({ } catch (e) { const isNodeError = containsNodeError(e as BaseError); const errMsg = errorSnapshot("", e); + spanAttributes["stage"] = 2; spanAttributes["isNodeError"] = isNodeError; spanAttributes["error"] = errMsg; spanAttributes["rawtx"] = JSON.stringify( From 4bb6a6c670ed989d266dffb48c0e61653841e4e9 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Wed, 20 Nov 2024 17:55:23 +0000 Subject: [PATCH 12/18] update --- src/account.ts | 19 ++++++++++++++++--- src/cli.ts | 12 ++++++++---- src/error.ts | 4 ++++ src/modes/interOrderbook.ts | 9 --------- src/modes/intraOrderbook.ts | 9 --------- src/modes/routeProcessor.ts | 9 --------- test/account.test.js | 2 +- test/mode-interOrderbook.test.js | 1 + test/mode-intraOrderbook.test.js | 1 + test/mode-routeProcessor.test.js | 13 +++++++------ 10 files changed, 38 insertions(+), 41 deletions(-) diff --git a/src/account.ts b/src/account.ts index 5e5b418c..9c13819b 100644 --- a/src/account.ts +++ b/src/account.ts @@ -157,10 +157,19 @@ export async function manageAccounts( ) { const removedWallets: ViemClient[] = []; let accountsToAdd = 0; - const gasPrice = await config.viemClient.getGasPrice(); + try { + const balances = await getBatchEthBalance( + config.accounts.map((v) => v.account.address), + config.viemClient as any as ViemClient, + ); + config.accounts.forEach((v, i) => (v.BALANCE = balances[i])); + } catch { + /**/ + } for (let i = config.accounts.length - 1; i >= 0; i--) { if (config.accounts[i].BALANCE.lt(avgGasCost.mul(4))) { try { + const gasPrice = await config.viemClient.getGasPrice(); await sweepToMainWallet( config.accounts[i], config.mainAccount, @@ -529,11 +538,15 @@ export async function sweepToMainWallet( } } - if (cumulativeGasLimit.mul(gasPrice).gt(fromWallet.BALANCE)) { + if (cumulativeGasLimit.mul(gasPrice).mul(120).div(100).gt(fromWallet.BALANCE)) { const span = tracer?.startSpan("fund-wallet-to-sweep", undefined, mainCtx); span?.setAttribute("details.wallet", fromWallet.account.address); try { - const transferAmount = cumulativeGasLimit.mul(gasPrice).sub(fromWallet.BALANCE); + const transferAmount = cumulativeGasLimit + .mul(gasPrice) + .mul(120) + .div(100) + .sub(fromWallet.BALANCE); span?.setAttribute("details.amount", ethers.utils.formatUnits(transferAmount)); const hash = await toWallet.sendTransaction({ to: fromWallet.account.address, diff --git a/src/cli.ts b/src/cli.ts index a33e3163..4be3575a 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -310,7 +310,7 @@ export const arbRound = async ( span.setAttribute("didClear", false); } if (avgGasCost) { - span.setAttribute("avgGasCost", avgGasCost.toString()); + span.setAttribute("avgGasCost", ethers.utils.formatUnits(avgGasCost)); } span.setStatus({ code: SpanStatusCode.OK }); span.end(); @@ -713,10 +713,14 @@ export const main = async (argv: any, version?: string) => { roundSpan.setStatus({ code: SpanStatusCode.ERROR, message: snapshot }); } if (config.accounts.length) { - roundSpan.setAttribute( - "circulatingAccounts", - config.accounts.map((v) => v.account.address), + const accountsWithBalance: Record = {}; + config.accounts.forEach( + (v) => + (accountsWithBalance[v.account.address] = ethers.utils.formatUnits( + v.BALANCE, + )), ); + roundSpan.setAttribute("circulatingAccounts", JSON.stringify(accountsWithBalance)); } if (avgGasCost) { roundSpan.setAttribute("avgGasCost", ethers.utils.formatUnits(avgGasCost)); diff --git a/src/error.ts b/src/error.ts index 628efade..f3b367c6 100644 --- a/src/error.ts +++ b/src/error.ts @@ -15,6 +15,7 @@ import { decodeErrorResult, ExecutionRevertedError, InsufficientFundsError, + FeeCapTooLowError, // InvalidInputRpcError, // TransactionRejectedRpcError, } from "viem"; @@ -100,12 +101,15 @@ export function errorSnapshot(header: string, err: any): string { */ export function containsNodeError(err: BaseError): boolean { try { + const snapshot = errorSnapshot("", err); return ( // err instanceof TransactionRejectedRpcError || // err instanceof InvalidInputRpcError || + err instanceof FeeCapTooLowError || err instanceof ExecutionRevertedError || err instanceof InsufficientFundsError || (err instanceof RpcRequestError && err.code === ExecutionRevertedError.code) || + (snapshot.includes("exceeds allowance") && !snapshot.includes("out of gas")) || ("cause" in err && containsNodeError(err.cause as any)) ); } catch (error) { diff --git a/src/modes/interOrderbook.ts b/src/modes/interOrderbook.ts index a8779750..13568ac6 100644 --- a/src/modes/interOrderbook.ts +++ b/src/modes/interOrderbook.ts @@ -109,15 +109,6 @@ 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, type: "legacy" })) .mul(config.gasLimitMultiplier) .div(100); diff --git a/src/modes/intraOrderbook.ts b/src/modes/intraOrderbook.ts index a4ebeda0..8e285313 100644 --- a/src/modes/intraOrderbook.ts +++ b/src/modes/intraOrderbook.ts @@ -106,15 +106,6 @@ 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, type: "legacy" })) .mul(config.gasLimitMultiplier) .div(100); diff --git a/src/modes/routeProcessor.ts b/src/modes/routeProcessor.ts index 4f0739ff..7cbca636 100644 --- a/src/modes/routeProcessor.ts +++ b/src/modes/routeProcessor.ts @@ -174,15 +174,6 @@ 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, type: "legacy" })) .mul(config.gasLimitMultiplier) .div(100); diff --git a/test/account.test.js b/test/account.test.js index c4d0ecb3..59a87f51 100644 --- a/test/account.test.js +++ b/test/account.test.js @@ -105,7 +105,7 @@ describe("Test accounts", async function () { it("should manage accounts successfully", async function () { const viemClient = { chain: { id: 137 }, - multicall: async () => [10000n, 0n, 0n], + multicall: async () => [10n, 0n], getGasPrice: async () => 3000000n, }; const mnemonic = "test test test test test test test test test test test junk"; diff --git a/test/mode-interOrderbook.test.js b/test/mode-interOrderbook.test.js index b7ac2a7e..bb02ab7d 100644 --- a/test/mode-interOrderbook.test.js +++ b/test/mode-interOrderbook.test.js @@ -433,6 +433,7 @@ describe("Test inter-orderbook find opp", async function () { [opposingOrderbookAddress]: { maxInput: vaultBalance.toString(), blockNumber: oppBlockNumber, + stage: 1, isNodeError: false, error: errorSnapshot("", err), rawtx: JSON.stringify(rawtx), diff --git a/test/mode-intraOrderbook.test.js b/test/mode-intraOrderbook.test.js index a7c84110..e2647c24 100644 --- a/test/mode-intraOrderbook.test.js +++ b/test/mode-intraOrderbook.test.js @@ -334,6 +334,7 @@ describe("Test intra-orderbook find opp", async function () { intraOrderbook: [ JSON.stringify({ blockNumber: oppBlockNumber, + stage: 1, isNodeError: false, error: errorSnapshot("", err), rawtx: JSON.stringify(rawtx), diff --git a/test/mode-routeProcessor.test.js b/test/mode-routeProcessor.test.js index bd20ce12..ca373fd2 100644 --- a/test/mode-routeProcessor.test.js +++ b/test/mode-routeProcessor.test.js @@ -230,6 +230,7 @@ describe("Test route processor dryrun", async function () { blockNumber: oppBlockNumber, error: errorSnapshot("", ethers.errors.UNPREDICTABLE_GAS_LIMIT), route: expectedRouteVisual, + stage: 1, rawtx: JSON.stringify(rawtx), isNodeError: false, }, @@ -470,9 +471,9 @@ describe("Test route processor find opp", async function () { reason: RouteProcessorDryrunHaltReason.NoOpportunity, spanAttributes: { hops: [ - `{"amountIn":"${formatUnits(vaultBalance)}","amountOut":"${formatUnits(getAmountOut(vaultBalance), 6)}","marketPrice":"${formatUnits(getCurrentPrice(vaultBalance))}","route":${JSON.stringify(expectedRouteVisual)},"blockNumber":${oppBlockNumber},"isNodeError":false,"error":${JSON.stringify(errorSnapshot("", ethers.errors.UNPREDICTABLE_GAS_LIMIT))},"rawtx":${JSON.stringify(rawtx)}}`, - `{"amountIn":"${formatUnits(vaultBalance.div(2))}","amountOut":"${formatUnits(getAmountOut(vaultBalance.div(2)), 6)}","marketPrice":"${formatUnits(getCurrentPrice(vaultBalance.div(2)))}","route":${JSON.stringify(expectedRouteVisual)},"blockNumber":${oppBlockNumber},"isNodeError":false}`, - `{"amountIn":"${formatUnits(vaultBalance.div(4))}","amountOut":"${formatUnits(getAmountOut(vaultBalance.div(4)), 6)}","marketPrice":"${formatUnits(getCurrentPrice(vaultBalance.div(4)))}","route":${JSON.stringify(expectedRouteVisual)},"blockNumber":${oppBlockNumber},"isNodeError":false}`, + `{"amountIn":"${formatUnits(vaultBalance)}","amountOut":"${formatUnits(getAmountOut(vaultBalance), 6)}","marketPrice":"${formatUnits(getCurrentPrice(vaultBalance))}","route":${JSON.stringify(expectedRouteVisual)},"blockNumber":${oppBlockNumber},"stage":1,"isNodeError":false,"error":${JSON.stringify(errorSnapshot("", ethers.errors.UNPREDICTABLE_GAS_LIMIT))},"rawtx":${JSON.stringify(rawtx)}}`, + `{"amountIn":"${formatUnits(vaultBalance.div(2))}","amountOut":"${formatUnits(getAmountOut(vaultBalance.div(2)), 6)}","marketPrice":"${formatUnits(getCurrentPrice(vaultBalance.div(2)))}","route":${JSON.stringify(expectedRouteVisual)},"blockNumber":${oppBlockNumber},"stage":1,"isNodeError":false}`, + `{"amountIn":"${formatUnits(vaultBalance.div(4))}","amountOut":"${formatUnits(getAmountOut(vaultBalance.div(4)), 6)}","marketPrice":"${formatUnits(getCurrentPrice(vaultBalance.div(4)))}","route":${JSON.stringify(expectedRouteVisual)},"blockNumber":${oppBlockNumber},"stage":1,"isNodeError":false}`, ], }, }; @@ -672,9 +673,9 @@ describe("Test find opp with retries", async function () { reason: RouteProcessorDryrunHaltReason.NoOpportunity, spanAttributes: { hops: [ - `{"amountIn":"${formatUnits(vaultBalance)}","amountOut":"${formatUnits(getAmountOut(vaultBalance), 6)}","marketPrice":"${formatUnits(getCurrentPrice(vaultBalance))}","route":${JSON.stringify(expectedRouteVisual)},"blockNumber":${oppBlockNumber},"isNodeError":false,"error":${JSON.stringify(errorSnapshot("", ethers.errors.UNPREDICTABLE_GAS_LIMIT))},"rawtx":${JSON.stringify(rawtx)}}`, - `{"amountIn":"${formatUnits(vaultBalance.div(2))}","amountOut":"${formatUnits(getAmountOut(vaultBalance.div(2)), 6)}","marketPrice":"${formatUnits(getCurrentPrice(vaultBalance.div(2)))}","route":${JSON.stringify(expectedRouteVisual)},"blockNumber":${oppBlockNumber},"isNodeError":false}`, - `{"amountIn":"${formatUnits(vaultBalance.div(4))}","amountOut":"${formatUnits(getAmountOut(vaultBalance.div(4)), 6)}","marketPrice":"${formatUnits(getCurrentPrice(vaultBalance.div(4)))}","route":${JSON.stringify(expectedRouteVisual)},"blockNumber":${oppBlockNumber},"isNodeError":false}`, + `{"amountIn":"${formatUnits(vaultBalance)}","amountOut":"${formatUnits(getAmountOut(vaultBalance), 6)}","marketPrice":"${formatUnits(getCurrentPrice(vaultBalance))}","route":${JSON.stringify(expectedRouteVisual)},"blockNumber":${oppBlockNumber},"stage":1,"isNodeError":false,"error":${JSON.stringify(errorSnapshot("", ethers.errors.UNPREDICTABLE_GAS_LIMIT))},"rawtx":${JSON.stringify(rawtx)}}`, + `{"amountIn":"${formatUnits(vaultBalance.div(2))}","amountOut":"${formatUnits(getAmountOut(vaultBalance.div(2)), 6)}","marketPrice":"${formatUnits(getCurrentPrice(vaultBalance.div(2)))}","route":${JSON.stringify(expectedRouteVisual)},"blockNumber":${oppBlockNumber},"stage":1,"isNodeError":false}`, + `{"amountIn":"${formatUnits(vaultBalance.div(4))}","amountOut":"${formatUnits(getAmountOut(vaultBalance.div(4)), 6)}","marketPrice":"${formatUnits(getCurrentPrice(vaultBalance.div(4)))}","route":${JSON.stringify(expectedRouteVisual)},"blockNumber":${oppBlockNumber},"stage":1,"isNodeError":false}`, ], }, }; From 5cb245035b19f0df4c1f7790c44732aa5dc868dc Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Wed, 20 Nov 2024 18:01:28 +0000 Subject: [PATCH 13/18] update --- src/account.ts | 9 --------- src/cli.ts | 21 ++++++++++++++++++++- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/account.ts b/src/account.ts index 9c13819b..0c3cec5a 100644 --- a/src/account.ts +++ b/src/account.ts @@ -157,15 +157,6 @@ export async function manageAccounts( ) { const removedWallets: ViemClient[] = []; let accountsToAdd = 0; - try { - const balances = await getBatchEthBalance( - config.accounts.map((v) => v.account.address), - config.viemClient as any as ViemClient, - ); - config.accounts.forEach((v, i) => (v.BALANCE = balances[i])); - } catch { - /**/ - } for (let i = config.accounts.length - 1; i >= 0; i--) { if (config.accounts[i].BALANCE.lt(avgGasCost.mul(4))) { try { diff --git a/src/cli.ts b/src/cli.ts index 4be3575a..4e3cc6da 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -13,7 +13,13 @@ import { BotConfig, CliOptions, ViemClient } from "./types"; import { CompressionAlgorithm } from "@opentelemetry/otlp-exporter-base"; import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"; import { SEMRESATTRS_SERVICE_NAME } from "@opentelemetry/semantic-conventions"; -import { manageAccounts, rotateProviders, sweepToMainWallet, sweepToEth } from "./account"; +import { + sweepToEth, + manageAccounts, + rotateProviders, + sweepToMainWallet, + getBatchEthBalance, +} from "./account"; import { diag, trace, @@ -627,6 +633,19 @@ export const main = async (argv: any, version?: string) => { roundSpan.setAttribute("didClear", false); } + // fecth account's balances + if (foundOpp && config.accounts.length) { + try { + const balances = await getBatchEthBalance( + config.accounts.map((v) => v.account.address), + config.viemClient as any as ViemClient, + ); + config.accounts.forEach((v, i) => (v.BALANCE = balances[i])); + } catch { + /**/ + } + } + // keep avg gas cost if (roundAvgGasCost) { const _now = Date.now(); From c7c91a8e4f51adf6412e2bef37f996cb25e2252b Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Wed, 20 Nov 2024 19:58:18 +0000 Subject: [PATCH 14/18] Update account.ts --- src/account.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/account.ts b/src/account.ts index 0c3cec5a..552444cc 100644 --- a/src/account.ts +++ b/src/account.ts @@ -529,20 +529,20 @@ export async function sweepToMainWallet( } } - if (cumulativeGasLimit.mul(gasPrice).mul(120).div(100).gt(fromWallet.BALANCE)) { + if (cumulativeGasLimit.mul(gasPrice).mul(125).div(100).gt(fromWallet.BALANCE)) { const span = tracer?.startSpan("fund-wallet-to-sweep", undefined, mainCtx); span?.setAttribute("details.wallet", fromWallet.account.address); try { const transferAmount = cumulativeGasLimit .mul(gasPrice) - .mul(120) + .mul(125) .div(100) .sub(fromWallet.BALANCE); span?.setAttribute("details.amount", ethers.utils.formatUnits(transferAmount)); const hash = await toWallet.sendTransaction({ to: fromWallet.account.address, value: transferAmount.toBigInt(), - nonce: await getNonce(fromWallet), + nonce: await getNonce(toWallet), }); const receipt = await toWallet.waitForTransactionReceipt({ hash, @@ -640,6 +640,7 @@ export async function sweepToMainWallet( if (transferAmount.gt(0)) { span?.setAttribute("details.amount", ethers.utils.formatUnits(transferAmount)); const hash = await fromWallet.sendTransaction({ + gasPrice, to: toWallet.account.address, value: transferAmount.toBigInt(), gas: gasLimit.toBigInt(), From 2ab4788f6ebaee30e4d793ba9cb15f7212622d29 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Thu, 21 Nov 2024 01:10:17 +0000 Subject: [PATCH 15/18] update --- src/account.ts | 8 +- src/cli.ts | 1 + src/error.ts | 6 +- src/modes/interOrderbook.ts | 68 ++++++------ src/modes/intraOrderbook.ts | 4 +- src/modes/routeProcessor.ts | 174 ++++++++++++++++++++++--------- src/utils.ts | 24 +++++ test/mode-interOrderbook.test.js | 94 ----------------- test/mode-routeProcessor.test.js | 28 ++--- test/utils.test.js | 35 ++++++- 10 files changed, 233 insertions(+), 209 deletions(-) diff --git a/src/account.ts b/src/account.ts index 552444cc..4bd5b75d 100644 --- a/src/account.ts +++ b/src/account.ts @@ -1,10 +1,10 @@ +import { ChainId, RPParams } from "sushi"; import { BigNumber, ethers } from "ethers"; import { ErrorSeverity, errorSnapshot } from "./error"; import { Native, Token, WNATIVE } from "sushi/currency"; import { ROUTE_PROCESSOR_4_ADDRESS } from "sushi/config"; import { getRpSwap, PoolBlackList, sleep } from "./utils"; import { createViemClient, getDataFetcher } from "./config"; -import { ChainId, LiquidityProviders, RPParams } from "sushi"; import { mnemonicToAccount, privateKeyToAccount } from "viem/accounts"; import { erc20Abi, multicall3Abi, orderbookAbi, routeProcessor3Abi } from "./abis"; import { context, Context, SpanStatusCode, trace, Tracer } from "@opentelemetry/api"; @@ -747,7 +747,7 @@ export async function sweepToEth(config: BotConfig, tracer?: Tracer, ctx?: Conte rp4Address, config.dataFetcher, gasPrice, - Object.values(LiquidityProviders).filter((v) => v !== LiquidityProviders.CurveSwap), + config.lps, ); let routeText = ""; route.legs.forEach((v, i) => { @@ -804,7 +804,7 @@ export async function sweepToEth(config: BotConfig, tracer?: Tracer, ctx?: Conte const rawtx = { to: rp4Address, data: "0x" as `0x${string}` }; let gas = 0n; let amountOutMin = ethers.constants.Zero; - for (let j = 50; j > 0; j--) { + for (let j = 50; j > 39; j--) { amountOutMin = ethers.BigNumber.from(rpParams.amountOutMin) .mul(2 * j) .div(100); @@ -820,7 +820,7 @@ export async function sweepToEth(config: BotConfig, tracer?: Tracer, ctx?: Conte gas = await config.mainAccount.estimateGas(rawtx); break; } catch (error) { - if (j === 1) throw error; + if (j === 40) throw error; } } const gasCost = gasPrice.mul(gas).mul(15).div(10); diff --git a/src/cli.ts b/src/cli.ts index 4e3cc6da..ce434561 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -740,6 +740,7 @@ export const main = async (argv: any, version?: string) => { )), ); roundSpan.setAttribute("circulatingAccounts", JSON.stringify(accountsWithBalance)); + roundSpan.setAttribute("lastAccountIndex", lastUsedAccountIndex); } if (avgGasCost) { roundSpan.setAttribute("avgGasCost", ethers.utils.formatUnits(avgGasCost)); diff --git a/src/error.ts b/src/error.ts index f3b367c6..88a8108c 100644 --- a/src/error.ts +++ b/src/error.ts @@ -64,11 +64,7 @@ export function errorSnapshot(header: string, err: any): string { if (err.name) message.push("Error: " + err.name); if (err.details) { message.push("Details: " + err.details); - if ( - err.name.includes("unknown reason") || - err.details.includes("unknown reason") || - err.shortMessage.includes("unknown reason") - ) { + if (message.some((v) => v.includes("unknown reason"))) { const { raw, decoded } = parseRevertError(err); if (decoded) { message.push("Error Name: " + decoded.name); diff --git a/src/modes/interOrderbook.ts b/src/modes/interOrderbook.ts index 13568ac6..1577f21e 100644 --- a/src/modes/interOrderbook.ts +++ b/src/modes/interOrderbook.ts @@ -109,7 +109,7 @@ export async function dryrun({ try { blockNumber = Number(await viemClient.getBlockNumber()); spanAttributes["blockNumber"] = blockNumber; - gasLimit = ethers.BigNumber.from(await signer.estimateGas({ ...rawtx, type: "legacy" })) + gasLimit = ethers.BigNumber.from(await signer.estimateGas(rawtx)) .mul(config.gasLimitMultiplier) .div(100); } catch (e) { @@ -154,7 +154,7 @@ export async function dryrun({ try { blockNumber = Number(await viemClient.getBlockNumber()); spanAttributes["blockNumber"] = blockNumber; - gasLimit = ethers.BigNumber.from(await signer.estimateGas({ ...rawtx, type: "legacy" })) + gasLimit = ethers.BigNumber.from(await signer.estimateGas(rawtx)) .mul(config.gasLimitMultiplier) .div(100); rawtx.gas = gasLimit.toBigInt(); @@ -269,7 +269,7 @@ export async function findOpp({ .filter((v) => v !== undefined) as BundledOrders[]; if (!opposingOrderbookOrders || !opposingOrderbookOrders.length) throw undefined; - let maximumInput = orderPairObject.takeOrders.reduce( + const maximumInput = orderPairObject.takeOrders.reduce( (a, b) => a.add(b.quote!.maxOutput), ethers.constants.Zero, ); @@ -304,37 +304,37 @@ export async function findOpp({ for (const err of (e as AggregateError).errors) { allNoneNodeErrors.push(err?.value?.noneNodeError); } - maximumInput = maximumInput.div(2); - try { - // try to find the first resolving binary search - return await Promise.any( - opposingOrderbookOrders.map((v) => { - // filter out the same owner orders - const opposingOrders = { - ...v, - takeOrders: v.takeOrders.filter( - (e) => - e.takeOrder.order.owner.toLowerCase() !== - orderPairObject.takeOrders[0].takeOrder.order.owner.toLowerCase(), - ), - }; - return binarySearch({ - orderPairObject, - opposingOrders, - signer, - maximumInput, - gasPrice, - arb, - inputToEthPrice, - outputToEthPrice, - config, - viemClient, - }); - }), - ); - } catch { - /**/ - } + // maximumInput = maximumInput.div(2); + // try { + // // try to find the first resolving binary search + // return await Promise.any( + // opposingOrderbookOrders.map((v) => { + // // filter out the same owner orders + // const opposingOrders = { + // ...v, + // takeOrders: v.takeOrders.filter( + // (e) => + // e.takeOrder.order.owner.toLowerCase() !== + // orderPairObject.takeOrders[0].takeOrder.order.owner.toLowerCase(), + // ), + // }; + // return binarySearch({ + // orderPairObject, + // opposingOrders, + // signer, + // maximumInput, + // gasPrice, + // arb, + // inputToEthPrice, + // outputToEthPrice, + // config, + // viemClient, + // }); + // }), + // ); + // } catch { + // /**/ + // } const allOrderbooksAttributes: any = {}; for (let i = 0; i < e.errors.length; i++) { allOrderbooksAttributes[opposingOrderbookOrders[i].orderbook] = diff --git a/src/modes/intraOrderbook.ts b/src/modes/intraOrderbook.ts index 8e285313..acf56665 100644 --- a/src/modes/intraOrderbook.ts +++ b/src/modes/intraOrderbook.ts @@ -106,7 +106,7 @@ export async function dryrun({ try { blockNumber = Number(await viemClient.getBlockNumber()); spanAttributes["blockNumber"] = blockNumber; - gasLimit = ethers.BigNumber.from(await signer.estimateGas({ ...rawtx, type: "legacy" })) + gasLimit = ethers.BigNumber.from(await signer.estimateGas(rawtx)) .mul(config.gasLimitMultiplier) .div(100); } catch (e) { @@ -161,7 +161,7 @@ export async function dryrun({ try { blockNumber = Number(await viemClient.getBlockNumber()); spanAttributes["blockNumber"] = blockNumber; - gasLimit = ethers.BigNumber.from(await signer.estimateGas({ ...rawtx, type: "legacy" })) + gasLimit = ethers.BigNumber.from(await signer.estimateGas(rawtx)) .mul(config.gasLimitMultiplier) .div(100); rawtx.gas = gasLimit.toBigInt(); diff --git a/src/modes/routeProcessor.ts b/src/modes/routeProcessor.ts index 7cbca636..6c0f7725 100644 --- a/src/modes/routeProcessor.ts +++ b/src/modes/routeProcessor.ts @@ -5,7 +5,14 @@ import { ChainId, DataFetcher, Router } from "sushi"; import { BigNumber, Contract, ethers } from "ethers"; import { containsNodeError, errorSnapshot } from "../error"; import { BotConfig, BundledOrders, ViemClient, DryrunResult, SpanAttrs } from "../types"; -import { estimateProfit, RPoolFilter, visualizeRoute, withBigintSerializer } from "../utils"; +import { + scale18, + scale18To, + RPoolFilter, + estimateProfit, + visualizeRoute, + withBigintSerializer, +} from "../utils"; /** * Specifies the reason that dryrun failed @@ -174,7 +181,7 @@ export async function dryrun({ try { blockNumber = Number(await viemClient.getBlockNumber()); spanAttributes["blockNumber"] = blockNumber; - gasLimit = ethers.BigNumber.from(await signer.estimateGas({ ...rawtx, type: "legacy" })) + gasLimit = ethers.BigNumber.from(await signer.estimateGas(rawtx)) .mul(config.gasLimitMultiplier) .div(100); } catch (e) { @@ -221,9 +228,7 @@ export async function dryrun({ try { blockNumber = Number(await viemClient.getBlockNumber()); spanAttributes["blockNumber"] = blockNumber; - gasLimit = ethers.BigNumber.from( - await signer.estimateGas({ ...rawtx, type: "legacy" }), - ) + gasLimit = ethers.BigNumber.from(await signer.estimateGas(rawtx)) .mul(config.gasLimitMultiplier) .div(100); rawtx.gas = gasLimit.toBigInt(); @@ -332,79 +337,81 @@ export async function findOpp({ (a, b) => a.add(b.quote!.maxOutput), ethers.constants.Zero, ); - let maximumInput = BigNumber.from(initAmount.toString()); + const maximumInput = BigNumber.from(initAmount.toString()); - const allSuccessHops: DryrunResult[] = []; const allHopsAttributes: string[] = []; const allNoneNodeErrors: (string | undefined)[] = []; - for (let i = 1; i < config.hops + 1; i++) { + try { + return await dryrun({ + mode, + orderPairObject, + dataFetcher, + fromToken, + toToken, + signer, + maximumInput, + gasPrice, + arb, + ethPrice, + config, + viemClient, + }); + } catch (e: any) { + // the fail reason can only be no route in case all hops fail reasons are no route + if (e.reason !== RouteProcessorDryrunHaltReason.NoRoute) noRoute = false; + allNoneNodeErrors.push(e?.value?.noneNodeError); + allHopsAttributes.push(JSON.stringify(e.spanAttributes)); + } + const maxTradeSize = findMaxInput({ + orderPairObject, + dataFetcher, + fromToken, + toToken, + maximumInput, + gasPrice, + config, + }); + if (maxTradeSize) { try { - const dryrunResult = await dryrun({ + return await dryrun({ mode, orderPairObject, dataFetcher, fromToken, toToken, signer, - maximumInput, + maximumInput: maxTradeSize, gasPrice, arb, ethPrice, config, viemClient, }); - - // return early if there was success on first attempt (ie full vault balance) - // else record the success result - if (i == 1) { - return dryrunResult; - } else { - allSuccessHops.push(dryrunResult); - } - // set the maxInput for next hop by increasing - maximumInput = maximumInput.add(initAmount.div(2 ** i)); } catch (e: any) { // the fail reason can only be no route in case all hops fail reasons are no route if (e.reason !== RouteProcessorDryrunHaltReason.NoRoute) noRoute = false; - - // record this hop attributes - // error attr is only recorded for first hop, - // since it is repeated and consumes lots of data - if (i !== 1) { - delete e.spanAttributes["error"]; - delete e.spanAttributes["rawtx"]; - } + delete e.spanAttributes["error"]; + delete e.spanAttributes["rawtx"]; allNoneNodeErrors.push(e?.value?.noneNodeError); allHopsAttributes.push(JSON.stringify(e.spanAttributes)); - - // set the maxInput for next hop by decreasing - maximumInput = maximumInput.sub(initAmount.div(2 ** i)); } } + // in case of no successfull hop, allHopsAttributes will be included + spanAttributes["hops"] = allHopsAttributes; - if (allSuccessHops.length) { - return allSuccessHops[allSuccessHops.length - 1]; - } else { - // in case of no successfull hop, allHopsAttributes will be included - spanAttributes["hops"] = allHopsAttributes; - - if (noRoute) result.reason = RouteProcessorDryrunHaltReason.NoRoute; - else { - const noneNodeErrors = allNoneNodeErrors.filter((v) => !!v); - if ( - allNoneNodeErrors.length && - noneNodeErrors.length / allNoneNodeErrors.length > 0.5 - ) { - result.value = { - noneNodeError: noneNodeErrors[0], - estimatedProfit: ethers.constants.Zero, - }; - } - result.reason = RouteProcessorDryrunHaltReason.NoOpportunity; + if (noRoute) result.reason = RouteProcessorDryrunHaltReason.NoRoute; + else { + const noneNodeErrors = allNoneNodeErrors.filter((v) => !!v); + if (allNoneNodeErrors.length && noneNodeErrors.length / allNoneNodeErrors.length > 0.5) { + result.value = { + noneNodeError: noneNodeErrors[0], + estimatedProfit: ethers.constants.Zero, + }; } - - return Promise.reject(result); + result.reason = RouteProcessorDryrunHaltReason.NoOpportunity; } + + return Promise.reject(result); } /** @@ -499,3 +506,66 @@ export async function findOppWithRetries({ throw result; } } + +/** + * Calculates the largest possible trade size, returns undefined if not possible, + * because price difference is larger to be covered by reducing the trade size + */ +export function findMaxInput({ + orderPairObject, + dataFetcher, + fromToken, + toToken, + maximumInput: maximumInputFixed, + gasPrice, + config, +}: { + config: BotConfig; + orderPairObject: BundledOrders; + dataFetcher: DataFetcher; + gasPrice: bigint; + toToken: Token; + fromToken: Token; + maximumInput: BigNumber; +}): BigNumber | undefined { + const result: BigNumber[] = []; + const ratio = orderPairObject.takeOrders[0].quote!.ratio; + const pcMap = dataFetcher.getCurrentPoolCodeMap(fromToken, toToken); + const initAmount = scale18To(maximumInputFixed, fromToken.decimals).div(2); + let maximumInput = BigNumber.from(initAmount.toString()); + for (let i = 1; i < 26; i++) { + const maxInput18 = scale18(maximumInput, fromToken.decimals); + const maxOutput18 = ratio.mul(maxInput18).div("1" + "0".repeat(18)); + const maximumOutput = scale18To(maxOutput18, toToken.decimals); + + const route = Router.findBestRoute( + pcMap, + config.chain.id as ChainId, + fromToken, + maximumInput.toBigInt(), + toToken, + Number(gasPrice), + undefined, + RPoolFilter, + undefined, + config.route, + ); + if (route.status == "NoWay") { + maximumInput = maximumInput.sub(initAmount.div(2 ** i)); + } else { + const amountOut = scale18(route.amountOutBI, toToken.decimals); + if (amountOut.gte(maximumOutput)) { + result.unshift(maximumInput); + maximumInput = maximumInput.add(initAmount.div(2 ** i)); + } else { + maximumInput = maximumInput.sub(initAmount.div(2 ** i)); + } + } + } + + if (result.length) { + return result[0]; + } else { + return undefined; + } +} diff --git a/src/utils.ts b/src/utils.ts index 19141231..51a58f72 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1282,3 +1282,27 @@ export function getMarketQuote( }; } } + +/** + * Scales a given value and its decimals to 18 fixed point decimals + */ +export function scale18(value: BigNumberish, decimals: BigNumberish): BigNumber { + const d = BigNumber.from(decimals).toNumber(); + if (d > 18) { + return BigNumber.from(value).div("1" + "0".repeat(d - 18)); + } else { + return BigNumber.from(value).mul("1" + "0".repeat(18 - d)); + } +} + +/** + * Scales a given 18 fixed point decimals value to the given decimals point value + */ +export function scale18To(value: BigNumberish, targetDecimals: BigNumberish): BigNumber { + const decimals = BigNumber.from(targetDecimals).toNumber(); + if (decimals > 18) { + return BigNumber.from(value).mul("1" + "0".repeat(decimals - 18)); + } else { + return BigNumber.from(value).div("1" + "0".repeat(18 - decimals)); + } +} diff --git a/test/mode-interOrderbook.test.js b/test/mode-interOrderbook.test.js index bb02ab7d..17d6c5f3 100644 --- a/test/mode-interOrderbook.test.js +++ b/test/mode-interOrderbook.test.js @@ -256,100 +256,6 @@ describe("Test inter-orderbook find opp", async function () { assert.deepEqual(result, expected); }); - it("should find opp with binary search", async function () { - // mock the signer to reject the first attempt on gas estimation - // so the dryrun goes into binary search - let rejectFirst = true; - signer.estimateGas = async () => { - if (rejectFirst) { - rejectFirst = false; - return Promise.reject(ethers.errors.UNPREDICTABLE_GAS_LIMIT); - } else return gasLimitEstimation; - }; - const result = await findOpp({ - orderPairObject, - signer, - gasPrice, - arb, - inputToEthPrice, - outputToEthPrice, - config, - viemClient, - orderbooksOrders, - }); - const opposingMaxInput = vaultBalance - .mul(3) - .div(4) - .mul(orderPairObject.takeOrders[0].quote.ratio) - .div(`1${"0".repeat(36 - orderPairObject.buyTokenDecimals)}`); - const opposingMaxIORatio = ethers.BigNumber.from(`1${"0".repeat(36)}`).div( - orderPairObject.takeOrders[0].quote.ratio, - ); - const obInterface = new ethers.utils.Interface(orderbookAbi); - const encodedFN = obInterface.encodeFunctionData("takeOrders2", [ - { - minimumInput: ethers.constants.One, - maximumInput: opposingMaxInput, - maximumIORatio: opposingMaxIORatio, - orders: opposingOrderPairObject.takeOrders.map((v) => v.takeOrder), - data: "0x", - }, - ]); - const expectedTakeOrdersConfigStruct = { - minimumInput: ethers.constants.One, - maximumInput: vaultBalance.mul(3).div(4), - maximumIORatio: ethers.constants.MaxUint256, - orders: [orderPairObject.takeOrders[0].takeOrder], - data: ethers.utils.defaultAbiCoder.encode( - ["address", "address", "bytes"], - [opposingOrderPairObject.orderbook, opposingOrderPairObject.orderbook, encodedFN], - ), - }; - const task = { - evaluable: { - interpreter: orderPairObject.takeOrders[0].takeOrder.order.evaluable.interpreter, - store: orderPairObject.takeOrders[0].takeOrder.order.evaluable.store, - bytecode: getBountyEnsureBytecode( - ethers.utils.parseUnits(inputToEthPrice), - ethers.utils.parseUnits(outputToEthPrice), - gasLimitEstimation.mul(gasPrice), - ), - }, - signedContext: [], - }; - const expected = { - value: { - rawtx: { - data: arb.interface.encodeFunctionData("arb3", [ - orderPairObject.orderbook, - expectedTakeOrdersConfigStruct, - task, - ]), - to: arb.address, - gasPrice, - gas: gasLimitEstimation.toBigInt(), - }, - maximumInput: vaultBalance.mul(3).div(4), - oppBlockNumber, - estimatedProfit: estimateProfit( - orderPairObject, - ethers.utils.parseUnits(inputToEthPrice), - ethers.utils.parseUnits(outputToEthPrice), - opposingOrderPairObject, - undefined, - vaultBalance.mul(3).div(4), - ), - }, - reason: undefined, - spanAttributes: { - oppBlockNumber, - foundOpp: true, - maxInput: vaultBalance.mul(3).div(4).toString(), - }, - }; - assert.deepEqual(result, expected); - }); - it("should NOT find opp", async function () { const err = ethers.errors.UNPREDICTABLE_GAS_LIMIT; signer.estimateGas = async () => { diff --git a/test/mode-routeProcessor.test.js b/test/mode-routeProcessor.test.js index ca373fd2..65059702 100644 --- a/test/mode-routeProcessor.test.js +++ b/test/mode-routeProcessor.test.js @@ -355,7 +355,7 @@ describe("Test route processor find opp", async function () { }); const expectedTakeOrdersConfigStruct = { minimumInput: ethers.constants.One, - maximumInput: vaultBalance.mul(3).div(4), + maximumInput: ethers.utils.parseUnits("9.999999701976776119"), maximumIORatio: ethers.constants.MaxUint256, orders: [orderPairObject.takeOrders[0].takeOrder], data: expectedRouteData, @@ -384,8 +384,8 @@ describe("Test route processor find opp", async function () { gasPrice, gas: gasLimitEstimation.toBigInt(), }, - maximumInput: vaultBalance.mul(3).div(4), - price: getCurrentPrice(vaultBalance.sub(vaultBalance.div(4))), + maximumInput: ethers.utils.parseUnits("9.999999701976776119"), + price: getCurrentPrice(ethers.utils.parseUnits("9.999999701976776119")), routeVisual: expectedRouteVisual, oppBlockNumber, estimatedProfit: estimateProfit( @@ -393,17 +393,17 @@ describe("Test route processor find opp", async function () { ethers.utils.parseUnits(ethPrice), undefined, undefined, - getCurrentPrice(vaultBalance.mul(3).div(4)), - vaultBalance.mul(3).div(4), + ethers.utils.parseUnits("0.996900529709950975"), + ethers.utils.parseUnits("9.999999701976776119"), ), }, reason: undefined, spanAttributes: { oppBlockNumber, foundOpp: true, - amountIn: formatUnits(vaultBalance.mul(3).div(4)), - amountOut: formatUnits(getAmountOut(vaultBalance.mul(3).div(4)), 6), - marketPrice: formatUnits(getCurrentPrice(vaultBalance.sub(vaultBalance.div(4)))), + amountIn: "9.999999701976776119", + amountOut: "9.969005", + marketPrice: "0.996900529709950975", route: expectedRouteVisual, }, }; @@ -472,8 +472,7 @@ describe("Test route processor find opp", async function () { spanAttributes: { hops: [ `{"amountIn":"${formatUnits(vaultBalance)}","amountOut":"${formatUnits(getAmountOut(vaultBalance), 6)}","marketPrice":"${formatUnits(getCurrentPrice(vaultBalance))}","route":${JSON.stringify(expectedRouteVisual)},"blockNumber":${oppBlockNumber},"stage":1,"isNodeError":false,"error":${JSON.stringify(errorSnapshot("", ethers.errors.UNPREDICTABLE_GAS_LIMIT))},"rawtx":${JSON.stringify(rawtx)}}`, - `{"amountIn":"${formatUnits(vaultBalance.div(2))}","amountOut":"${formatUnits(getAmountOut(vaultBalance.div(2)), 6)}","marketPrice":"${formatUnits(getCurrentPrice(vaultBalance.div(2)))}","route":${JSON.stringify(expectedRouteVisual)},"blockNumber":${oppBlockNumber},"stage":1,"isNodeError":false}`, - `{"amountIn":"${formatUnits(vaultBalance.div(4))}","amountOut":"${formatUnits(getAmountOut(vaultBalance.div(4)), 6)}","marketPrice":"${formatUnits(getCurrentPrice(vaultBalance.div(4)))}","route":${JSON.stringify(expectedRouteVisual)},"blockNumber":${oppBlockNumber},"stage":1,"isNodeError":false}`, + `{"amountIn":"9.999999701976776119","amountOut":"9.969005","marketPrice":"0.996900529709950975","route":${JSON.stringify(expectedRouteVisual)},"blockNumber":${oppBlockNumber},"stage":1,"isNodeError":false}`, ], }, }; @@ -505,11 +504,7 @@ describe("Test route processor find opp", async function () { value: undefined, reason: RouteProcessorDryrunHaltReason.NoRoute, spanAttributes: { - hops: [ - `{"amountIn":"${formatUnits(vaultBalance)}","route":"no-way"}`, - `{"amountIn":"${formatUnits(vaultBalance.div(2))}","route":"no-way"}`, - `{"amountIn":"${formatUnits(vaultBalance.div(4))}","route":"no-way"}`, - ], + hops: [`{"amountIn":"${formatUnits(vaultBalance)}","route":"no-way"}`], }, }; assert.deepEqual(error, expected); @@ -674,8 +669,7 @@ describe("Test find opp with retries", async function () { spanAttributes: { hops: [ `{"amountIn":"${formatUnits(vaultBalance)}","amountOut":"${formatUnits(getAmountOut(vaultBalance), 6)}","marketPrice":"${formatUnits(getCurrentPrice(vaultBalance))}","route":${JSON.stringify(expectedRouteVisual)},"blockNumber":${oppBlockNumber},"stage":1,"isNodeError":false,"error":${JSON.stringify(errorSnapshot("", ethers.errors.UNPREDICTABLE_GAS_LIMIT))},"rawtx":${JSON.stringify(rawtx)}}`, - `{"amountIn":"${formatUnits(vaultBalance.div(2))}","amountOut":"${formatUnits(getAmountOut(vaultBalance.div(2)), 6)}","marketPrice":"${formatUnits(getCurrentPrice(vaultBalance.div(2)))}","route":${JSON.stringify(expectedRouteVisual)},"blockNumber":${oppBlockNumber},"stage":1,"isNodeError":false}`, - `{"amountIn":"${formatUnits(vaultBalance.div(4))}","amountOut":"${formatUnits(getAmountOut(vaultBalance.div(4)), 6)}","marketPrice":"${formatUnits(getCurrentPrice(vaultBalance.div(4)))}","route":${JSON.stringify(expectedRouteVisual)},"blockNumber":${oppBlockNumber},"stage":1,"isNodeError":false}`, + `{"amountIn":"9.999999701976776119","amountOut":"9.969005","marketPrice":"0.996900529709950975","route":${JSON.stringify(expectedRouteVisual)},"blockNumber":${oppBlockNumber},"stage":1,"isNodeError":false}`, ], }, }; diff --git a/test/utils.test.js b/test/utils.test.js index 388b49b3..008d560b 100644 --- a/test/utils.test.js +++ b/test/utils.test.js @@ -1,9 +1,10 @@ const { assert } = require("chai"); const testData = require("./data"); -const { clone, getTotalIncome, checkOwnedOrders } = require("../src/utils"); +const { clone, getTotalIncome, checkOwnedOrders, scale18, scale18To } = require("../src/utils"); const { ethers, utils: { hexlify, randomBytes }, + BigNumber, } = require("ethers"); describe("Test utils functions", async function () { @@ -85,4 +86,36 @@ describe("Test utils functions", async function () { })); assert.deepEqual(result, expected); }); + + it("should scale to 18", async function () { + // down + const value1 = "123456789"; + const decimals1 = 3; + const result1 = scale18(value1, decimals1); + const expected1 = BigNumber.from("123456789000000000000000"); + assert.deepEqual(result1, expected1); + + // up + const value2 = "123456789"; + const decimals2 = 23; + const result2 = scale18(value2, decimals2); + const expected2 = BigNumber.from("1234"); + assert.deepEqual(result2, expected2); + }); + + it("should scale from 18", async function () { + // down + const value1 = "123456789"; + const decimals1 = 12; + const result1 = scale18To(value1, decimals1); + const expected1 = BigNumber.from("123"); + assert.deepEqual(result1, expected1); + + // up + const value2 = "123456789"; + const decimals2 = 23; + const result2 = scale18To(value2, decimals2); + const expected2 = BigNumber.from("12345678900000"); + assert.deepEqual(result2, expected2); + }); }); From 7630d7bc123f1a45293312d023f31549f9b35cf1 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Thu, 21 Nov 2024 02:33:53 +0000 Subject: [PATCH 16/18] update --- src/error.ts | 14 +++++++++++++- src/processOrders.ts | 22 +++++++++++++++++----- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/error.ts b/src/error.ts index 88a8108c..00f5152b 100644 --- a/src/error.ts +++ b/src/error.ts @@ -11,11 +11,12 @@ import { abi as genericArbAbi } from "../test/abis/GenericPoolOrderBookV4ArbOrde import { isHex, BaseError, + TimeoutError, RpcRequestError, + FeeCapTooLowError, decodeErrorResult, ExecutionRevertedError, InsufficientFundsError, - FeeCapTooLowError, // InvalidInputRpcError, // TransactionRejectedRpcError, } from "viem"; @@ -113,6 +114,17 @@ export function containsNodeError(err: BaseError): boolean { } } +/** + * Checks if a viem BaseError is timeout error + */ +export function isTimeout(err: BaseError): boolean { + try { + return err instanceof TimeoutError || ("cause" in err && isTimeout(err.cause as any)); + } catch (error) { + return false; + } +} + /** * Handles a reverted transaction by simulating it and returning the revert error */ diff --git a/src/processOrders.ts b/src/processOrders.ts index 3c3069e5..96f55392 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, handleRevert } from "./error"; +import { containsNodeError, ErrorSeverity, errorSnapshot, handleRevert, isTimeout } from "./error"; import { Report, BotConfig, @@ -318,8 +318,14 @@ export const processOrders = async ( if (e.error) { message = errorSnapshot(message, e.error); span.setAttribute("errorDetails", message); + if (isTimeout(e.error)) { + span.setAttribute("severity", ErrorSeverity.LOW); + } else { + span.setAttribute("severity", ErrorSeverity.HIGH); + } + } else { + span.setAttribute("severity", ErrorSeverity.HIGH); } - span.setAttribute("severity", ErrorSeverity.HIGH); span.setStatus({ code: SpanStatusCode.ERROR, message }); span.setAttribute("unsuccessfulClear", true); span.setAttribute("txSendFailed", true); @@ -343,14 +349,20 @@ export const processOrders = async ( if (e.error) { message = errorSnapshot(message, e.error); span.setAttribute("errorDetails", message); + if (isTimeout(e.error)) { + span.setAttribute("severity", ErrorSeverity.LOW); + } else { + span.setAttribute("severity", ErrorSeverity.HIGH); + } + } else { + span.setAttribute("severity", ErrorSeverity.HIGH); } - span.setAttribute("severity", ErrorSeverity.HIGH); span.setStatus({ code: SpanStatusCode.ERROR, message }); span.setAttribute("unsuccessfulClear", true); span.setAttribute("txMineFailed", true); } else { // record the error for the span - let message = pair + "unexpected error"; + let message = "unexpected error"; if (e.error) { message = errorSnapshot(message, e.error); span.recordException(e.error); @@ -361,7 +373,7 @@ export const processOrders = async ( } } else { // record the error for the span - let message = pair + ": unexpected error"; + let message = "unexpected error"; if (e.error) { message = errorSnapshot(message, e.error); span.recordException(e.error); From 8d35140ffcf36c065f7dc45921c1107959d91167 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Thu, 21 Nov 2024 04:56:06 +0000 Subject: [PATCH 17/18] fix --- src/modes/routeProcessor.ts | 74 ++++++++++++++++++-------------- test/mode-routeProcessor.test.js | 39 ++++++++--------- 2 files changed, 60 insertions(+), 53 deletions(-) diff --git a/src/modes/routeProcessor.ts b/src/modes/routeProcessor.ts index 6c0f7725..48890675 100644 --- a/src/modes/routeProcessor.ts +++ b/src/modes/routeProcessor.ts @@ -48,6 +48,7 @@ export async function dryrun({ ethPrice, config, viemClient, + hasPriceMatch, }: { mode: number; config: BotConfig; @@ -61,6 +62,7 @@ export async function dryrun({ toToken: Token; fromToken: Token; maximumInput: BigNumber; + hasPriceMatch?: { value: boolean }; }) { const spanAttributes: SpanAttrs = {}; const result: DryrunResult = { @@ -112,6 +114,7 @@ export async function dryrun({ // exit early if market price is lower than order quote ratio if (price.lt(orderPairObject.takeOrders[0].quote!.ratio)) { + if (hasPriceMatch) hasPriceMatch.value = false; result.reason = RouteProcessorDryrunHaltReason.NoOpportunity; spanAttributes["error"] = "Order's ratio greater than market price"; return Promise.reject(result); @@ -333,6 +336,9 @@ export async function findOpp({ }; let noRoute = true; + const hasPriceMatch = { + value: true, + }; const initAmount = orderPairObject.takeOrders.reduce( (a, b) => a.add(b.quote!.maxOutput), ethers.constants.Zero, @@ -355,6 +361,7 @@ export async function findOpp({ ethPrice, config, viemClient, + hasPriceMatch, }); } catch (e: any) { // the fail reason can only be no route in case all hops fail reasons are no route @@ -362,38 +369,39 @@ export async function findOpp({ allNoneNodeErrors.push(e?.value?.noneNodeError); allHopsAttributes.push(JSON.stringify(e.spanAttributes)); } - const maxTradeSize = findMaxInput({ - orderPairObject, - dataFetcher, - fromToken, - toToken, - maximumInput, - gasPrice, - config, - }); - if (maxTradeSize) { - try { - return await dryrun({ - mode, - orderPairObject, - dataFetcher, - fromToken, - toToken, - signer, - maximumInput: maxTradeSize, - gasPrice, - arb, - ethPrice, - config, - viemClient, - }); - } catch (e: any) { - // the fail reason can only be no route in case all hops fail reasons are no route - if (e.reason !== RouteProcessorDryrunHaltReason.NoRoute) noRoute = false; - delete e.spanAttributes["error"]; - delete e.spanAttributes["rawtx"]; - allNoneNodeErrors.push(e?.value?.noneNodeError); - allHopsAttributes.push(JSON.stringify(e.spanAttributes)); + if (!hasPriceMatch.value) { + const maxTradeSize = findMaxInput({ + orderPairObject, + dataFetcher, + fromToken, + toToken, + maximumInput, + gasPrice, + config, + }); + if (maxTradeSize) { + try { + return await dryrun({ + mode, + orderPairObject, + dataFetcher, + fromToken, + toToken, + signer, + maximumInput: maxTradeSize, + gasPrice, + arb, + ethPrice, + config, + viemClient, + }); + } catch (e: any) { + // the fail reason can only be no route in case all hops fail reasons are no route + if (e.reason !== RouteProcessorDryrunHaltReason.NoRoute) noRoute = false; + delete e.spanAttributes["rawtx"]; + allNoneNodeErrors.push(e?.value?.noneNodeError); + allHopsAttributes.push(JSON.stringify(e.spanAttributes)); + } } } // in case of no successfull hop, allHopsAttributes will be included @@ -555,7 +563,7 @@ export function findMaxInput({ } else { const amountOut = scale18(route.amountOutBI, toToken.decimals); if (amountOut.gte(maximumOutput)) { - result.unshift(maximumInput); + result.unshift(scale18(maximumInput, fromToken.decimals)); maximumInput = maximumInput.add(initAmount.div(2 ** i)); } else { maximumInput = maximumInput.sub(initAmount.div(2 ** i)); diff --git a/test/mode-routeProcessor.test.js b/test/mode-routeProcessor.test.js index 65059702..8040e646 100644 --- a/test/mode-routeProcessor.test.js +++ b/test/mode-routeProcessor.test.js @@ -1,7 +1,7 @@ const { assert } = require("chai"); const testData = require("./data"); const { errorSnapshot } = require("../src/error"); -const { estimateProfit } = require("../src/utils"); +const { estimateProfit, clone } = require("../src/utils"); const { ethers, utils: { formatUnits }, @@ -331,18 +331,17 @@ describe("Test route processor find opp", async function () { dataFetcher.getCurrentPoolCodeMap = () => { return poolCodeMap; }; - // mock the signer to reject the first attempt on gas estimation - // so the dryrun goes into binary search - let rejectFirst = true; signer.estimateGas = async () => { - if (rejectFirst) { - rejectFirst = false; - return Promise.reject(ethers.errors.UNPREDICTABLE_GAS_LIMIT); - } else return gasLimitEstimation; + return gasLimitEstimation; }; + const orderPairObjectCopy = clone(orderPairObject); + orderPairObjectCopy.takeOrders[0].quote.ratio = ethers.utils.parseUnits("0.009900695135"); + orderPairObjectCopy.takeOrders[0].quote.maxOutput = ethers.BigNumber.from( + "1" + "0".repeat(25), + ); const result = await findOpp({ mode: 0, - orderPairObject, + orderPairObject: orderPairObjectCopy, dataFetcher, fromToken, toToken, @@ -355,7 +354,7 @@ describe("Test route processor find opp", async function () { }); const expectedTakeOrdersConfigStruct = { minimumInput: ethers.constants.One, - maximumInput: ethers.utils.parseUnits("9.999999701976776119"), + maximumInput: ethers.utils.parseUnits("9999999.701976776123046875"), maximumIORatio: ethers.constants.MaxUint256, orders: [orderPairObject.takeOrders[0].takeOrder], data: expectedRouteData, @@ -384,26 +383,28 @@ describe("Test route processor find opp", async function () { gasPrice, gas: gasLimitEstimation.toBigInt(), }, - maximumInput: ethers.utils.parseUnits("9.999999701976776119"), - price: getCurrentPrice(ethers.utils.parseUnits("9.999999701976776119")), + maximumInput: ethers.utils.parseUnits("9999999.701976776123046875"), + price: getCurrentPrice(ethers.utils.parseUnits("9999999.701976776123046875")), routeVisual: expectedRouteVisual, oppBlockNumber, estimatedProfit: estimateProfit( - orderPairObject, + orderPairObjectCopy, ethers.utils.parseUnits(ethPrice), undefined, undefined, - ethers.utils.parseUnits("0.996900529709950975"), - ethers.utils.parseUnits("9.999999701976776119"), + ethers.utils.parseUnits("0.009900695426163716"), + ethers.utils.parseUnits("9999999.701976776123046875"), ), }, reason: undefined, spanAttributes: { oppBlockNumber, foundOpp: true, - amountIn: "9.999999701976776119", - amountOut: "9.969005", - marketPrice: "0.996900529709950975", + amountIn: "9999999.701976776123046875", + amountOut: "99006.951311", + marketPrice: ethers.utils.formatUnits( + getCurrentPrice(ethers.utils.parseUnits("9999999.701976776123046875")), + ), route: expectedRouteVisual, }, }; @@ -472,7 +473,6 @@ describe("Test route processor find opp", async function () { spanAttributes: { hops: [ `{"amountIn":"${formatUnits(vaultBalance)}","amountOut":"${formatUnits(getAmountOut(vaultBalance), 6)}","marketPrice":"${formatUnits(getCurrentPrice(vaultBalance))}","route":${JSON.stringify(expectedRouteVisual)},"blockNumber":${oppBlockNumber},"stage":1,"isNodeError":false,"error":${JSON.stringify(errorSnapshot("", ethers.errors.UNPREDICTABLE_GAS_LIMIT))},"rawtx":${JSON.stringify(rawtx)}}`, - `{"amountIn":"9.999999701976776119","amountOut":"9.969005","marketPrice":"0.996900529709950975","route":${JSON.stringify(expectedRouteVisual)},"blockNumber":${oppBlockNumber},"stage":1,"isNodeError":false}`, ], }, }; @@ -669,7 +669,6 @@ describe("Test find opp with retries", async function () { spanAttributes: { hops: [ `{"amountIn":"${formatUnits(vaultBalance)}","amountOut":"${formatUnits(getAmountOut(vaultBalance), 6)}","marketPrice":"${formatUnits(getCurrentPrice(vaultBalance))}","route":${JSON.stringify(expectedRouteVisual)},"blockNumber":${oppBlockNumber},"stage":1,"isNodeError":false,"error":${JSON.stringify(errorSnapshot("", ethers.errors.UNPREDICTABLE_GAS_LIMIT))},"rawtx":${JSON.stringify(rawtx)}}`, - `{"amountIn":"9.999999701976776119","amountOut":"9.969005","marketPrice":"0.996900529709950975","route":${JSON.stringify(expectedRouteVisual)},"blockNumber":${oppBlockNumber},"stage":1,"isNodeError":false}`, ], }, }; From 9c8a24c2f44c4b06546f5541ea26fd2c4966ea25 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Sat, 23 Nov 2024 01:31:30 +0000 Subject: [PATCH 18/18] fix --- src/modes/routeProcessor.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/modes/routeProcessor.ts b/src/modes/routeProcessor.ts index 48890675..20cd8935 100644 --- a/src/modes/routeProcessor.ts +++ b/src/modes/routeProcessor.ts @@ -543,9 +543,6 @@ export function findMaxInput({ let maximumInput = BigNumber.from(initAmount.toString()); for (let i = 1; i < 26; i++) { const maxInput18 = scale18(maximumInput, fromToken.decimals); - const maxOutput18 = ratio.mul(maxInput18).div("1" + "0".repeat(18)); - const maximumOutput = scale18To(maxOutput18, toToken.decimals); - const route = Router.findBestRoute( pcMap, config.chain.id as ChainId, @@ -558,15 +555,18 @@ export function findMaxInput({ undefined, config.route, ); + if (route.status == "NoWay") { maximumInput = maximumInput.sub(initAmount.div(2 ** i)); } else { const amountOut = scale18(route.amountOutBI, toToken.decimals); - if (amountOut.gte(maximumOutput)) { - result.unshift(scale18(maximumInput, fromToken.decimals)); - maximumInput = maximumInput.add(initAmount.div(2 ** i)); - } else { + const price = amountOut.mul("1" + "0".repeat(18)).div(maxInput18); + + if (price.lt(ratio)) { maximumInput = maximumInput.sub(initAmount.div(2 ** i)); + } else { + result.unshift(maxInput18); + maximumInput = maximumInput.add(initAmount.div(2 ** i)); } } }