Reproduced Exploit
Blueberry: `requestRedeem` never deducts redemption requests from vault totals
Chain
Other
Category
untagged
Date
Jan 1970
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: Blueberry-security-review_2025-03-12. The historical source/toolchain is unavailable; this entry is documentation only and claims no executable Forge PoC.
Vulnerability classes: vuln/logic · vuln/accounting
Reproduction: a faithful minimal reproduction of the vulnerable finding —
HyperEvmVault.requestRedeemis reproduced verbatim (marked@>) with faithful minimal doubles for the ERC20 shares, escrow, and finalize paths; local deploy, no fork.
Root cause#
HyperEvmVault.requestRedeem() records a redemption request into request and $.totalRedeemRequests, but never subtracts the requested shares/assets from totalSupply() or the tvl() escrow. Until the request is finalized, every share-price calculation (assets = shares.mulDivDown(tvl(), totalSupply())) still counts the already-spoken-for shares and assets, so the reported price stays stale and inflated for every other user. The vulnerable lines, reproduced verbatim:
function requestRedeem(uint256 shares_) external nonReentrant {
V1Storage storage $ = _getV1Storage();
uint256 balance = this.balanceOf(msg.sender);
// Determine if the user withdrawal request is valid
require(shares_ <= balance, Errors.INSUFFICIENT_BALANCE());
RedeemRequest storage request = $.redeemRequests[msg.sender];
request.shares += shares_;
require(request.shares <= balance, Errors.INSUFFICIENT_BALANCE());
// User will redeem assets at the current share price
uint256 tvl_ = _totalEscrowValue($);
_takeFee($, tvl_);
uint256 assetsToRedeem = shares_.mulDivDown(tvl_, totalSupply());
request.assets += uint64(assetsToRedeem);
@> $.totalRedeemRequests += uint64(assetsToRedeem); // recorded, but totalSupply() and tvl() are NOT reduced
--snip--
}
request.assets and $.totalRedeemRequests are incremented, so the claim is locked at the current price — but totalSupply() and the escrow returned by _totalEscrowValue()/tvl() are left untouched. The redemption is fully spoken for, yet the vault still prices everyone else as if those shares and assets were unclaimed.
Why it's exploitable here#
Following the finding's worked example on a 6-decimal token:
- WHALE deposits
900,000and LATE deposits100,000at price 1 — escrow andtotalSupply()both reach1,000,000. - WHALE calls
requestRedeemfor all900,000shares, locking a900,000claim at price 1. The vulnerable line records the request but leavestotalSupply()at1,000,000and the escrow at1,000,000. - The vault loses
100,000to bad debt — escrow drops to900,000, supply still1,000,000. - LATE calls
requestRedeemfor its100,000shares. Priced against the stale totals (900,000 / 1,000,000 = 0.9), it is credited a90,000claim — backed by assets already reserved for WHALE. - Outstanding claims
900,000 + 90,000 = 990,000now exceed the900,000escrow: the vault is insolvent by90,000. WHALE finalizes first and is paid900,000in full, draining escrow; LATE finalizes and receives nothing —90,000of user funds are frozen/lost.
Attack path#
Marked-line walkthrough (Playground)#
The EVM Playground pins each step to the exact executed source line in 0x671d353a…:
- L54 — Underlying hUSD uses 6 decimals: Setup: the vault escrows a 6-decimal hUSD token, fixing the unit for every share-price and redemption-claim calculation that follows.
- L113 — Deposits mint vault shares: Setup: each deposit mints shares and grows totalSupply; both honest users deposit at price 1, so escrow and supply both reach 1,000,000.
- L118 — Finalize burns requester shares: On finalize the vault burns the requester's shares to settle their price-locked claim against whatever escrow is left.
- L143 — Reentrancy guard armed: Setup: requestRedeem's nonReentrant guard starts unlocked; the flaw is missing accounting, not reentrancy, so the guard never helps.
- L175 — Deposit reads escrow for price: Setup: deposit reads current escrow (tvl) to price minted shares; this escrow-over-supply ratio is exactly what requestRedeem later leaves stale.
- L205 — Requests never deducted from totals: Root cause: requestRedeem records the request but never subtracts it from totalSupply()/tvl(), so a later redeemer is mispriced and finalize can't pay all.
- L209 — Finalize pays the locked claim: finalizeRedeem pays each price-locked claim from remaining escrow; the whale drains it in full, leaving the late redeemer's 90,000 claim unpaid.
PoC#
Registry (Foundry, local deploy — verbatim vulnerable source + harm-asserting test):
cd 61454-h-01-unapproved-requests-are-not-deducted-from-total-assets_exp && forge test -vvv
The browser Playground replays the same synthetic opcode-for-opcode and measures the harm: two users deposit at price 1, a whale requests redemption, the vault loses value, and a later redeemer is credited a claim against the stale totals — outstanding claims (990,000) exceed escrow (900,000), leaving the vault insolvent by 90,000 hUSD. Both gates are green (registry forge test PASS + Playground _verify-poc VERDICT: PASS).
Sources & further analysis#
Reproductions & code
- No executable Forge reproduction is claimed; the historical source/toolchain was unavailable for this finding.
- AuditVault finding: Blueberry-security-review_2025-03-12.
- Upstream DeFiHackLabs PoC directory: src/test.
Alerts & third-party analyses
- DeFiHackLabs incident explorer: search "Blueberry:
requestRedeemnever deducts redemption requests from vault totals". - 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.