Alert Source Discuss
⚠️ Draft Standards Track: ERC

ERC-8377: Reference-Relative Slippage Bounds

An interface for swap slippage relative to a live reference price at execution time, not a static minimum fixed at signing time

Authors Faisal Firdani (@zexoverz)
Created 2026-08-05
Discussion Link https://ethereum-magicians.org/t/erc-8377-reference-relative-slippage-bounds/29292
Requires EIP-165, EIP-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); MUST enforce freshness
    uint32  expectedCostBps;  // known non-adversarial cost vs the mid reference (fee + impact)
    uint32  maxDeviationBps;  // adverse-only shortfall tolerance beyond the expected output
    uint256 hardFloor;        // absolute minimum output accepted regardless of the reference
    uint256 deadline;         // unix seconds past which the intent expires; 0 means unbounded
}
  • quoteOracle MUST implement ERC-7726 (getQuote). Its quote is a mid price, with no fee or price impact.
  • expectedCostBps is the known, non-adversarial discount from the mid: the pool fee plus the modeled price impact for this size. It separates expected execution cost from slippage protection, so a normal, honest fill is not mistaken for an attack. It MUST be <= 10_000.
  • maxDeviationBps is the adverse-only shortfall tolerated beyond the expected output. It MUST be <= 10_000. Splitting the two is what keeps this narrower than ERC-5143’s single band: folding fee, impact, and drift into one tolerance rebuilds the wide band this proposal exists to shrink.
  • hardFloor is an absolute floor; the effective floor is max(referenceFloor, hardFloor).
  • deadline is unix seconds past which the intent to trade expires. A zero deadline is unbounded, so a policy that sets none behaves as though the field were absent. It bounds a different staleness from the floor: the floor is recomputed at execution and does not go stale, but the caller’s decision to trade at all does.
  • Reference freshness is not optional. Because ERC-7726 getQuote is stateless and the standard makes no freshness guarantee, an implementation MUST use an oracle that enforces a freshness bound and reverts when it cannot give a reliable quote, and MUST NOT treat a quote whose freshness cannot be established as valid.
  • The reference MUST be independent of the venue being traded. quoteOracle MUST NOT be a spot price read from the pool the swap executes against, because an attacker who moves that pool moves the floor with it and the bound becomes self-referential.

Guarded swap interface

interface ISlippageBoundedSwap {
    error SlippageExceeded(uint256 realizedOut, uint256 floor);
    error InvalidPolicy(uint32 expectedCostBps, uint32 maxDeviationBps);
    error InvalidRecipient();
    error DeadlineExpired(uint256 deadline, uint256 timestamp);

    /// @dev MUST revert DeadlineExpired before reading the reference or running the
    ///      route when deadline != 0 && block.timestamp > deadline, then
    ///      read the reference at execution via ERC-7726 getQuote, compute
    ///      referenceOut = getQuote(amountIn, tokenIn, tokenOut),
    ///      expectedOut = referenceOut * (10_000 - expectedCostBps) / 10_000,
    ///      floor = max(expectedOut * (10_000 - maxDeviationBps) / 10_000, hardFloor),
    ///      measure realizedOut as the recipient's tokenOut balance delta, and revert
    ///      SlippageExceeded if realizedOut < floor.
    function swapWithPolicy(
        address tokenIn,
        address tokenOut,
        uint256 amountIn,
        address recipient,
        SlippagePolicy calldata policy,
        bytes calldata routeData
    ) external returns (uint256 amountOut);
}

An executor implementing ISlippageBoundedSwap:

  1. MUST revert InvalidPolicy if policy.expectedCostBps > 10_000 or policy.maxDeviationBps > 10_000, and MUST revert InvalidRecipient if recipient is the zero address.
  2. MUST revert DeadlineExpired(policy.deadline, block.timestamp) if policy.deadline != 0 and block.timestamp > policy.deadline. This check MUST happen before the reference is read and before the route runs, so rejecting an expired intent does not depend on an oracle read succeeding. A policy.deadline of zero imposes no bound.
  3. 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, and MUST use an oracle that enforces freshness (see Slippage policy).
  4. MUST compute expectedOut = referenceOut * (10_000 - policy.expectedCostBps) / 10_000, then floor = max(expectedOut * (10_000 - policy.maxDeviationBps) / 10_000, policy.hardFloor).
  5. MUST execute the route and measure the realized amountOut as recipient’s tokenOut balance increase across the call. It MUST NOT use a value the route reports. routeData is an opaque execution hint and MUST NOT influence the token pair, the recipient, or the measured amountOut.
  6. MUST revert SlippageExceeded(amountOut, floor) if amountOut < floor.

