Slippage & Fill Protection
Levant is oracle-priced, so there is no order book and no thin-liquidity slippage. What you are protected against instead is the spread widening — or the mid moving — between the moment you submit an intent and the moment a keeper fills it. Two on-chain guards enforce that: a mandatory slippage tolerance you set, and a hard liquidatable-at-open check the protocol sets. A fill must pass both.
What fill protection means on Levant
Every trade fills against the shared Levant Vault at a verified oracle mid price plus a deterministic spread. There is no book to eat into and no queue to be picked off in. But execution is two-step: you escrow collateral on-chain (submitOpenIntent for a market order, submitLimitIntent for a limit or stop), and a keeper fills it in a later transaction with a signed price report (executeOpen). The price you fill at is therefore a forward price, and the spread is recomputed against the book state at fill time.
That gap is the real risk to manage. Between submit and fill the mid can move, and your size plus the market's long/short skew determine how wide the spread lands. Fill protection is what caps how far that fill is allowed to drift from the mid before the keeper is forbidden to execute it.
maxSlippageBps is mandatory
Every open and every limit/stop intent carries a maxSlippageBps in the range [1, 500] — that is 1 basis point (0.01%) up to 500 bps (5%). There is no implicit default. Passing 0, or anything above 500, reverts with BadSlippage at submission. This is deliberate: a silent default is exactly how a trader ends up filled 2% away from the price they were shown.
// on submitOpenIntent / submitLimitIntent
require 1 <= maxSlippageBps <= 500 // else revert BadSlippage
// 1 bp = 0.01% 500 bps = 5% (no implicit default)The ceiling of 500 bps is not arbitrary: it equals the protocol's maximum spread cap (5%). The engine can never quote a spread wider than 5%, so a tolerance above that would protect against nothing. 500 bps is therefore the widest tolerance that still means something.
How the check works at fill
When the keeper calls executeOpen, the facet takes the verified mid, computes your spread-adjusted entry, and measures how far that entry sits from the mid. If the deviation is greater than your tolerance, the whole call reverts with SlippageExceeded.
// inside executeOpen, after the spread-adjusted entry is computed
entryPrice = mid + spread // open long (buy): fills above mid
entryPrice = mid - spread // open short (sell): fills below mid
deviationBps = |entryPrice - mid| * 10_000 / mid // = the total spread, in bps
if deviationBps > maxSlippageBps: revert SlippageExceededBecause the spread only ever moves your entry *against* you — a fill is never better than the mid — the deviation the check measures is exactly the total spread in basis points. So your tolerance is effectively a cap on the spread you are willing to accept. That spread has three parts:
spread(bps) = 2 + notional * 100 / depth1pct + skewCoeff * imbalance / oiCap
= constant + price-impact + skew
floored at 0 (never better than mid), capped at 500 bps (5%)
// constant = 2 bps (0.02%)
// depth1pct = 10,000,000 USDG (notional that moves price 1%)
// skewCoeff = 100 bps at a fully skewed book
// oiCap = 2,000,000 USDGThe practical consequence: bigger size (larger price-impact term) and trading into an already-crowded side (larger skew term) both widen the spread, and a wide-enough spread trips your tolerance. A trade that *reduces* skew pays a smaller premium and is more likely to clear a tight tolerance.
SlippageExceeded leaves your intent pending and refundable
A SlippageExceeded revert rolls back the entire executeOpen transaction. Nothing is consumed: no position is minted, no open fee is taken, and your intent stays pending with its escrow fully intact. The keeper can simply retry — the fill lands as soon as the spread narrows back inside your tolerance (for example, once skew rebalances or the mid settles).
- The intent is not consumed — it remains pending and eligible for the next keeper attempt.
- Your escrowed collateral stays held by the Diamond, untouched.
- No open fee is charged and no position is created.
- The keeper retries automatically; the fill executes once the spread fits your tolerance.
- You can reclaim your collateral at any time with
cancelIntentif you no longer want the fill.
cancelIntent is callable only by the intent's owner and refunds the full escrowed collateral (the gross amount you deposited). It reverts once the intent has already been filled, so there is never a double-spend between a fill and a cancel.
The liquidatable-at-open guard
A second guard sits immediately after the slippage check, and it is not something you configure. executeOpen refuses to mint a position that would already be at or under its maintenance margin at the mark it just filled against — it reverts LiquidatableAtOpen. Because your entry is always worse than the mid by the spread, every position opens fractionally underwater by exactly that spread. At very high leverage the entry spread alone can consume the whole maintenance buffer, and the protocol will not open a position that is dead on arrival.
// inside executeOpen, after the slippage check passes
equity = margin + PnL(notional, entryPrice -> mid) // starts negative by ~the spread
if equity <= margin * 10% (maintenance margin): revert LiquidatableAtOpen
// a long's opening loss ~= notional * spread = margin * leverage * spread
// dead-on-arrival roughly when leverage * spread >= 90%
// e.g. at 100x, an entry spread near ~0.9% already fails this checkThe maintenance margin is 10%, so the buffer between opening and liquidation is 90% of margin. A position whose opening loss (its own spread) eats through that buffer would be liquidatable in the very next block — the trader would pay the open fee only to be immediately liquidated. Refusing the mint protects the trader from that, and it protects the vault from churn. The rollback semantics are identical to SlippageExceeded: the intent stays pending, no fee is taken, and you can retry (with lower leverage or smaller size) or cancelIntent.
Why both guards exist
The two checks defend different things and are fully independent — a fill must clear both. One is about *price quality*, which is yours to set; the other is about *not opening straight into liquidation*, which the protocol fixes.
| Guard | Protects against | Trader-configurable? | Trips when | On trip |
|---|---|---|---|---|
| SlippageExceeded | Filling at a worse price than you agreed to | Yes — maxSlippageBps, 1–500 bps | Entry deviates from the oracle mid by more than your tolerance | Reverts; intent stays pending; escrow refundable; no fee |
| LiquidatableAtOpen | Minting a position that is already liquidatable | No — fixed protocol invariant | Equity ≤ 10% maintenance margin at the fill mark | Reverts; intent stays pending; escrow refundable; no fee |
Together they bound what the keeper can do. The keeper chooses the timing and signs the price, but it can only ever fill you at a verified oracle price, within the slippage tolerance you signed, and never into an immediately liquidatable position. It is non-custodial: your funds are escrowed by the Diamond, never held by the keeper.
Setting Max slippage in the UI
In the order ticket you choose a Max slippage preset — 0.10%, 0.30%, 0.50%, or 1.00%. That value is your maxSlippageBps (10, 30, 50, or 100 bps). The app never submits an intent without one, so there is no way to fire an order with an unset tolerance.
| Max slippage | In bps | Typical use |
|---|---|---|
| 0.10% | 10 bps | Deep majors (BTC, ETH), small size, calm markets |
| 0.30% | 30 bps | Liquid markets, moderate size |
| 0.50% | 50 bps | Larger size or thinner markets |
| 1.00% | 100 bps | Volatile conditions, or size large enough to move skew |
The trade-off is direct. A tighter tolerance gives stronger price protection but a higher chance the fill is rejected and retried when markets are fast or the book is crowded. A looser tolerance fills more reliably but accepts a wider spread. Pick the tolerance to your size and the market's depth, not the other way around.
- 1Open the order ticket and choose your market, direction, size, and leverage.
- 2Set Max slippage to the tolerance you are willing to accept (this becomes maxSlippageBps).
- 3Submit — your collateral is escrowed on-chain by the Diamond.
- 4A keeper fills at the next verified mark if the spread fits your tolerance and the position is not liquidatable-at-open; otherwise the intent stays pending and retries.
- 5If it is not filling and you have changed your mind, call cancelIntent to refund your escrow in full.
Scope: limits, closes, and liquidation
The slippage tolerance guards the open path. Market opens and limit/stop entries both carry maxSlippageBps and both are checked in executeOpen. A limit or stop additionally cannot fill before its trigger is crossed — the keeper's fill reverts NotTriggered until the mark reaches your triggerPrice, so a limit can never fill at a worse-than-trigger price *and* is still bounded by your slippage tolerance on top.
Closing applies the same deterministic spread to the mid (a close pays spread at the other end of the round trip) but does not carry a separate per-close slippage tolerance in this version. Liquidation is different again: it settles at the raw oracle mark with no adverse spread applied, and only ever affects the one liquidated position (margin is isolated).
