ERC-8377: Reference-Relative Slippage Bounds

Posting the draft spec for a small ERC. It reuses ERC-7726 for the reference price, so it stays intentionally narrow. Feedback welcome, especially on the open questions at the end.

requires: ERC-165, ERC-7726

Abstract

This proposal defines an interface for reference-relative slippage protection on token swaps. Instead of committing to a static minAmountOut at signing time, the caller supplies a slippage policy, an ERC-7726 quote oracle and a maximum deviation, and the executing contract derives the acceptable output floor from the reference price read at execution time, reverting if the realized output deviates beyond tolerance.

By moving the slippage floor from a stale, sign-time constant to a live, execution-time bound, this shrinks the window a sandwich attacker can extract, and lets wallets and aggregators express slippage protection in a single interoperable way, reusing the existing ERC-7726 oracle API rather than inventing another price source.

Motivation

Today a swap is protected by a single minAmountOut chosen when the transaction is built. This is the exact lever MEV extraction exploits:

  • Staleness. minAmountOut is set against a quote from block N, but the swap executes at block N+k. A sandwich bot moves the pool price inside that gap; as long as realized output stays above the stale floor, the sandwich is profitable and the victim cannot tell.
  • Over-wide tolerance. To avoid failed transactions during volatility, wallets default slippage high (1 to 3 percent). That headroom is precisely the extractable surface.
  • No standard. Every router, aggregator, and wallet encodes slippage differently, so protection cannot be reasoned about or improved uniformly.

A reference-relative floor addresses the first two: the floor is computed at execution against a fresh reference, so it tracks real market conditions rather than a number already stale when signed. Standardizing the interface addresses the third. This is not a claim to eliminate MEV; it narrows the extractable band and makes slippage protection legible and composable.

Specification

The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHOULD”, “SHOULD NOT”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119 and RFC 8174.

Slippage policy

struct SlippagePolicy {
    address quoteOracle;     // an ERC-7726 oracle for (tokenIn, tokenOut)
    uint32  maxDeviationBps; // max allowed shortfall vs the reference output, in basis points
    uint256 hardFloor;       // absolute minimum output accepted regardless of the reference
}
  • quoteOracle MUST implement ERC-7726 (getQuote).
  • maxDeviationBps is a shortfall tolerance: realized output MAY be at most maxDeviationBps below the reference-implied output. It MUST be <= 10_000.
  • hardFloor is an absolute floor; the effective floor is max(referenceFloor, hardFloor).
  • Reference freshness and manipulation resistance are the oracle’s responsibility (ERC-7726 implementations are expected to revert when they cannot provide a reliable quote). Callers select an oracle whose freshness SLA and manipulation cost fit the trade.

Guarded swap interface

interface ISlippageBoundedSwap {
    error SlippageExceeded(uint256 realizedOut, uint256 floor);
    error InvalidDeviation(uint32 maxDeviationBps);

    function swapWithPolicy(
        address tokenIn,
        address tokenOut,
        uint256 amountIn,
        SlippagePolicy calldata policy,
        bytes calldata routeData
    ) external returns (uint256 amountOut);
}

An executor implementing ISlippageBoundedSwap:

  1. MUST obtain the reference at execution time by calling IERC7726(policy.quoteOracle).getQuote(amountIn, tokenIn, tokenOut). It MUST NOT accept a reference output supplied by the caller.
  2. MUST revert InvalidDeviation if policy.maxDeviationBps > 10_000.
  3. MUST compute floor = max(referenceOut * (10_000 - policy.maxDeviationBps) / 10_000, policy.hardFloor).
  4. MUST perform the swap via routeData and measure the realized amountOut.
  5. MUST revert SlippageExceeded(amountOut, floor) if amountOut < floor.

Implementers MUST support ERC-165 and return true for the ISlippageBoundedSwap interface id.

Rationale

Why reference-relative instead of a static minimum? A static minAmountOut encodes the market as of signing; the attacker operates in the delta to execution. Recomputing the floor against a fresh reference collapses that delta into whatever the oracle’s freshness and manipulation cost allow.

Why reuse ERC-7726? A quote oracle is exactly ERC-7726’s remit (getQuote returns an explicit token amount for a (base, quote) pair), and it already has adapters across venues. Defining another oracle interface here would fragment the ecosystem and duplicate a standard; this proposal fixes only the slippage contract on top of it.