Interface detection

Implementers MUST support ERC-165 and MUST return true from supportsInterface for the ISlippageBoundedSwap interface id 0x41b46b60.

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 two fields (expectedCostBps and maxDeviationBps) instead of one tolerance? The ERC-7726 reference is a mid price, so a real fill is always below it by the pool fee plus price impact before any attack. A single tolerance would have to absorb that expected cost, which pushes it back above 100 basis points and rebuilds the wide extractable band the Motivation criticizes. Separating the known cost (expectedCostBps) from the adverse-only tolerance (maxDeviationBps) lets the guard subtract what execution honestly costs and then police only the adversarial remainder, which is the difference that makes this narrower than a static single band.

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

Why measure the output on-chain rather than trust the route? routeData is an opaque call to an arbitrary venue. If the guard trusted a number the route returned, the route could report a passing amount it never paid. Measuring recipient’s tokenOut balance delta makes the floor check independent of what the route claims, so the security property does not depend on the honesty of the route.

Why measure at the recipient rather than the executor? The bound is a statement about what the trade delivered, and the executor is only the caller. It may forward the output, take a fee, or sit in the path, so an executor that keeps what the route paid would satisfy a floor checked against its own balance while the account the swap settles to received nothing. Naming the recipient makes the guarantee land on the account it is about. Passing the executor’s own address is still allowed and reproduces the simpler case.

Why a deadline as well as a live reference? This proposal exists because a number computed at signing time goes stale, and its answer is to carry the policy and derive the number at execution. A deadline is the other half of that same problem. The policy does not go stale, but the decision to trade does. Because a reference-relative bound is immune to price drift by construction, a caller who decided to swap yesterday gets today’s price with the same bps guarantee, and no reference-relative bound can protect against that. That is the honest trade this proposal makes, and a deadline is what covers it.

This is a different property from a stale quote, and the two need separate mechanisms. The floor already fails closed when the oracle cannot produce a fresh quote, because the oracle reverts and the executor bubbles it. That covers a stale reference. It says nothing about a stale intent, because the reference the guard reads is fresh in exactly the case the caller’s decision is old.

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 (deposit, mint, withdraw, redeem with a caller-supplied bound). It is scoped to tokenized vaults and to a static, caller-supplied minimum. 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 can implement both.

Test Cases

All cases use amountIn = 1000 and a mid reference from the oracle. expectedOut = referenceOut * (10_000 - expectedCostBps) / 10_000, floor = max(expectedOut * (10_000 - maxDeviationBps) / 10_000, hardFloor). Integer division truncates.

# referenceOut expectedCostBps maxDeviationBps hardFloor floor realized amountOut Expected result
1 1000 0 100 0 990 995 returns 995
2 1000 0 100 0 990 989 reverts SlippageExceeded(989, 990)
3 1000 200 100 0 970 970 returns 970
4 1000 200 100 0 970 969 reverts SlippageExceeded(969, 970)
5 1000 0 100 996 996 995 reverts SlippageExceeded(995, 996)
6 2000 0 100 0 1980 1979 reverts SlippageExceeded(1979, 1980)
7 1000 30 50 0 992 991 reverts SlippageExceeded(991, 992)
8 1000 30 50 0 992 992 returns 992
9 1000 0 100 0 990 0 reverts SlippageExceeded(0, 990)
10 1000 0 100 0 990 0 to the recipient, 1000 kept by the executor reverts SlippageExceeded(0, 990)

