Reproduced Exploit

Sablier Bob Escrow — Circular slippage protection in `SablierLidoAdapter::_wstETHToWeth` enables sandwich attacks

1. _wstETHToWeth sets minEthOut from Curve get_dy (current reserves). 2. exchange reads the same reserves — so a sandwich that depresses the pool makes both quote and swap use the manipulated price. 3. Slippage tolerance only guards movement between get_dy and exchange

Mar 2026Otheroracle4 min read

Chain

Other

Category

oracle

Date

Mar 2026

Source

AuditVault

EVM Playground

Source-level debugger — step opcodes and Solidity in sync

evm-hack-analyzer

The attack is replayed in an in-browser EVM preloaded with the exact dumped fork state. The execution tree shows every call; step by Solidity line or by opcode across all depths — source, Stack, Memory, Storage, Balances (native / ERC-20 / NFT), Transient storage and Return value stay in sync. Click a tree node, opcode, or source line to jump. No backend, no live RPC.

Loading fork state…

Source & credit. Reproduction of a public audit finding curated by AuditVault — the original finding: 65583-circular-slippage-protection-in-sablierlidoadapter-wstethtow. Standalone Foundry PoC and full write-up: 65583-circular-slippage-protection-in-sablierlidoadapter-wstethtow_exp in the evm-hack-registry mirror.


Vulnerability classes: vuln/oracle/spot-price · impact/mev/sandwich · misassumption/price-cannot-be-manipulated

Reproduction: a self-contained Foundry PoC that compiles & runs in an isolated project with only forge-std — no fork, no RPC. Full trace: output.txt. PoC: test/65583-circular-slippage-protection-in-sablierlidoadapter-wstethtow_exp.sol.


Key info#

ImpactHIGH — sandwich attacker steals a portion of every adapter vault's WETH at unstake; loss is permanent for all subsequent redemptions
ProtocolSablier Bob Escrow (Lido adapter)
Vulnerable contractSablierLidoAdapter::_wstETHToWeth
Bug classOn-chain slippage derived from manipulable DEX spot price (circular check)
FindingCyfrin — Sablier Bob Escrow v2.0, 2026-03-25 · #65583
Reportsolodit Cyfrin report
SourceAuditVault
StatusAudit finding — fixed by replacing Curve get_dy with Chainlink oracle (2e0abaf). Reproduced here as a standalone local PoC.
Compiler^0.8.24 (PoC)

This is an audit finding, not a historical on-chain incident. The PoC models Curve reserve manipulation and shows the circular slippage check passing while the vault permanently receives a depressed WETH amount.


TL;DR#

  1. _wstETHToWeth sets minEthOut from Curve get_dy (current reserves).
  2. exchange reads the same reserves — so a sandwich that depresses the pool makes both quote and swap use the manipulated price.
  3. Slippage tolerance only guards movement between get_dy and exchange in the same tx (always zero) — the protection is circular.
  4. unstakeTokensViaAdapter is permissionless — attacker chooses timing.
  5. Depressed rate is written once to _wethReceivedAfterUnstaking → every user redeem forever pays less.

The vulnerable code#

SOLIDITY
function _wstETHToWeth(uint128 wstETHAmount) private returns (uint128 wethReceived) {
    uint256 stETHAmount = WSTETH.unwrap(wstETHAmount);

    // @> VULN: minEthOut derived from get_dy (manipulable current reserves)
    uint256 expectedEthOut = CURVE_POOL.get_dy(1, 0, stETHAmount);
    uint256 minEthOut = (expectedEthOut * (UNIT - slippageTolerance)) / UNIT;
    uint256 ethReceived = CURVE_POOL.exchange(1, 0, stETHAmount, minEthOut);

    if (ethReceived < minEthOut) revert("slippage exceeded");
    return uint128(ethReceived);
}

Use a Chainlink stETH/ETH oracle (or caller-supplied minEthOut) instead of the Curve spot quote:

SOLIDITY
uint256 oraclePrice = _getStETHToETHOraclePrice();
uint256 fairEthOut = stETHAmount * oraclePrice / 1e18;
uint256 minEthOut = ud(fairEthOut).mul(UNIT.sub(slippageTolerance)).unwrap();

Root cause#

On-chain slippage floors computed from the same AMM spot the swap executes against cannot detect a pre-transaction sandwich. The check only proves exchange did not move further after get_dy in the same atomic call — which is always true.


Preconditions#

  • Adapter vault with staked wstETH ready to unstake.
  • Attacker can manipulate Curve stETH/ETH reserves (flashloan) and call unstakeTokensViaAdapter (permissionless once settled).

Attack walkthrough#

  1. Front-run: dump stETH into Curve → stETH/ETH rate −4%.
  2. Call unstakeTokensViaAdapter:
    • get_dy → 96 (depressed)
    • minEthOut → 96 × 0.995 = 95.52
    • exchange → 96, passes
  3. Back-run: restore pool; keep sandwich profit (4 WETH in the PoC).
  4. _wethReceivedAfterUnstaking = 96 forever → all redemptions short ~4%.

Diagrams#

sequenceDiagram participant Att as Attacker participant Curve as Curve stETH/ETH participant Bob as SablierBob participant Ad as SablierLidoAdapter Att->>Curve: front-run dump stETH #59; rate -4pct Att->>Bob: unstakeTokensViaAdapter Bob->>Ad: unstakeFullAmount / _wstETHToWeth Ad->>Curve: get_dy #59; returns depressed 96 Note over Ad: minEthOut = 96 * 0.995 = 95.52 Ad->>Curve: exchange #59; pays 96 #59; check passes Note over Ad: wethReceivedAfterUnstaking = 96 permanent Att->>Curve: back-run #59; sandwich profit 4 WETH
flowchart TD fr["Front-run manipulate reserves"] --> gd["get_dy reads depressed spot"] gd --> min["minEthOut = depressed * tolerance"] min --> ex["exchange at depressed rate"] ex --> pass{"ethReceived >= minEthOut?"} pass -->|"yes always under sandwich"| store["store depressed wethReceived"] store --> harm["All user redeems permanently reduced"]

Impact#

  • Attacker steals the gap between fair and manipulated stETH/ETH rate on every adapter-vault unstake they sandwich.
  • Loss capped per vault by max slippage tolerance (~5%), but cumulative across all vaults the attacker monitors.
  • PoC: 100 WETH vault → 96 to users, 4 WETH sandwich profit; ≥3% permanent loss.

Taxonomy (AuditVault)#

  • severity/high
  • sector/dex · sector/liquid-staking · sector/streaming · sector/oracle
  • platform/cyfrin
  • genome: spot-price · sandwich · defi/sandwich-attack · known-pattern
  • misassumption/oracle-is-reliable · misassumption/price-cannot-be-manipulated
  • fix/use-twap

Sources#


Sources & further analysis#

Reproductions & code

Alerts & third-party analyses

  • Web3Sec X hacked database: search.
  • Rekt leaderboard: search.
  • Solodit incident search: search.

These dashboards index community alerts tweets, post-mortems, and independent write-ups. Reach them through the protocol name above to cross-check this reproduction against other analyses.