From 2a012221949c805b73eb00f343a0723f99898eda Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Tue, 25 Aug 2026 01:35:12 +0000 Subject: [PATCH] init --- src/core/modes/inter/simulate.test.ts | 32 ++++- src/core/modes/inter/simulate.ts | 78 +++++++----- src/core/modes/intra/simulation.test.ts | 29 ++++- src/core/modes/intra/simulation.ts | 84 ++++++------ src/core/modes/raindex/simulation.test.ts | 42 +++++- src/core/modes/raindex/simulation.ts | 71 ++++++----- src/core/modes/router/index.test.ts | 3 + src/core/modes/router/index.ts | 1 + src/core/modes/router/simulate.test.ts | 25 +++- src/core/modes/router/simulate.ts | 71 ++++++----- src/core/modes/simulator.test.ts | 2 + src/core/modes/simulator.ts | 6 +- src/router/sushi/index.test.ts | 148 +--------------------- src/router/sushi/index.ts | 40 ++---- 14 files changed, 314 insertions(+), 318 deletions(-) diff --git a/src/core/modes/inter/simulate.test.ts b/src/core/modes/inter/simulate.test.ts index ddd80ea3..5ca77dab 100644 --- a/src/core/modes/inter/simulate.test.ts +++ b/src/core/modes/inter/simulate.test.ts @@ -7,7 +7,14 @@ import { RainSolverSigner } from "../../../signer"; import { SimulationHaltReason } from "../simulator"; import { ABI, Dispair, Result } from "../../../common"; import { describe, it, expect, vi, beforeEach, Mock, assert } from "vitest"; -import { encodeAbiParameters, encodeFunctionData, formatUnits, maxUint256, parseUnits } from "viem"; +import { + maxUint256, + parseUnits, + formatUnits, + zeroAddress, + encodeFunctionData, + encodeAbiParameters, +} from "viem"; import { InterOrderbookTradeSimulator, SimulateInterOrderbookTradeArgs, @@ -326,6 +333,29 @@ describe("Test InterOrderbookTradeSimulator", () => { getCalldataSpy.mockRestore(); }); + + it("should use empty task when noTask is set", async () => { + const getCalldataSpy = vi.spyOn(simulator, "getCalldata"); + getCalldataSpy.mockReturnValue("0xencodedData"); + + const result = await simulator.setTransactionData({ + ...preparedParams, + noTask: true, + }); + assert(result.isOk()); + expect(preparedParams.rawtx.data).toBe("0xencodedData"); + expect(getEnsureBountyTaskBytecode).not.toHaveBeenCalled(); + expect(getCalldataSpy).toHaveBeenCalledWith(preparedParams.takeOrdersConfigStruct, { + evaluable: { + interpreter: zeroAddress, + store: zeroAddress, + bytecode: "0x", + }, + signedContext: [], + }); + + getCalldataSpy.mockRestore(); + }); }); describe("Test estimateProfit method", () => { diff --git a/src/core/modes/inter/simulate.ts b/src/core/modes/inter/simulate.ts index d8494193..ef0460b8 100644 --- a/src/core/modes/inter/simulate.ts +++ b/src/core/modes/inter/simulate.ts @@ -6,7 +6,14 @@ import { WasmEncodedError } from "@rainlanguage/float"; import { TradeType, FailedSimulation, TaskType } from "../../types"; import { SimulationHaltReason, TradeSimulatorBase } from "../simulator"; import { Result, ABI, RawTransaction, maxFloat, toFloat, minFloat } from "../../../common"; -import { encodeAbiParameters, encodeFunctionData, formatUnits, maxUint256, parseUnits } from "viem"; +import { + maxUint256, + parseUnits, + formatUnits, + zeroAddress, + encodeFunctionData, + encodeAbiParameters, +} from "viem"; import { EnsureBountyTaskType, EnsureBountyTaskErrorType, @@ -51,6 +58,8 @@ export type InterOrderbookTradePreparedParams = { takeOrdersConfigStruct: TakeOrdersConfigType; minimumExpected: bigint; price?: bigint; + /** If set, builds the tx data with an empty task */ + noTask?: boolean; }; /** @@ -166,39 +175,46 @@ export class InterOrderbookTradeSimulator extends TradeSimulatorBase { params.type, )!; - // try to get task bytecode for ensure bounty task - const taskBytecodeResult = await getEnsureBountyTaskBytecode( - { - type: EnsureBountyTaskType.External, - inputToEthPrice: parseUnits(this.tradeArgs.inputToEthPrice, 18), - outputToEthPrice: parseUnits(this.tradeArgs.outputToEthPrice, 18), - minimumExpected: params.minimumExpected, - sender: this.tradeArgs.signer.account.address, - }, - this.tradeArgs.solver.state.client, - addresses.dispair, - ); - if (taskBytecodeResult.isErr()) { - const errMsg = await errorSnapshot("", taskBytecodeResult.error); - this.spanAttributes["isNodeError"] = - taskBytecodeResult.error.type === EnsureBountyTaskErrorType.ParseError; - this.spanAttributes["error"] = errMsg; - const result = { - type: TradeType.InterOrderbook, - spanAttributes: this.spanAttributes, - reason: SimulationHaltReason.FailedToGetTaskBytecode, - }; - this.spanAttributes["duration"] = performance.now() - this.startTime; - return Result.err(result); + // build the ensure bounty task bytecode, unless an empty task is + // explicitly requested or gas coverage is 0, in which cases the tx + // wont need onchain bounty assurance + let bytecode: `0x${string}` = "0x"; + let interpreter: `0x${string}` = zeroAddress; + let store: `0x${string}` = zeroAddress; + if (!params.noTask && this.tradeArgs.solver.appOptions.gasCoveragePercentage !== "0") { + const taskBytecodeResult = await getEnsureBountyTaskBytecode( + { + type: EnsureBountyTaskType.External, + inputToEthPrice: parseUnits(this.tradeArgs.inputToEthPrice, 18), + outputToEthPrice: parseUnits(this.tradeArgs.outputToEthPrice, 18), + minimumExpected: params.minimumExpected, + sender: this.tradeArgs.signer.account.address, + }, + this.tradeArgs.solver.state.client, + addresses.dispair, + ); + if (taskBytecodeResult.isErr()) { + const errMsg = await errorSnapshot("", taskBytecodeResult.error); + this.spanAttributes["isNodeError"] = + taskBytecodeResult.error.type === EnsureBountyTaskErrorType.ParseError; + this.spanAttributes["error"] = errMsg; + const result = { + type: TradeType.InterOrderbook, + spanAttributes: this.spanAttributes, + reason: SimulationHaltReason.FailedToGetTaskBytecode, + }; + this.spanAttributes["duration"] = performance.now() - this.startTime; + return Result.err(result); + } + bytecode = taskBytecodeResult.value; + interpreter = addresses.dispair.interpreter as `0x${string}`; + store = addresses.dispair.store as `0x${string}`; } const task = { evaluable: { - interpreter: addresses.dispair.interpreter as `0x${string}`, - store: addresses.dispair.store as `0x${string}`, - bytecode: - this.tradeArgs.solver.appOptions.gasCoveragePercentage === "0" - ? "0x" - : taskBytecodeResult.value, + interpreter, + store, + bytecode, }, signedContext: [], }; diff --git a/src/core/modes/intra/simulation.test.ts b/src/core/modes/intra/simulation.test.ts index 0f529363..30819e00 100644 --- a/src/core/modes/intra/simulation.test.ts +++ b/src/core/modes/intra/simulation.test.ts @@ -5,7 +5,7 @@ import { RainSolverSigner } from "../../../signer"; import { SimulationHaltReason } from "../simulator"; import { Order, Pair, TakeOrderDetails } from "../../../order"; import { ABI, Dispair, maxFloat, Result } from "../../../common"; -import { encodeFunctionData, formatUnits, maxUint256, parseUnits } from "viem"; +import { encodeFunctionData, formatUnits, maxUint256, parseUnits, zeroAddress } from "viem"; import { describe, it, expect, vi, beforeEach, Mock, assert } from "vitest"; import { IntraOrderbookTradeSimulator, @@ -264,6 +264,29 @@ describe("Test IntraOrderbookTradeSimulator", () => { TradeType.IntraOrderbook, ); }); + + it("should use empty task when noTask is set", async () => { + const getCalldataSpy = vi.spyOn(simulator, "getCalldata"); + getCalldataSpy.mockReturnValue("0xencodedData"); + + const result = await simulator.setTransactionData({ + ...preparedParams, + noTask: true, + }); + assert(result.isOk()); + expect(preparedParams.rawtx.data).toBe("0xencodedData"); + expect(getEnsureBountyTaskBytecode).not.toHaveBeenCalled(); + expect(getCalldataSpy).toHaveBeenCalledWith({ + evaluable: { + interpreter: zeroAddress, + store: zeroAddress, + bytecode: "0x", + }, + signedContext: [], + }); + + getCalldataSpy.mockRestore(); + }); }); describe("Test estimateProfit method", () => { @@ -550,7 +573,7 @@ describe("Test IntraOrderbookTradeSimulator", () => { .mockReturnValueOnce("0xencodedData2") .mockReturnValueOnce("0xencodedData3") .mockReturnValueOnce("0xmulticallData"); - const task = { task: "task-value" } as any; + const task = { task: "task-value", evaluable: { bytecode: "0xbytecode" } } as any; const result = simulator.getCalldataForV3Order(task); expect(result).toBe("0xmulticallData"); expect(encodeFunctionData).toHaveBeenCalledTimes(4); @@ -619,7 +642,7 @@ describe("Test IntraOrderbookTradeSimulator", () => { .mockReturnValueOnce("0xencodedData2") .mockReturnValueOnce("0xencodedData3") .mockReturnValueOnce("0xmulticallData"); - const task = { task: "task-value" } as any; + const task = { task: "task-value", evaluable: { bytecode: "0xbytecode" } } as any; const result = simulator.getCalldataForV4Order(task); expect(result).toBe("0xmulticallData"); expect(encodeFunctionData).toHaveBeenCalledTimes(4); diff --git a/src/core/modes/intra/simulation.ts b/src/core/modes/intra/simulation.ts index 871db019..d8475e29 100644 --- a/src/core/modes/intra/simulation.ts +++ b/src/core/modes/intra/simulation.ts @@ -6,7 +6,7 @@ import { Pair, TakeOrderDetails } from "../../../order"; import { TradeType, FailedSimulation, TaskType } from "../../types"; import { Result, ABI, RawTransaction, maxFloat } from "../../../common"; import { SimulationHaltReason, TradeSimulatorBase } from "../simulator"; -import { encodeFunctionData, formatUnits, maxUint256, parseUnits } from "viem"; +import { encodeFunctionData, formatUnits, maxUint256, parseUnits, zeroAddress } from "viem"; import { EnsureBountyTaskType, EnsureBountyTaskErrorType, @@ -43,6 +43,8 @@ export type IntraOrderbookTradePrepareedParams = { rawtx: RawTransaction; minimumExpected: bigint; price?: bigint; + /** If set, builds the tx data with an empty task */ + noTask?: boolean; }; /** @@ -124,41 +126,51 @@ export class IntraOrderbookTradeSimulator extends TradeSimulatorBase { params.type, )!; - // build clear function call data and withdraw tasks - const taskBytecodeResult = await getEnsureBountyTaskBytecode( - { - type: EnsureBountyTaskType.Internal, - botAddress: this.tradeArgs.signer.account.address, - inputToken: this.tradeArgs.orderDetails.buyToken, - outputToken: this.tradeArgs.orderDetails.sellToken, - orgInputBalance: this.tradeArgs.inputBalance, - orgOutputBalance: this.tradeArgs.outputBalance, - inputToEthPrice: parseUnits(this.tradeArgs.inputToEthPrice, 18), - outputToEthPrice: parseUnits(this.tradeArgs.outputToEthPrice, 18), - minimumExpected: params.minimumExpected, - sender: this.tradeArgs.signer.account.address, - }, - this.tradeArgs.solver.state.client, - addresses.dispair, - ); - if (taskBytecodeResult.isErr()) { - const errMsg = await errorSnapshot("", taskBytecodeResult.error); - this.spanAttributes["isNodeError"] = - taskBytecodeResult.error.type === EnsureBountyTaskErrorType.ParseError; - this.spanAttributes["error"] = errMsg; - const result = { - type: TradeType.IntraOrderbook, - spanAttributes: this.spanAttributes, - reason: SimulationHaltReason.FailedToGetTaskBytecode, - }; - this.spanAttributes["duration"] = performance.now() - this.startTime; - return Result.err(result); + // build the ensure bounty task bytecode for the withdraw tasks, unless + // an empty task is explicitly requested or gas coverage is 0, in which + // cases the tx wont need onchain bounty assurance + let bytecode: `0x${string}` = "0x"; + let interpreter: `0x${string}` = zeroAddress; + let store: `0x${string}` = zeroAddress; + if (!params.noTask && this.tradeArgs.solver.appOptions.gasCoveragePercentage !== "0") { + const taskBytecodeResult = await getEnsureBountyTaskBytecode( + { + type: EnsureBountyTaskType.Internal, + botAddress: this.tradeArgs.signer.account.address, + inputToken: this.tradeArgs.orderDetails.buyToken, + outputToken: this.tradeArgs.orderDetails.sellToken, + orgInputBalance: this.tradeArgs.inputBalance, + orgOutputBalance: this.tradeArgs.outputBalance, + inputToEthPrice: parseUnits(this.tradeArgs.inputToEthPrice, 18), + outputToEthPrice: parseUnits(this.tradeArgs.outputToEthPrice, 18), + minimumExpected: params.minimumExpected, + sender: this.tradeArgs.signer.account.address, + }, + this.tradeArgs.solver.state.client, + addresses.dispair, + ); + if (taskBytecodeResult.isErr()) { + const errMsg = await errorSnapshot("", taskBytecodeResult.error); + this.spanAttributes["isNodeError"] = + taskBytecodeResult.error.type === EnsureBountyTaskErrorType.ParseError; + this.spanAttributes["error"] = errMsg; + const result = { + type: TradeType.IntraOrderbook, + spanAttributes: this.spanAttributes, + reason: SimulationHaltReason.FailedToGetTaskBytecode, + }; + this.spanAttributes["duration"] = performance.now() - this.startTime; + return Result.err(result); + } + bytecode = taskBytecodeResult.value; + interpreter = addresses.dispair.interpreter as `0x${string}`; + store = addresses.dispair.store as `0x${string}`; } const task = { evaluable: { - interpreter: addresses.dispair.interpreter as `0x${string}`, - store: addresses.dispair.store as `0x${string}`, - bytecode: taskBytecodeResult.value, + interpreter, + store, + bytecode, }, signedContext: [], }; @@ -228,7 +240,7 @@ export class IntraOrderbookTradeSimulator extends TradeSimulatorBase { this.tradeArgs.orderDetails.sellToken, BigInt(this.outputBountyVaultId), maxUint256, - this.tradeArgs.solver.appOptions.gasCoveragePercentage === "0" ? [] : [task], + task.evaluable.bytecode === "0x" ? [] : [task], ], }); const clear2Calldata = encodeFunctionData({ @@ -286,7 +298,7 @@ export class IntraOrderbookTradeSimulator extends TradeSimulatorBase { this.tradeArgs.orderDetails.sellToken, this.outputBountyVaultId, maxFloat(this.tradeArgs.orderDetails.sellTokenDecimals), - this.tradeArgs.solver.appOptions.gasCoveragePercentage === "0" ? [] : [task], + task.evaluable.bytecode === "0x" ? [] : [task], ], }); const clear3Calldata = encodeFunctionData({ @@ -344,7 +356,7 @@ export class IntraOrderbookTradeSimulator extends TradeSimulatorBase { this.tradeArgs.orderDetails.sellToken, this.outputBountyVaultId, maxFloat(this.tradeArgs.orderDetails.sellTokenDecimals), - this.tradeArgs.solver.appOptions.gasCoveragePercentage === "0" ? [] : [task], + task.evaluable.bytecode === "0x" ? [] : [task], ], }); const clear2Calldata = encodeFunctionData({ diff --git a/src/core/modes/raindex/simulation.test.ts b/src/core/modes/raindex/simulation.test.ts index e7a04a66..cfdb1a46 100644 --- a/src/core/modes/raindex/simulation.test.ts +++ b/src/core/modes/raindex/simulation.test.ts @@ -5,7 +5,13 @@ import { RainSolverSigner } from "../../../signer"; import { SimulationHaltReason } from "../simulator"; import { ABI, Dispair, maxFloat, minFloat, Result } from "../../../common"; import { describe, it, expect, vi, beforeEach, Mock, assert } from "vitest"; -import { encodeAbiParameters, encodeFunctionData, formatUnits, parseUnits } from "viem"; +import { + encodeAbiParameters, + encodeFunctionData, + formatUnits, + parseUnits, + zeroAddress, +} from "viem"; import { RaindexRouterTradeSimulator, SimulateRaindexRouterTradeArgs, @@ -364,21 +370,47 @@ describe("Test RaindexRouterTradeSimulator", () => { getCalldataSpy.mockRestore(); }); - it("should use empty bytecode when gasCoveragePercentage is zero", async () => { - (getEnsureBountyTaskBytecode as Mock).mockResolvedValueOnce(Result.ok("0xtaskdata")); + it("should use empty task when gasCoveragePercentage is zero", async () => { tradeArgs.solver.appOptions.gasCoveragePercentage = "0"; const getCalldataSpy = vi.spyOn(simulator, "getCalldata"); getCalldataSpy.mockReturnValue("0xencodedCalldata"); const result = await simulator.setTransactionData(preparedParams); assert(result.isOk()); + expect(getEnsureBountyTaskBytecode).not.toHaveBeenCalled(); expect(getCalldataSpy).toHaveBeenCalledWith( preparedParams.takeOrders, preparedParams.exchangeData, { evaluable: { - interpreter: dispair.interpreter as `0x${string}`, - store: dispair.store as `0x${string}`, + interpreter: zeroAddress, + store: zeroAddress, + bytecode: "0x", + }, + signedContext: [], + }, + ); + + getCalldataSpy.mockRestore(); + }); + + it("should use empty task when noTask is set", async () => { + const getCalldataSpy = vi.spyOn(simulator, "getCalldata"); + getCalldataSpy.mockReturnValue("0xencodedCalldata"); + + const result = await simulator.setTransactionData({ + ...preparedParams, + noTask: true, + }); + assert(result.isOk()); + expect(getEnsureBountyTaskBytecode).not.toHaveBeenCalled(); + expect(getCalldataSpy).toHaveBeenCalledWith( + preparedParams.takeOrders, + preparedParams.exchangeData, + { + evaluable: { + interpreter: zeroAddress, + store: zeroAddress, bytecode: "0x", }, signedContext: [], diff --git a/src/core/modes/raindex/simulation.ts b/src/core/modes/raindex/simulation.ts index 9b577525..116f0bbe 100644 --- a/src/core/modes/raindex/simulation.ts +++ b/src/core/modes/raindex/simulation.ts @@ -8,7 +8,7 @@ import { SushiRouterQuote } from "../../../router"; import { PairV4, TakeOrdersConfigTypeV5 } from "../../../order"; import { TradeType, FailedSimulation, TaskType } from "../../types"; import { SimulationHaltReason, TradeSimulatorBase } from "../simulator"; -import { encodeAbiParameters, encodeFunctionData, formatUnits } from "viem"; +import { encodeAbiParameters, encodeFunctionData, formatUnits, zeroAddress } from "viem"; import { Result, ABI, RawTransaction, maxFloat, minFloat } from "../../../common"; import { EnsureBountyTaskType, @@ -54,6 +54,8 @@ export type RaindexRouterTradePreparedParams = { minimumExpected: bigint; exchangeData: `0x${string}`; price?: bigint; + /** If set, builds the tx data with an empty task */ + noTask?: boolean; }; /** @@ -191,39 +193,46 @@ export class RaindexRouterTradeSimulator extends TradeSimulatorBase { params.type, )!; - // try to get task bytecode for ensure bounty task - const taskBytecodeResult = await getEnsureBountyTaskBytecode( - { - type: EnsureBountyTaskType.External, - inputToEthPrice: this.tradeArgs.counterpartyInputToEthPrice, - outputToEthPrice: this.tradeArgs.counterpartyOutputToEthPrice, - minimumExpected: params.minimumExpected, - sender: this.tradeArgs.signer.account.address, - }, - this.tradeArgs.solver.state.client, - addresses.dispair, - ); - if (taskBytecodeResult.isErr()) { - const errMsg = await errorSnapshot("", taskBytecodeResult.error); - this.spanAttributes["isNodeError"] = - taskBytecodeResult.error.type === EnsureBountyTaskErrorType.ParseError; - this.spanAttributes["error"] = errMsg; - const result = { - type: TradeType.Raindex, - spanAttributes: this.spanAttributes, - reason: SimulationHaltReason.FailedToGetTaskBytecode, - }; - this.spanAttributes["duration"] = performance.now() - this.startTime; - return Result.err(result); + // build the ensure bounty task bytecode, unless an empty task is + // explicitly requested or gas coverage is 0, in which cases the tx + // wont need onchain bounty assurance + let bytecode: `0x${string}` = "0x"; + let interpreter: `0x${string}` = zeroAddress; + let store: `0x${string}` = zeroAddress; + if (!params.noTask && this.tradeArgs.solver.appOptions.gasCoveragePercentage !== "0") { + const taskBytecodeResult = await getEnsureBountyTaskBytecode( + { + type: EnsureBountyTaskType.External, + inputToEthPrice: this.tradeArgs.counterpartyInputToEthPrice, + outputToEthPrice: this.tradeArgs.counterpartyOutputToEthPrice, + minimumExpected: params.minimumExpected, + sender: this.tradeArgs.signer.account.address, + }, + this.tradeArgs.solver.state.client, + addresses.dispair, + ); + if (taskBytecodeResult.isErr()) { + const errMsg = await errorSnapshot("", taskBytecodeResult.error); + this.spanAttributes["isNodeError"] = + taskBytecodeResult.error.type === EnsureBountyTaskErrorType.ParseError; + this.spanAttributes["error"] = errMsg; + const result = { + type: TradeType.Raindex, + spanAttributes: this.spanAttributes, + reason: SimulationHaltReason.FailedToGetTaskBytecode, + }; + this.spanAttributes["duration"] = performance.now() - this.startTime; + return Result.err(result); + } + bytecode = taskBytecodeResult.value; + interpreter = addresses.dispair.interpreter as `0x${string}`; + store = addresses.dispair.store as `0x${string}`; } const task = { evaluable: { - interpreter: addresses.dispair.interpreter as `0x${string}`, - store: addresses.dispair.store as `0x${string}`, - bytecode: - this.tradeArgs.solver.appOptions.gasCoveragePercentage === "0" - ? "0x" - : taskBytecodeResult.value, + interpreter, + store, + bytecode, }, signedContext: [], }; diff --git a/src/core/modes/router/index.test.ts b/src/core/modes/router/index.test.ts index a8925a08..549c52af 100644 --- a/src/core/modes/router/index.test.ts +++ b/src/core/modes/router/index.test.ts @@ -280,6 +280,9 @@ describe("Test findBestRouterTrade", () => { assert(result.isErr()); expect(result.error.noneNodeError).toBe("order ratio issue"); expect(result.error.type).toBe("router"); + expect(result.error.spanAttributes["partial.error"]).toBe( + "no viable partial trade size found", + ); expect(extendObjectWithHeader).toHaveBeenCalledWith( expect.any(Object), { error: "ratio too high" }, diff --git a/src/core/modes/router/index.ts b/src/core/modes/router/index.ts index f0446eae..4e381bcd 100644 --- a/src/core/modes/router/index.ts +++ b/src/core/modes/router/index.ts @@ -99,6 +99,7 @@ export async function findBestRouterTrade( this.appOptions.route, ); if (!partialTradeSize) { + spanAttributes["partial.error"] = "no viable partial trade size found"; return Result.err({ type: fullTradeSizeSimResult.error.type, spanAttributes, diff --git a/src/core/modes/router/simulate.test.ts b/src/core/modes/router/simulate.test.ts index dcf25b9d..a62275ff 100644 --- a/src/core/modes/router/simulate.test.ts +++ b/src/core/modes/router/simulate.test.ts @@ -5,7 +5,7 @@ import { ONE18, scaleFrom18 } from "../../../math"; import { RainSolverSigner } from "../../../signer"; import { SimulationHaltReason } from "../simulator"; import { ABI, Dispair, Result } from "../../../common"; -import { encodeFunctionData, formatUnits, parseUnits } from "viem"; +import { encodeFunctionData, formatUnits, parseUnits, zeroAddress } from "viem"; import { describe, it, expect, vi, beforeEach, Mock, assert } from "vitest"; import { RainSolverRouterError, RainSolverRouterErrorType, RouterType } from "../../../router"; import { @@ -446,6 +446,29 @@ describe("Test RouterTradeSimulator", () => { getCalldataSpy.mockRestore(); }); + + it("should use empty task when noTask is set", async () => { + const getCalldataSpy = vi.spyOn(simulator, "getCalldata"); + getCalldataSpy.mockReturnValue("0xencodedData"); + + const result = await simulator.setTransactionData({ + ...preparedParams, + noTask: true, + }); + assert(result.isOk()); + expect(preparedParams.rawtx.data).toBe("0xencodedData"); + expect(getEnsureBountyTaskBytecode).not.toHaveBeenCalled(); + expect(getCalldataSpy).toHaveBeenCalledWith(preparedParams.takeOrdersConfigStruct, { + evaluable: { + interpreter: zeroAddress, + store: zeroAddress, + bytecode: "0x", + }, + signedContext: [], + }); + + getCalldataSpy.mockRestore(); + }); }); describe("Test estimateProfit method", () => { diff --git a/src/core/modes/router/simulate.ts b/src/core/modes/router/simulate.ts index 752b4fbc..2460ad13 100644 --- a/src/core/modes/router/simulate.ts +++ b/src/core/modes/router/simulate.ts @@ -5,7 +5,7 @@ import { ONE18, scaleFrom18 } from "../../../math"; import { RainSolverSigner } from "../../../signer"; import { Pair, TakeOrdersConfigType } from "../../../order"; import { Result, ABI, RawTransaction } from "../../../common"; -import { encodeFunctionData, formatUnits, parseUnits } from "viem"; +import { encodeFunctionData, formatUnits, parseUnits, zeroAddress } from "viem"; import { TradeType, FailedSimulation, TaskType } from "../../types"; import { SimulationHaltReason, TradeSimulatorBase } from "../simulator"; import { RainSolverRouterErrorType, RouterType } from "../../../router"; @@ -46,6 +46,8 @@ export type RouterTradePreparedParams = { price: bigint; minimumExpected: bigint; takeOrdersConfigStruct: TakeOrdersConfigType; + /** If set, builds the tx data with an empty task */ + noTask?: boolean; }; /** @@ -189,39 +191,46 @@ export class RouterTradeSimulator extends TradeSimulatorBase { params.type, )!; - // try to get task bytecode for ensure bounty task - const taskBytecodeResult = await getEnsureBountyTaskBytecode( - { - type: EnsureBountyTaskType.External, - inputToEthPrice: parseUnits(this.tradeArgs.ethPrice, 18), - outputToEthPrice: 0n, - minimumExpected: params.minimumExpected, - sender: this.tradeArgs.signer.account.address, - }, - this.tradeArgs.solver.state.client, - addresses.dispair, - ); - if (taskBytecodeResult.isErr()) { - const errMsg = await errorSnapshot("", taskBytecodeResult.error); - this.spanAttributes["isNodeError"] = - taskBytecodeResult.error.type === EnsureBountyTaskErrorType.ParseError; - this.spanAttributes["error"] = errMsg; - const result = { - type: params.type, - spanAttributes: this.spanAttributes, - reason: SimulationHaltReason.FailedToGetTaskBytecode, - }; - this.spanAttributes["duration"] = performance.now() - this.startTime; - return Result.err(result); + // build the ensure bounty task bytecode, unless an empty task is + // explicitly requested or gas coverage is 0, in which cases the tx + // wont need onchain bounty assurance + let bytecode: `0x${string}` = "0x"; + let interpreter: `0x${string}` = zeroAddress; + let store: `0x${string}` = zeroAddress; + if (!params.noTask && this.tradeArgs.solver.appOptions.gasCoveragePercentage !== "0") { + const taskBytecodeResult = await getEnsureBountyTaskBytecode( + { + type: EnsureBountyTaskType.External, + inputToEthPrice: parseUnits(this.tradeArgs.ethPrice, 18), + outputToEthPrice: 0n, + minimumExpected: params.minimumExpected, + sender: this.tradeArgs.signer.account.address, + }, + this.tradeArgs.solver.state.client, + addresses.dispair, + ); + if (taskBytecodeResult.isErr()) { + const errMsg = await errorSnapshot("", taskBytecodeResult.error); + this.spanAttributes["isNodeError"] = + taskBytecodeResult.error.type === EnsureBountyTaskErrorType.ParseError; + this.spanAttributes["error"] = errMsg; + const result = { + type: params.type, + spanAttributes: this.spanAttributes, + reason: SimulationHaltReason.FailedToGetTaskBytecode, + }; + this.spanAttributes["duration"] = performance.now() - this.startTime; + return Result.err(result); + } + bytecode = taskBytecodeResult.value; + interpreter = addresses.dispair.interpreter as `0x${string}`; + store = addresses.dispair.store as `0x${string}`; } const task = { evaluable: { - interpreter: addresses.dispair.interpreter, - store: addresses.dispair.store, - bytecode: - this.tradeArgs.solver.appOptions.gasCoveragePercentage === "0" - ? "0x" - : taskBytecodeResult.value, + interpreter, + store, + bytecode, }, signedContext: [], }; diff --git a/src/core/modes/simulator.test.ts b/src/core/modes/simulator.test.ts index f90e502f..99f0841d 100644 --- a/src/core/modes/simulator.test.ts +++ b/src/core/modes/simulator.test.ts @@ -454,6 +454,7 @@ describe("Test TradeSimulatorBase", () => { (dryrunResult2.estimatedGasCost * BigInt(mockSolver.appOptions.gasCoveragePercentage)) / 100n, + noTask: true, }); expect(dryrun).toHaveBeenCalledTimes(2); expect(dryrun).toHaveBeenCalledWith( @@ -568,6 +569,7 @@ describe("Test TradeSimulatorBase", () => { (dryrunResult2.estimatedGasCost * BigInt(mockSolver.appOptions.gasCoveragePercentage)) / 100n, + noTask: true, }); expect(dryrun).toHaveBeenCalledTimes(2); expect(dryrun).toHaveBeenCalledWith( diff --git a/src/core/modes/simulator.ts b/src/core/modes/simulator.ts index 0a3914f1..e1e9fa80 100644 --- a/src/core/modes/simulator.ts +++ b/src/core/modes/simulator.ts @@ -206,13 +206,17 @@ export abstract class TradeSimulatorBase { "gasEst.final", ); - // update the tx data again with the new min sender output + // update the tx data again, this time with an empty task, as the + // profitability of the trade was already validated by the dryrun + // above with headroom, so the actual submitting tx doesnt need to + // carry the ensure bounty task anymore minimumExpected = (estimatedGasCost * BigInt(this.tradeArgs.solver.appOptions.gasCoveragePercentage)) / 100n; setTransactionDataResult = await this.setTransactionData({ ...prepareParamsResult.value, minimumExpected, + noTask: true, }); if (setTransactionDataResult.isErr()) { return Result.err(setTransactionDataResult.error); diff --git a/src/router/sushi/index.test.ts b/src/router/sushi/index.test.ts index 76b6ce62..f35ca91b 100644 --- a/src/router/sushi/index.test.ts +++ b/src/router/sushi/index.test.ts @@ -2,10 +2,9 @@ import { ONE18 } from "../../math"; import { Token } from "sushi/currency"; import { SharedState } from "../../state"; import { Dispair, Result } from "../../common"; -import { calculateEffectivePrice } from "./index"; import { RouterType, RouteStatus } from "../types"; -import { RouteLeg, MultiRoute } from "sushi/tines"; -import { maxUint256, PublicClient, parseUnits } from "viem"; +import { RouteLeg } from "sushi/tines"; +import { maxUint256, PublicClient } from "viem"; import { SushiRouterError, SushiRouterErrorType } from "./error"; import { LiquidityProviders, RainDataFetcher, Router } from "sushi"; import { describe, it, expect, vi, beforeEach, Mock, assert } from "vitest"; @@ -1022,146 +1021,3 @@ describe("test SushiRouter methods", () => { }); }); }); - -describe("calculateEffectivePrice", () => { - const mockFromToken = new Token({ - address: "0x1111111111111111111111111111111111111111", - decimals: 18, - symbol: "TOKEN1", - chainId: 1, - name: "Token 1", - }); - - const mockToToken = new Token({ - address: "0x2222222222222222222222222222222222222222", - decimals: 6, - symbol: "TOKEN2", - chainId: 1, - name: "Token 2", - }); - - it("should return the base price when priceImpact is undefined", () => { - const maximumInput = parseUnits("100", 18); - const route: MultiRoute = { - status: RouteStatus.Success, - amountOutBI: parseUnits("200", 6), - priceImpact: undefined, - } as any as MultiRoute; - - const result = calculateEffectivePrice(maximumInput, route, mockFromToken, mockToToken); - - // Expected price: (200 * 10^6 * 10^18) / (100 * 10^18) = 2 * 10^6 = 2000000 - // Scaled to 18 decimals: 2 * 10^18 - expect(result).toBe(parseUnits("2", 18)); - }); - - it("should apply price impact when priceImpact is defined", () => { - const maximumInput = parseUnits("100", 18); - const route: MultiRoute = { - status: RouteStatus.Success, - amountOutBI: parseUnits("200", 6), - priceImpact: 0.05, // 5% price impact - } as any as MultiRoute; - - const result = calculateEffectivePrice(maximumInput, route, mockFromToken, mockToToken); - - // Base price: 2 * 10^18 - // With 5% impact: 2 * 0.95 = 1.9 * 10^18 - expect(result).toBe(parseUnits("1.9", 18)); - }); - - it("should handle zero price impact", () => { - const maximumInput = parseUnits("100", 18); - const route: MultiRoute = { - status: RouteStatus.Success, - amountOutBI: parseUnits("150", 6), - priceImpact: 0, - } as any as MultiRoute; - - const result = calculateEffectivePrice(maximumInput, route, mockFromToken, mockToToken); - - // Price: 1.5 * 10^18, no impact - expect(result).toBe(parseUnits("1.5", 18)); - }); - - it("should handle small price impact correctly", () => { - const maximumInput = parseUnits("1000", 18); - const route: MultiRoute = { - status: RouteStatus.Success, - amountOutBI: parseUnits("1000", 6), - priceImpact: 0.001, // 0.1% price impact - } as any as MultiRoute; - - const result = calculateEffectivePrice(maximumInput, route, mockFromToken, mockToToken); - - // Base price: 1 * 10^18 - // With 0.1% impact: 1 * 0.999 = 0.999 * 10^18 - expect(result).toBe(parseUnits("0.999", 18)); - }); - - it("should handle very small amounts", () => { - const maximumInput = parseUnits("0.001", 18); // 1e-3 - const route: MultiRoute = { - status: RouteStatus.Success, - amountOutBI: parseUnits("0.002", 6), // 2e-3 - priceImpact: 0.1, // 10% price impact - } as any as MultiRoute; - - const result = calculateEffectivePrice(maximumInput, route, mockFromToken, mockToToken); - - // Base price: 2 * 10^18 - // With 10% impact: 2 * 0.9 = 1.8 * 10^18 - expect(result).toBe(parseUnits("1.8", 18)); - }); - - it("should handle different token decimals", () => { - const token8Decimals: Token = { - ...mockFromToken, - decimals: 8, - } as Token; - - const maximumInput = parseUnits("50", 8); - const route: MultiRoute = { - status: RouteStatus.Success, - amountOutBI: parseUnits("100", 6), - priceImpact: 0.02, // 2% price impact - } as any as MultiRoute; - - const result = calculateEffectivePrice(maximumInput, route, token8Decimals, mockToToken); - - // Base price: 2 * 10^18 - // With 2% impact: 2 * 0.98 = 1.96 * 10^18 - expect(result).toBe(parseUnits("1.96", 18)); - }); - - it("should handle high price impact", () => { - const maximumInput = parseUnits("1000", 18); - const route: MultiRoute = { - status: RouteStatus.Success, - amountOutBI: parseUnits("500", 6), - priceImpact: 0.5, // 50% price impact - } as any as MultiRoute; - - const result = calculateEffectivePrice(maximumInput, route, mockFromToken, mockToToken); - - // Base price: 0.5 * 10^18 - // With 50% impact: 0.5 * 0.5 = 0.25 * 10^18 - expect(result).toBe(parseUnits("0.25", 18)); - }); - - it("should handle very small price impact (scientific notation)", () => { - const maximumInput = parseUnits("10000", 18); - const route: MultiRoute = { - status: RouteStatus.Success, - amountOutBI: parseUnits("10000", 6), - priceImpact: 1e-20, // extremely small impact - } as any as MultiRoute; - - const result = calculateEffectivePrice(maximumInput, route, mockFromToken, mockToToken); - - // Base price: 1 * 10^18 - // With negligible impact, should be very close to base price - expect(result).toBeGreaterThan(parseUnits("0.999999999999999999", 18)); - expect(result).toBeLessThanOrEqual(parseUnits("1", 18)); - }); -}); diff --git a/src/router/sushi/index.ts b/src/router/sushi/index.ts index 79563367..71aa5638 100644 --- a/src/router/sushi/index.ts +++ b/src/router/sushi/index.ts @@ -6,17 +6,9 @@ import { MultiRoute, RouteLeg } from "sushi/tines"; import { BlackListSet, poolFilter } from "./blacklist"; import { TakeOrdersConfigType } from "../../order/types"; import { SushiRouterError, SushiRouterErrorType } from "./error"; -import { calculatePrice18, ONE18, scaleFrom18, scaleTo18 } from "../../math"; +import { calculatePrice18, scaleFrom18, scaleTo18 } from "../../math"; import { ChainId, LiquidityProviders, PoolCode, RainDataFetcher, Router } from "sushi"; -import { - Chain, - Account, - Transport, - parseUnits, - formatUnits, - PublicClient, - encodeAbiParameters, -} from "viem"; +import { Chain, Account, Transport, formatUnits, PublicClient, encodeAbiParameters } from "viem"; import { RouterType, RouteStatus, @@ -541,11 +533,13 @@ export class SushiRouter extends RainSolverRouterBase { maximumInput = maximumInput - initAmount / 2n ** i; } } else { - const effectivePrice = calculateEffectivePrice( + // realized average execution price of the simulated swap, this already + // includes the route's price impact, same as the trade simulation gate + const effectivePrice = calculatePrice18( maximumInput, - route, - fromToken, - toToken, + route.amountOutBI, + fromToken.decimals, + toToken.decimals, ); if (effectivePrice < ratio) { maximumInput = maximumInput - initAmount / 2n ** i; @@ -563,21 +557,3 @@ export class SushiRouter extends RainSolverRouterBase { } } } - -export function calculateEffectivePrice( - maximumInput: bigint, - route: MultiRoute, - fromToken: Token, - toToken: Token, -): bigint { - const price = calculatePrice18( - maximumInput, - route.amountOutBI, - fromToken.decimals, - toToken.decimals, - ); - if (typeof route.priceImpact === "undefined") { - return price; - } - return (price * parseUnits((1 - route.priceImpact).toFixed(12), 18)) / ONE18; -}