Reproduced Exploit
Blueberry HyperEvmVault: `_calculateFee` double-subtracts `requestSum.assets`
Chain
Other
Category
logic
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/logic/fee-calculation · vuln/arithmetic/underflow
Reproduction: a faithful minimal reproduction of the vulnerable finding — the
_calculateFeeand_totalEscrowValuebodies are reproduced verbatim (the double-subtracting line marked@>) with faithful minimal doubles for the escrows,requestSum, and fee config; local deploy, no fork.
Root cause#
_calculateFee(grossAssets) subtracts $.requestSum.assets from grossAssets to size the fee base — but grossAssets is passed straight from _totalEscrowValue, which already returned assets_ - $.requestSum.assets. The pending-redemption amount is therefore subtracted twice, so the management fee is levied on a doubly-reduced value (or the second subtraction underflows and reverts). The vulnerable line, reproduced verbatim from the report:
function _calculateFee(V1Storage storage $, uint256 grossAssets) internal view returns (uint256 feeAmount_) {
if (grossAssets == 0 || block.timestamp <= $.lastFeeCollectionTimestamp) {
return 0;
}
// Calculate time elapsed since last fee collection
uint256 timeElapsed = block.timestamp - $.lastFeeCollectionTimestamp;
// We subtract the pending redemption requests from the total asset value to avoid taking more fees than needed from
// users who do not have any pending redemption requests
@> uint256 eligibleForFeeTake = grossAssets - $.requestSum.assets;
// Calculate the pro-rated management fee based on time elapsed
feeAmount_ = eligibleForFeeTake * $.managementFeeBps * timeElapsed / BPS_DENOMINATOR / ONE_YEAR;
return feeAmount_;
}
The upstream _totalEscrowValue — also reproduced verbatim — is where the first, legitimate subtraction happens, which is what makes the @> line a double subtraction:
function _totalEscrowValue(V1Storage storage $) internal view returns (uint256 assets_) {
uint256 escrowLength = $.escrows.length;
for (uint256 i = 0; i < escrowLength; ++i) {
VaultEscrow escrow = VaultEscrow($.escrows[i]);
assets_ += escrow.tvl();
}
if ($.lastL1Block == l1Block()) {
assets_ += $.currentBlockDeposits;
}
@> return assets_ - $.requestSum.assets;
}
Why it's exploitable here#
The reproduction configures a vault with TVL = 1000e18 held in one escrow, requestSum.assets = 400e18 of pending redemptions, a 2% annual management fee, and timeElapsed = ONE_YEAR:
_totalEscrowValue()returns1000e18 - 400e18 = 600e18— this is thegrossAssetshanded to_calculateFee.- The
@>line computeseligibleForFeeTake = 600e18 - 400e18 = 200e18, subtracting the pending amount a second time. - Buggy fee:
200e18 * 2% = 4e18. Correct fee (finding's fix — do not subtract again):600e18 * 2% = 12e18. - The fee recipient is shorted
12e18 - 4e18 = 8e18every collection — a full fee on the entire400e18pending amount. WhenrequestSum.assetsexceeds the already-reducedgrossAssets, the second subtraction underflows and reverts, bricking fee collection.
Attack path#
Marked-line walkthrough (Playground)#
The EVM Playground pins each step to the exact executed source line in 0x671d353a…:
- L135 — Total escrow value nets pending once:
_totalEscrowValuesums each escrow's TVL and returns it already reduced by$.requestSum.assets— the one legitimate subtraction of pending redemptions. - L149 — Fee function receives net assets:
_calculateFeeis handedgrossAssets, which is exactly the value_totalEscrowValuealready netted down by the pending redemptions. - L159 — Pending redemptions subtracted a second time: Root cause:
grossAssets - $.requestSum.assetssubtracts pending redemptions again, double-counting them and shrinking the fee base too far. - L161 — Fee levied on doubly-reduced base: The fee is pro-rated over
eligibleForFeeTake, charging it onTVL - 2*requestSuminstead ofTVL - requestSum, so the protocol is undercharged. - L178 — previewFee wires the two together:
previewFeecalls_calculateFee(_totalEscrowValue()), feeding the already-netted value straight into the buggy line and realizing the double subtraction. - L185 — Seed the vault with TVL: Setup:
addEscrowregisters one escrow holding the full 1000e18 TVL that_totalEscrowValuewill sum. - L190 — Record pending redemption amount: Setup:
setRequestSumAssetsstores 400e18 of pending redemptions — the value that ends up subtracted twice.
PoC#
Registry (Foundry, local deploy — verbatim vulnerable source + harm-asserting test):
cd 61468-c-01-incorrect-fee-due-to-double-subtracting-requestsumasset_exp && forge test -vvv
The browser Playground replays the same synthetic opcode-for-opcode and measures the harm: with TVL = 1000e18, requestSum = 400e18, and a 2% annual fee, the verbatim _calculateFee(_totalEscrowValue()) chain charges 4e18 instead of 12e18, and the 8e18 under-collected fee is minted to the SINK marker as the harm magnitude. 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:
_calculateFeedouble-subtractsrequestSum.assets". - 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.