Why a shortfall tolerance (maxDeviationBps) rather than the caller passing a floor? So protection scales with size and live price automatically, and wallets can express one policy (“never more than 0.5 percent below the reference”) rather than recomputing a number per trade.

Why keep hardFloor? Oracles fail. hardFloor guarantees a worst case the caller pre-accepts even if the reference is unavailable within tolerance.

Relationship to ERC-5143. ERC-5143 defines slippage-protected variants of the ERC-4626 vault entrypoints with a caller-supplied static minimum, scoped to tokenized vaults. This proposal is scoped to general swaps and derives the bound from a live ERC-7726 reference rather than a static input. They are complementary.

Backwards Compatibility

Additive. Routers that do not implement ISlippageBoundedSwap are unaffected, and callers can keep using static-minAmountOut entrypoints. A router MAY implement both.

Security Considerations

  • The oracle is the trust root. A manipulable reference makes the floor manipulable. Because the reference comes from an ERC-7726 oracle, manipulation resistance and freshness are that oracle’s responsibility; callers SHOULD select an oracle (for example a TWAP window sized so moving it costs more than the sandwich it would enable) appropriate to the trade, and a spot price from the pool being traded MUST NOT be used as the reference.
  • Not an MEV eliminator. This narrows the sandwich band; it does not remove reordering, back-running, or extraction that stays within maxDeviationBps. It composes with private mempools and PBS-level protections rather than replacing them.
  • Oracle failure. If the oracle reverts or cannot quote within tolerance, the swap reverts or falls to the hardFloor path; callers set hardFloor as the accepted worst case.
  • Reference and venue divergence. If the reference and the execution venue diverge legitimately (thin liquidity, real moves), honest trades can revert. Callers SHOULD size maxDeviationBps for the venue’s normal basis.

Open questions

  • Reference source. Is a TWAP the right default, or should the interface stay agnostic and let the ERC-7726 adapter decide, as it does now?
  • Should maxDeviationBps be per-call only, or also expressible as a signed policy a wallet reuses across trades?
  • Is hardFloor worth keeping, or does relying on the oracle to revert when it cannot quote cover the failure case cleanly enough?

I have a reference implementation with the negative tests (the interface, an abstract base with the floor logic, and a mock ERC-7726 oracle) and will link the repo in a reply.

Reference implementation is up: GitHub - zexoverz/reference-relative-slippage-bounds: Reference implementation for the Reference-Relative Slippage Bounds ERC draft (interface + floor logic + ERC-7726 adapter + fork test) · GitHub

It has the interface, an abstract base with the floor logic, a mock ERC-7726 oracle for the unit tests, and a real Chainlink ERC-7726 adapter. The unit suite covers the floor breach, the hard-floor takeover, the reference being read live rather than caller-supplied, and the bounds. There is also a mainnet-fork test that derives the floor from the live Chainlink ETH/USD price, so it runs against real market data and not just a mock.

Awesome idea @zexoverz, after reviewing both the ERC draft on Magicians and the reference repo at zexoverz/reference-relative-slippage-bounds here is my feedback:

The core idea is sound and reusing ERC-7726 instead of inventing another oracle interface is the right call. There are three problems worth fixing before this goes further, and one of them is load bearing for the whole security claim.

1. referenceOut is a mid price, not an achievable output

ERC-7726 adapters are linear value conversions. A typical adapter fetches the unit price and multiplies it by inAmount. There is no depth or curve awareness in the standard. So getQuote(amountIn, tokenIn, tokenOut) returns what the trade would fetch at zero price impact and zero fee.

That means maxDeviationBps has to absorb all of the following at once: LP fee (30bps typical), price impact from the trade’s own size, oracle lag, and honest market drift. On any non trivial size in a normal pool that is well over 100bps, which is exactly the wide band the Motivation section criticizes. As written, the proposal reintroduces the problem it is solving, just with more steps.

Suggested fix. Split the budget into two signed inputs.

floor = referenceOut * (10000 - expectedCostBps - maxDeviationBps) / 10000

expectedCostBps is fee plus simulated route impact, computed at build time from the actual route. maxDeviationBps becomes adverse deviation only. Now maxDeviationBps can honestly be 20 to 50bps and the guarantee means something.

