Skip to content

A reverting calculate-io eval reverts the whole takeOrders4 batch instead of skipping that order #2837

Description

@thedavidmeister

Narrow successor to #169, scoped to what is actually missing in RaindexV6: an order whose rainlang eval reverts. Dead orders, too-expensive orders and non-positive amounts are already skipped; handle-io stays a hard revert.

Current behaviour

takeOrders4 already drops orders gracefully in two cases: a dead order emits OrderNotFound and the loop continues, and an order whose calculation is too expensive or yields a non-positive amount/ratio is a no-op. The rationale is in the code:

Skip orders that are too expensive rather than revert as we have no way of knowing if a specific order becomes too expensive between submitting to mempool and execution, but other orders may be valid so we want to take advantage of those if possible.

A revert gets none of that. calculateOrderIO calls order.evaluable.interpreter.eval4(...) (src/concrete/raindex/RaindexV6.sol:859) with no try, so a revert inside an order's calculate rainlang — or the UnsupportedCalculateOutputs check at :875 — takes the whole transaction down, including every viable order later in the list. The current answer sits in the comment beside that call: "the caller controls which orders are eval'd as they can drop failing calls and resubmit a new transaction" — resubmission, which is exactly what the price-skip path exists to avoid. It hits arb takers hardest, since they submit multi-order batches whose runtime behaviour they do not control.

Why this is now cheap and safe

  • calculateOrderIO makes no store writes. The calculate KVs are written inside handleIO (:1068), which runs only for orders that are taken. The loop says so: "An untaken order runs no handleIO, so it makes no state change at all."
  • So catching a calculate revert leaves nothing to roll back, and the zero-amount branch it should fall into already exists and already writes nothing.
  • The eval is an external call, so try/catch is available at that site.

This is the resolution recorded on #169 in 2024: treat a calculate-io revert as amount io-ratio: 0 … and ignore its writes.

Explicitly out of scope

Handle-io reverts stay hard reverts. By the time handleIO evaluates, the order is taken and its calculate KVs are already committed at :1068, so swallowing a revert there would persist state for an order that did not settle. #169's 2026-06-15 triage note flags that invariant for the auditors; this issue must not weaken it.

Notes for implementation

  • Emit an event for an order skipped because it reverted. A silent skip makes a broken order indistinguishable from an unprofitable one, both on chain and to the bots reading the logs.
  • Decide the gas policy explicitly. try forwards 63/64 of remaining gas, so an order that burns it all can still starve the rest of the batch — either cap the gas passed to the calculate eval, or state that it is deliberately unbounded.
  • Tests: an order whose calculate reverts is skipped while later orders still take; the store shows no writes for the skipped order; a handle-io revert still reverts the whole call.

Refs #169.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions