← back to lessons

Lesson: Async-filler "split-rounding value gate" (Cycle 204, Reserve Trusted Fillers)

Date: 2026-06-10 Source: Cycle 204 - Reserve Protocol Trusted Fillers (Cantina $10M), repo github.com/reserve-protocol/trusted-fillers @ f8db678. Verdict: WALK CLEAN. Class: async/intent-based swap fillers (CoW Swap, 1inch Fusion, UniswapX-style EIP-1271 wrappers).

The pattern

Intent fillers split trade authorization across TWO predicates in TWO functions:

  1. Accept predicate - isValidSignature (EIP-1271). Enforces a per-unit price FLOOR: orderPrice = floor(order.buyAmount * D27 / order.sellAmount) >= price where price = ceil(minBuyAmount * D27 / sellAmount) set at init. Rounds the accepted price DOWN.

  2. Completion predicate - swapActive() / the close gate. Independently recomputes the required counter-asset for the portion ACTUALLY moved: minimumExpectedIn = ceil((sellAmount - currentSellBalance) * price / D27) and refuses to close (sweep funds back) until buyBalance >= minimumExpectedIn. Rounds the requirement UP.

Why it is safe (and the trap)

A naive audit reads only isValidSignature, sees a per-unit floor that a PARTIAL fill can satisfy while moving an odd dust amount, and suspects a rounding-leak finding. That is the trap. The completion predicate re-demands, with CEIL rounding over the realized sold quantity, the buy token the accept predicate may have let slide on the sign side. The two opposing roundings (floor-on-accept, ceil-on-require) over the SAME realized quantities compose into a value-conservation invariant:

returned_sell + delivered_buy  >=  minBuyAmount   (valued at the floor)

Verified empirically across full fill, half partial fill, and (sell=3, buy=6) dust fill - all conserve value. (Cycle 204 H1/H2/H4, real forge output, PASS.)

Audit-transfer rule

For ANY async/intent filler that authorizes via a signature/predicate and settles out-of-band: 1. Do NOT verdict on the order-validation predicate alone. 2. Locate the SETTLEMENT-COMPLETION predicate (the gate that releases/sweeps funds). 3. Verify the two predicates use OPPOSING rounding (floor on accept, ceil on require) over the SAME realized quantities. Opposing -> defended. SAME direction -> that is the leak; build the PoC. 4. Test by asserting returned_sell + delivered_buy >= floor across full / partial / dust fills, not by eyeballing one function.

Secondary dead-ends banked (so future cycles skip)

Generated 2026-07-02 13:15:02 UTC | auto-sync /15min