Cases 3 and 4 show the two fields stacking rather than collapsing: a 2% known cost yields expectedOut = 980, and the 1% adverse tolerance applies to that, not to the mid. Case 5 shows hardFloor taking over when it is higher than the reference floor. Case 6 changes only the oracle rate, so a floor that moves with it proves the reference is read at execution rather than supplied by the caller. Cases 7 and 8 are a sandwich either side of the boundary: the reference stays a fresh mid at 1000 while the fill is pushed to 991, one unit below the floor. Case 9 is a route that delivers nothing, which the guard catches because it measures a balance delta rather than trusting a route-reported amount. Case 10 is the same rejection for a route that did pay in full but paid the executor instead of the recipient, which is why the measurement is taken at the recipient.

Two policy cases are independent of the floor arithmetic:

Input Expected result
expectedCostBps = 10_001, maxDeviationBps = 100 reverts InvalidPolicy(10001, 100)
expectedCostBps = 0, maxDeviationBps = 10_001 reverts InvalidPolicy(0, 10001)
recipient = address(0) reverts InvalidRecipient()

Three deadline cases, all with referenceOut = 1000, expectedCostBps = 0, maxDeviationBps = 100, hardFloor = 0 and a realized output of 995, which is above the floor of 990 and so settles unless the deadline rejects first:

deadline block.timestamp Expected result
999_999 1_000_000 reverts DeadlineExpired(999999, 1000000)
1_000_000 1_000_000 returns 995; the deadline is the last second that still settles
0 4_000_000_000 returns 995; a zero deadline imposes no bound

The first case also holds with an oracle that cannot quote: DeadlineExpired is what surfaces, because the intent is checked before the reference is read.

An oracle that cannot produce a fresh quote reverts, and the executor bubbles that revert rather than falling back to an unbounded swap.

These cases are executable as SlippageBoundedSwap.t.sol. ForkSlippageBounded.t.sol additionally derives the floor from a live Chainlink ETH/USD reference through an ERC-7726 adapter and settles a real USDC balance delta.

Reference Implementation

  • SlippageBoundedSwap.sol - SlippagePolicy, the ISlippageBoundedSwap interface and its errors, and an abstract base implementing the floor logic, with route execution left as the internal _route hook so any router can inherit the guard.
  • ChainlinkQuoteOracle.sol - an ERC-7726 adapter over a Chainlink feed that enforces the freshness bound.
  • MockQuoteOracle.sol - the oracle used by the unit tests.

Security Considerations

  • The oracle is the trust root. A manipulable or stale reference makes the floor manipulable. ERC-7726 getQuote is stateless and the standard makes no freshness guarantee, so this proposal does not lean on an unstated assumption: the Specification requires an oracle that enforces a freshness bound and reverts when it cannot give a reliable quote. The guarantee is a property of the deployed oracle instance the policy points at, not of the interface: an oracle that is conforming by interface but configured with no staleness bound silently opts out of that requirement, so callers need to verify that the specific oracle instance enforces the freshness the trade needs. Callers are advised to select an oracle appropriate to the trade, for example a TWAP window sized so moving it costs more than the sandwich it would enable. The Specification forbids using a spot price from the pool being traded as the reference.
  • Output is measured, not reported. The realized amount is recipient’s tokenOut balance delta across the route, so a malicious or buggy routeData cannot pass the floor with an output it did not deliver. The Specification forbids re-introducing a route-reported amount into the floor check.
  • A deadline bounds intent, not inclusion. It caps how long a signed decision stays executable, which is what a reference-relative floor cannot do, but it does not stop a builder or relay from withholding a transaction until it expires. Callers are advised to size it to how long the decision stays wanted rather than to how long inclusion is expected to take, and to treat expiry as a re-decision rather than a failure.
  • 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 are advised to size maxDeviationBps for the venue’s normal basis.

Copyright and related rights waived via CC0.

Citation

Please cite this document as:

Faisal Firdani (@zexoverz), "ERC-8377: Reference-Relative Slippage Bounds [DRAFT]," Ethereum Improvement Proposals, no. 8377, August 2026. Available: https://eips.ethereum.org/EIPS/eip-8377.