The likely objection is the existing MUST NOT rule against a caller supplied reference output. But that rule is really protecting a narrower invariant than it states. The floor must not depend on anything an adversary can move between signing and execution. A caller signed cost estimate satisfies that, because the caller is the party being protected. Writing that invariant into the spec explicitly would make the two field design fall out naturally.

2. ERC-7726 does not give you the freshness property the draft assumes

The draft says ERC-7726 implementations are expected to revert when they cannot provide a reliable quote. That is not actually a property of ERC-7726. The standard purposefully provides no methods for consumers to assess data validity, and leaves it to individual implementations to decide and publish their own data quality, including when they stop serving. There is no timestamp field, no staleness flag, no way for the executor to check anything on chain.

So the entire security argument currently rests on an assumption the underlying standard explicitly declines to make. This cannot be fixed inside this spec (adding a freshness method would be exactly the fragmentation the draft correctly avoids), but it should be named directly. Suggest adding to Security Considerations that oracle freshness is an unverifiable off chain trust assumption, and that callers are effectively selecting from a curated set of adapters whose guarantees they have read in advance.

3. routeData is an unbounded arbitrary call and the spec says nothing about it

Security Considerations covers the oracle thoroughly and the executor side not at all. If an executor decodes routeData into a target plus calldata with an unconstrained target, that is an arbitrary call primitive over its own approvals and residual balances. That is the shape behind a long list of router drains.

Suggest adding MUST level text. The executor MUST constrain route targets to an allowlist or an immutable router, MUST NOT depend on standing approvals, and MUST zero any approval granted for the route before returning.

Smaller points

Realized amountOut is undefined. “MUST perform the swap via routeData and measure the realized amountOut” is where bugs live. Specify it as the balance delta of a named recipient measured across the route call, and state the fee on transfer and rebasing assumption. Related, the interface has no recipient parameter and no deadline. Both are standard on a swap entrypoint and their absence will get raised.

hardFloor contradicts itself as written. With max(), an oracle revert reverts the whole call, so the hardFloor path described in Security Considerations is unreachable. Either add explicit try and catch semantics, or drop that sentence and reframe hardFloor as protection against an oracle that returns a wrong but live low value rather than an oracle failure fallback. That reframe also answers open question 3.

Rounding and boundaries. Specify the rounding direction on the floor computation (round up, conservative). maxDeviationBps at 10000 collapses the floor to hardFloor, so either forbid that value or document the behavior. maxDeviationBps at 0 will effectively always revert once real fees exist, which is a symptom of point 1 above.

TWAP lag is directional. When price trends against the trade, the reference sits above spot and the floor becomes unreachable, so the honest swap reverts during exactly the volatility where the user most needs to trade. When price trends with the trade, the reference sits below spot and the attacker gets a wider band than the caller intended. Protection quality is regime dependent and this belongs in Security Considerations.

Placement matters. A caller who does not trust the router gains nothing from that router checking its own floor. The guarantee only binds when the executor is honest, which is the case where it was needed least. The version that is easiest to adopt is a standalone guard wrapper usable in front of any existing router, or a v4 hook, shipped alongside the abstract base. Rationale should also address why this is not simply “use a private mempool,” since that will be an early question on the thread.

The repo needs the adversarial test, not only the happy path. The unit suite covers the interface contract well. What is missing is the test that makes the actual argument: simulate a sandwich against a static minAmountOut and against the guard on the same pool state, and report extracted value in both cases. That one comparison would do more to persuade reviewers than anything else in the repo. Also worth adding: a stale heartbeat test (the current Chainlink fork test uses a deviation threshold feed, which is the stale reference case), a fee on transfer token test, a hostile routeData target test, and a test at the 10000 boundary.

Editorial. Front matter needs eip, title, author, status Draft, type Standards Track, category ERC, created, and requires. Missing the CC0 Copyright section, Test Cases section, and Reference Implementation section. Worth publishing the computed interfaceId for swapWithPolicy(address,address,uint256,(address,uint32,uint256),bytes). The Abstract currently runs two paragraphs of motivation and editors will likely ask for it to be cut to one.

Open questions from the post

Reference source. Stay agnostic rather than mandating TWAP. Keep the existing rule against using the execution venue’s own spot price, and add a SHOULD that the adapter’s window be sized so that moving it costs more than the sandwich it would enable. Mandating TWAP specifically would exclude aggregated and off chain signed feeds for no real gain.

Per call versus signed reusable policy. Keep it per call for now. A reusable signed policy carries its own replay, revocation, and expiry surface, which is a separate design problem. Worth noting as future work while keeping this ERC narrow, which is its main strength right now.

Is hardFloor worth keeping. Yes, but under the reframed justification above. It is not an oracle failure fallback, it is protection against a live but wrong low value from the oracle.

Thank you, this is exactly the review the draft needed, and the mid-price point is correct and load bearing. Let me take them in order.

On referenceOut being a mid price. You are right that ERC-7726 returns a zero-impact, zero-fee quote, so folding fee, impact, oracle lag, and drift into one maxDeviationBps rebuilds the wide band I criticize. I am adopting your split. The floor becomes referenceOut adjusted by an expectedCostBps that captures the known, non-adversarial cost, and maxDeviationBps becomes an adverse-only tolerance on top. That separation is the thing that distinguishes this from ERC-5143’s single band, so it belongs in the normative core.

On freshness. Also correct, and I was leaning on a guarantee ERC-7726 declines to give. I will add a MUST that an implementation validates the reference against a caller-supplied max staleness and reverts otherwise, and state the assumption in Security Considerations.

On routeData. Agreed it is too open. I will constrain it at MUST level: routeData MUST NOT influence the recipient, the token pair, or the measured output, and the floor check MUST run against the actually received balance after the route executes, never a number the route reports.

On the editorial gaps, front matter, CC0, and interfaceId, conceded and fixed next push.

On the test, agreed. I will add an adversarial fork test where an attacker moves the pool between sign and execute, and assert the floor reverts rather than settling short.

Will push a revision with the two-field floor, the staleness MUST, and the routeData constraints, and ping you to recheck the security claim.

Pushed the revision. Two-field floor with expectedCostBps for the known cost and maxDeviationBps as adverse-only, the freshness requirement is now a MUST on the oracle rather than an assumption, routeData is constrained and the realized output is measured as the on-chain balance delta so the route cannot report what it did not pay, plus InvalidPolicy, the interfaceId, and an adversarial sandwich test. Full suite is green including the fork. Would appreciate your recheck on whether the security claim holds under the new shape.

Ran the suite before commenting, 10/10 green on the revised shape, including test_adversarialSandwich and test_routeCannotReportUnpaidOutput. @mzf11125’s mid-price catch was the load-bearing one and the two-field split is the right resolution; nothing to add there that the revision hasn’t already fixed.

What I want to name instead is a structural point neither of you has said out loud, because it makes this draft bigger than a swap guard:

This is the same rule as re-check-at-action-time, in a different domain. A minAmountOut fixed at signing time is a stale premise: it references market state as of issuance, and the transaction executes later against different state. Your fix is to stop carrying the answer and carry the policy instead, then derive the answer from a live reference at execution.

ERC-8323 arrived at the identical shape for identity, in almost the same words: a verdict attests the identity premise as of its timestamp, the premise does not carry forward, so any consumer gating an irreversible action MUST re-check at action time rather than trusting the attestation’s moment. Sign-time constant vs execution-time derivation, same failure, same fix, different asset. When one rule shows up independently in two unrelated domains, it’s usually a real invariant rather than a local design taste.

Two smaller observations from reading the code, both in that spirit:

1 · Measuring the balance delta is the strongest line in the implementation, and it’s underclaimed. amountOut comes from the executor’s own tokenOut balance change, not from anything the route reports. That’s “don’t trust the actor’s self-report, measure the effect”, the same reason our cells recompute rather than accept a claimed result. Right now it reads as an implementation detail in a comment; I’d make it a normative MUST in the spec, because a conforming implementation that trusted a route-reported amountOut would pass every test you have while losing the entire guarantee.

2 · Freshness delegated to the oracle is the right call, but it deserves the caveat stated. The policy says the oracle MUST enforce freshness, and ChainlinkQuoteOracle does (maxStaleness, reverting on stale or non-positive). The remaining sharp edge: the guarantee is only as good as the deployed adapter the policy points at, a caller can pass a conforming-by-interface oracle with maxStaleness = 0 and silently opt out of the MUST. Worth one sentence saying the floor’s strength is a property of the referenced oracle instance, not of the interface. (Same shape as an anchor only being a cutoff for a verifier who can actually fetch it — the guarantee travels with the instance, not the type.)

Neither blocks anything. Good draft, and the revision cycle on it was fast.

1 Like

This is the framing I wanted and had not put into words. Carrying the policy instead of the answer and deriving at execution is the same move as re-check-at-action-time, and seeing it show up independently for identity in ERC-8323 and for slippage here is the sign it is a real invariant rather than a local taste. A sign-time constant references state as of issuance, the transaction settles against later state, and the fix in both is to stop trusting the moment and re-derive at the action.

Both hardening points are in. The balance-delta measurement is a normative MUST in the spec now, not a comment, for exactly the reason you give, a conforming implementation that trusted a route-reported amountOut would pass every test and lose the whole guarantee. And I added the sentence that the freshness guarantee is a property of the deployed oracle instance the policy points at, not of the interface, including the maxStaleness of zero opt-out.

Thanks for running the suite. Opening the ethereum/ERCs PR next.

2 Likes

I’ve been following the revisions here and wanted to summarize where the discussion seems to have landed before raising a couple of questions.

@mzf11125 pointed out that referenceOut from ERC-7726 is a mid-price rather than an achievable execution output, so a single maxDeviationBps would otherwise have to absorb fees, price impact, oracle lag, and adverse movement. The resulting split into expectedCostBps and maxDeviationBps seems to address that distinction.

The same review also raised the oracle freshness assumption, the unconstrained role of routeData, the definition of realized output, and the need for an adversarial test rather than only testing the floor mechanically.

@zexoverz has since incorporated most of those points: the two-field floor, constraints around routeData, balance-delta measurement, the oracle-instance freshness assumption, and an adversarial sandwich test.

@TMerlini then framed the broader pattern as carrying a policy rather than carrying a result from signing time, and deriving the result again at execution time. They also called out two implementation properties that now appear to be part of the normative shape: measuring the effect through the balance delta rather than trusting route-reported output, and treating freshness as a property of the referenced oracle instance rather than ERC-7726 itself.

Given that, I wonder if the next discussion could focus less on the basic floor construction and more on the boundary of the guarantee.

One question is what exactly the protected outcome is.

The implementation measures the executor’s tokenOut balance delta. Is the intended invariant therefore:

the executor received at least floor

or should it eventually be:

the recipient received at least floor?

Those differ if the executor later forwards the output, applies another fee, or interacts with tokens with unusual transfer semantics. @mzf11125 already noted the absence of a recipient parameter, but I’m not sure the thread has settled whether that is intentionally outside the ERC’s scope or something the interface should eventually represent.

A second question is around expectedCostBps.

The revision separates it from adversarial deviation, but it is still an input determined before execution from assumptions about the selected route, trade size, fee structure, and expected impact.

Does the security model require expectedCostBps to be bound in some way to the route that is actually executed?

For example, if the signed expectedCostBps assumes route A but routeData produces a materially different execution path, the floor is still computed from the original expected cost. An overestimate makes the effective bound weaker, while an underestimate increases the chance of a normal trade reverting.

Maybe that is acceptable because the caller is choosing both the policy and the route, but it seems worth stating whether the relationship between expectedCostBps and routeData is part of the invariant or only a caller-side responsibility.

This also seems like a useful place for the tests to go next. The current adversarial test asks whether execution below the reference-relative floor reverts. A comparison across trade sizes, liquidity conditions, oracle lag, and different expectedCostBps assumptions could show where the bound sits between two outcomes: value left available to an attacker and otherwise valid trades that revert.

That may help clarify what properties belong in the ERC itself versus what wallets and route builders are expected to enforce.

Good questions. On the first, it should be the recipient received at least floor, not the executor. The executor is just the caller, it can forward output, take a fee, or sit in the path, so measuring its balance would be gameable and would not reflect what the trade was actually for. The invariant belongs on the account the swap settles to.

On the second, expectedCostBps should not be bound to the executed route, that would be circular, you could never detect a route that came in worse than the reference. What it does need is freshness. It comes from a reference reading taken at decision time, independent of whatever route runs, and the floor compares the actual outcome against that. Your stale-assumption case is real, but the fix is the freshness requirement on the oracle instance, not tying the reference to the route it is meant to police. A caller cannot just assert a loose expectedCostBps, it has to come from that fresh reference.