Reproduced Exploit
Blueberry HyperEvmVault: redeem preview converts on the caller, not the share owner
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-26. The historical source/toolchain is unavailable; this entry is documentation only and claims no executable Forge PoC.
Vulnerability classes: vuln/theft · vuln/logic
Reproduction: a faithful minimal reproduction of the vulnerable finding — the overridden
previewWithdraw/previewRedeembodies are reproduced verbatim (marked@>) with faithful minimal doubles; local deploy, no fork.
Root cause#
Withdrawals in HyperEvmVault are gated by a per-account redeemRequests snapshot (the shares and assets that settled back from L1). The overridden previewWithdraw() / previewRedeem() index that snapshot by msg.sender — the caller — even though ERC4626 supports a flow where the caller differs from the share owner. The vulnerable bodies, reproduced verbatim from the report:
function previewWithdraw(uint256 assets_) public view override(ERC4626Upgradeable, IERC4626) returns (uint256) {
V1Storage storage $ = _getV1Storage();
@> RedeemRequest memory request = $.redeemRequests[msg.sender];
return assets_.mulDivUp(request.shares, request.assets);
}
function previewRedeem(uint256 shares_) public view override(ERC4626Upgradeable, IERC4626) returns (uint256) {
V1Storage storage $ = _getV1Storage();
@> RedeemRequest memory request = $.redeemRequests[msg.sender];
return shares_.mulDivDown(request.assets, request.shares);
}
_withdraw() correctly burns the owner's shares and spends the owner's allowance, but the payout amount it transfers is computed by these previews off the caller's request. So a caller with a more favourable snapshot redeems the owner's shares at the caller's conversion rate.
Why it's exploitable here#
With numbers taken from the reproduction:
- Honest depositors seed the pool with
1000e18. - The victim
ownerdeposits100e18(mints100e18shares) and legitimately requests a redeem at the 1:1 L1 rate, soredeemRequests[owner] = {shares: 100e18, assets: 100e18}— worth exactly100e18. - The attacker deposits
100e18, sets the L1 settlement rate to 3x, and records its own request, soredeemRequests[attacker] = {shares: 100e18, assets: 300e18}. - The owner approves the attacker as a shares spender (
100e18) — the standard ERC4626 caller-differs-from-owner flow. - The attacker calls
redeem(100e18, attacker, owner).previewRedeemreadsredeemRequests[msg.sender]= the attacker's request, computing100e18.mulDivDown(300e18, 100e18) = 300e18._withdrawburns the owner's100e18shares but transfers300e18to the attacker — 3x the owner's fair value.
Net result: the attacker extracts 300e18 for a position worth 100e18, a 200e18 theft that drains honest depositors' pooled liquidity.
Attack path#
Marked-line walkthrough (Playground)#
The EVM Playground pins each step to the exact executed source line in 0x671d353a…:
- L50 — Underlying vault asset token: Setup: declares the vault's underlying ERC20, the pooled asset honest depositors fund and that the exploit ultimately drains.
- L117 — Owner approves the spender: Setup: the shares'
approve()grants a spender an allowance, enabling the ERC4626 flow where the redeeming caller differs from the share owner. - L137 — Deposit shares at 1:1: Setup: depositors mint vault shares 1:1 for assets; honest users and the victim owner both fund the pool here before requesting any redeem.
- L164 — Withdraw transfers preview amount: The
_withdrawcore burns the owner's shares but transfers theassetsamount the preview computed — the value the flawed preview inflates. - L207 — Attacker sets favorable redeem rate: Setup: writes the per-account L1 settlement rate; the attacker snapshots its own request at 3x while the owner's stayed at 1x.
- L222 — previewWithdraw indexes the caller:
previewWithdrawreadsredeemRequests[msg.sender](the caller) — the withdraw-path twin of the same bug, converting on the caller, not the owner. - L228 — previewRedeem uses caller not owner: Root cause:
previewRedeemreadsredeemRequests[msg.sender](the caller), so the owner's shares redeem at the caller's 3x rate — paying triple and draining the pool.
PoC#
Registry (Foundry, local deploy — verbatim vulnerable source + harm-asserting test):
cd 61469-h-01-flawed-withdrawal-logic-when-caller-differs-from-share_exp && forge test -vvv
The browser Playground replays the same synthetic opcode-for-opcode and measures the harm: the attacker redeems the owner's 100e18-worth shares for 300e18, a 200e18 theft that drains the pooled liquidity of honest depositors. 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-26.
- Upstream DeFiHackLabs PoC directory: src/test.
Alerts & third-party analyses
- DeFiHackLabs incident explorer: search "Blueberry HyperEvmVault: redeem preview converts on the caller, not the share owner".
- 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.