Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 147 additions & 0 deletions src/core/modes/router/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,153 @@ describe("Test findBestRouterTrade", () => {
);
});

it("should backoff with halved trade sizes when partial trade fails with MinimalOutputBalanceViolation", async () => {
const mockFullTradeError = Result.err({
type: TradeType.RouteProcessor,
reason: SimulationHaltReason.OrderRatioGreaterThanMarketPrice,
spanAttributes: { error: "ratio too high" },
noneNodeError: "order ratio issue",
});
const mockViolationError = Result.err({
type: TradeType.RouteProcessor,
reason: SimulationHaltReason.NoOpportunity,
spanAttributes: {
error: "execution reverted: MinimalOutputBalanceViolation(0xtoken, 123)",
},
});
const mockFallbackSuccess = Result.ok({
type: TradeType.RouteProcessor,
spanAttributes: { foundOpp: true },
estimatedProfit: 25n,
oppBlockNumber: 123,
});

(trySimulateTradeSpy as Mock)
.mockResolvedValueOnce(mockFullTradeError) // full size
.mockResolvedValueOnce(mockViolationError) // partial size 1000n
.mockResolvedValueOnce(mockViolationError) // partialFallback1 500n
.mockResolvedValueOnce(mockFallbackSuccess); // partialFallback2 250n
(mockRainSolver.state.router.findLargestTradeSize as Mock).mockReturnValue(1000n);

const result: SimulationResult = await findBestRouterTrade.call(
mockRainSolver,
orderDetails,
signer,
ethPrice,
toToken,
fromToken,
blockNumber,
);

assert(result.isOk());
expect(result.value.spanAttributes).toEqual({ foundOpp: true });
expect(result.value.estimatedProfit).toBe(25n);
expect(trySimulateTradeSpy).toHaveBeenCalledTimes(4);
expect(simulatorWithArgsSpy).toHaveBeenNthCalledWith(
3,
expect.objectContaining({ maximumInputFixed: 500n, isPartial: true }),
);
expect(simulatorWithArgsSpy).toHaveBeenNthCalledWith(
4,
expect.objectContaining({ maximumInputFixed: 250n, isPartial: true }),
);
});

it("should return error with MinimalOutputBalanceViolation reason when all backoff steps fail", async () => {
const mockFullTradeError = Result.err({
type: TradeType.RouteProcessor,
reason: SimulationHaltReason.OrderRatioGreaterThanMarketPrice,
spanAttributes: { error: "ratio too high" },
noneNodeError: "order ratio issue",
});
const mockViolationError = Result.err({
type: TradeType.RouteProcessor,
reason: SimulationHaltReason.NoOpportunity,
spanAttributes: {
error: "execution reverted: MinimalOutputBalanceViolation(0xtoken, 123)",
},
});

(trySimulateTradeSpy as Mock)
.mockResolvedValueOnce(mockFullTradeError) // full size
.mockResolvedValue(mockViolationError); // partial + all fallbacks
(mockRainSolver.state.router.findLargestTradeSize as Mock).mockReturnValue(1024000n);

const result: SimulationResult = await findBestRouterTrade.call(
mockRainSolver,
orderDetails,
signer,
ethPrice,
toToken,
fromToken,
blockNumber,
);

assert(result.isErr());
expect(result.error.reason).toBe(SimulationHaltReason.MinimalOutputBalanceViolation);
expect(result.error.noneNodeError).toBe("order ratio issue");
// 1 full + 1 partial + 5 fallbacks
expect(trySimulateTradeSpy).toHaveBeenCalledTimes(7);
expect(simulatorWithArgsSpy).toHaveBeenLastCalledWith(
expect.objectContaining({ maximumInputFixed: 32000n, isPartial: true }),
);
expect(result.error.spanAttributes["full.error"]).toBe("ratio too high");
expect(result.error.spanAttributes["partial.error"]).toContain(
"MinimalOutputBalanceViolation",
);
expect(result.error.spanAttributes["partialFallback1.error"]).toContain(
"MinimalOutputBalanceViolation",
);
expect(result.error.spanAttributes["partialFallback5.error"]).toContain(
"MinimalOutputBalanceViolation",
);
expect(result.error.spanAttributes["partialFallback6.error"]).toBeUndefined();
});

it("should stop backoff when a step fails with an error other than MinimalOutputBalanceViolation", async () => {
const mockFullTradeError = Result.err({
type: TradeType.RouteProcessor,
reason: SimulationHaltReason.OrderRatioGreaterThanMarketPrice,
spanAttributes: { error: "ratio too high" },
noneNodeError: "order ratio issue",
});
const mockViolationError = Result.err({
type: TradeType.RouteProcessor,
reason: SimulationHaltReason.NoOpportunity,
spanAttributes: {
error: "execution reverted: MinimalOutputBalanceViolation(0xtoken, 123)",
},
});
const mockOtherError = Result.err({
type: TradeType.RouteProcessor,
reason: SimulationHaltReason.NoOpportunity,
spanAttributes: { error: "some other revert" },
});

(trySimulateTradeSpy as Mock)
.mockResolvedValueOnce(mockFullTradeError) // full size
.mockResolvedValueOnce(mockViolationError) // partial size
.mockResolvedValueOnce(mockOtherError); // partialFallback1
(mockRainSolver.state.router.findLargestTradeSize as Mock).mockReturnValue(1000n);

const result: SimulationResult = await findBestRouterTrade.call(
mockRainSolver,
orderDetails,
signer,
ethPrice,
toToken,
fromToken,
blockNumber,
);

assert(result.isErr());
expect(result.error.reason).toBe(SimulationHaltReason.MinimalOutputBalanceViolation);
// 1 full + 1 partial + 1 fallback, stopped early
expect(trySimulateTradeSpy).toHaveBeenCalledTimes(3);
expect(result.error.spanAttributes["partialFallback1.error"]).toBe("some other revert");
expect(result.error.spanAttributes["partialFallback2.error"]).toBeUndefined();
});

it("should retry with the failing route dexes excluded when full trade dryrun fails", async () => {
const sushiQuote = {
route: {
Expand Down
49 changes: 49 additions & 0 deletions src/core/modes/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,13 +227,62 @@ export async function tryFindBestRouterTrade(
partialTradeSizeSimResult.error.spanAttributes,
"partial",
);

// if the partial trade size sim got rejected onchain with MinimalOutputBalanceViolation,
// it means the offchain pool data overestimated the output for the found partial trade
// size, so backoff by halving the trade size at each step validated against onchain
// dryrun and accept the first size that passes, the backoff stops early if a step fails
// with any other error
let reason = partialTradeSizeSimResult.error.reason;
if (
SimulationHaltReason.isMinimalOutputBalanceViolation(
partialTradeSizeSimResult.error.spanAttributes["error"],
)
) {
reason = SimulationHaltReason.MinimalOutputBalanceViolation;
let fallbackTradeSize = partialTradeSize;
for (let i = 1; i <= 5; i++) {
fallbackTradeSize /= 2n;
if (fallbackTradeSize <= 0n) break;
const partialFallbackSimulator = RouterTradeSimulator.withArgs({
type: TradeType.Router,
solver: this,
orderDetails,
fromToken,
toToken,
signer,
maximumInputFixed: fallbackTradeSize,
ethPrice,
isPartial: true,
blockNumber,
excludeDexes,
});
const partialFallbackSimResult = await partialFallbackSimulator.trySimulateTrade();
if (partialFallbackSimResult.isOk()) {
return { result: partialFallbackSimResult, quote };
}
extendObjectWithHeader(
spanAttributes,
partialFallbackSimResult.error.spanAttributes,
`partialFallback${i}`,
);
if (
!SimulationHaltReason.isMinimalOutputBalanceViolation(
partialFallbackSimResult.error.spanAttributes["error"],
)
) {
break;
}
}
}
return {
result: Result.err({
type: fullTradeSizeSimResult.error.type,
spanAttributes,
noneNodeError:
fullTradeSizeSimResult.error.noneNodeError ??
partialTradeSizeSimResult.error.noneNodeError,
reason,
}),
quote,
};
Expand Down
15 changes: 15 additions & 0 deletions src/core/modes/simulator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -620,3 +620,18 @@ describe("Test TradeSimulatorBase", () => {
});
});
});

describe("Test SimulationHaltReason namespace", () => {
it("should detect MinimalOutputBalanceViolation in the given text", () => {
expect(
SimulationHaltReason.isMinimalOutputBalanceViolation(
'execution reverted: MinimalOutputBalanceViolation(0xtoken, 123)"',
),
).toBe(true);
expect(SimulationHaltReason.isMinimalOutputBalanceViolation("some other error")).toBe(
false,
);
expect(SimulationHaltReason.isMinimalOutputBalanceViolation(undefined)).toBe(false);
expect(SimulationHaltReason.isMinimalOutputBalanceViolation(123)).toBe(false);
});
});
11 changes: 11 additions & 0 deletions src/core/modes/simulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,17 @@ export enum SimulationHaltReason {
OrderRatioGreaterThanMarketPrice,
FailedToGetTaskBytecode,
UndefinedTradeDestinationAddress,
MinimalOutputBalanceViolation,
}
export namespace SimulationHaltReason {
/**
* Returns true if the given input contains the sushi RouteProcessor
* contract MinimalOutputBalanceViolation error selector name
* @param text - The text to search in
*/
export function isMinimalOutputBalanceViolation(text: unknown): boolean {
return typeof text === "string" && text.includes("MinimalOutputBalanceViolation");
}
}

export type SimulateTradeArgs =
Expand Down
Loading