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
Chain
Other
Category
oracle
Date
Mar 2026
Source
AuditVault
EVM Playground
Source-level debugger — step opcodes and Solidity in sync
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.
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-registrymirror.
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#
| Impact | HIGH — sandwich attacker steals a portion of every adapter vault's WETH at unstake; loss is permanent for all subsequent redemptions |
| Protocol | Sablier Bob Escrow (Lido adapter) |
| Vulnerable contract | SablierLidoAdapter::_wstETHToWeth |
| Bug class | On-chain slippage derived from manipulable DEX spot price (circular check) |
| Finding | Cyfrin — Sablier Bob Escrow v2.0, 2026-03-25 · #65583 |
| Report | solodit Cyfrin report |
| Source | AuditVault |
| Status | Audit 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#
_wstETHToWethsetsminEthOutfrom Curveget_dy(current reserves).exchangereads the same reserves — so a sandwich that depresses the pool makes both quote and swap use the manipulated price.- Slippage tolerance only guards movement between
get_dyandexchangein the same tx (always zero) — the protection is circular. unstakeTokensViaAdapteris permissionless — attacker chooses timing.- Depressed rate is written once to
_wethReceivedAfterUnstaking→ every user redeem forever pays less.
The vulnerable code#
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);
}
Recommended fix#
Use a Chainlink stETH/ETH oracle (or caller-supplied minEthOut) instead of
the Curve spot quote:
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#
- Front-run: dump stETH into Curve → stETH/ETH rate −4%.
- Call
unstakeTokensViaAdapter:get_dy→ 96 (depressed)minEthOut→ 96 × 0.995 = 95.52exchange→ 96, passes
- Back-run: restore pool; keep sandwich profit (4 WETH in the PoC).
_wethReceivedAfterUnstaking= 96 forever → all redemptions short ~4%.
Diagrams#
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/highsector/dex·sector/liquid-staking·sector/streaming·sector/oracleplatform/cyfrin- genome:
spot-price·sandwich·defi/sandwich-attack·known-pattern misassumption/oracle-is-reliable·misassumption/price-cannot-be-manipulatedfix/use-twap
Sources#
- AuditVault finding #65583
- Cyfrin report — Sablier Bob Escrow v2.0 (2026-03-25)
- Vulnerable source: sablier-labs/lockup@3c669df —
bob/src/SablierLidoAdapter.sol_wstETHToWeth(parent of oracle fix2e0abaf) - Fix: 2e0abaf — Chainlink oracle for minEthOut · 7fae842 — revert on zero oracle price
- Pattern reference: On-chain slippage calculation can be manipulated
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 65583-circular-slippage-protection-in-sablierlidoadapter-wstethtow_exp (evm-hack-registry mirror).
- AuditVault finding: 65583-circular-slippage-protection-in-sablierlidoadapter-wstethtow.
Alerts & third-party analyses
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.