Reproduced Exploit
Ostium: `unlockDeposit()` socializes deposit discounts into trader PnL, leaving the vault insolvent
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: Ostium-security-review_2025-09-14. The historical source/toolchain is unavailable; this entry is documentation only and claims no executable Forge PoC.
Vulnerability classes: vuln/accounting · vuln/insolvency
Reproduction: a faithful minimal reproduction of the vulnerable finding — the discount-socialization block of
unlockDeposit()is reproduced verbatim (marked@>) alongside the verbatim gToken price math, with faithful minimal doubles (ERC20 asset, share ledger, locked-deposit NFT); local deploy, no fork.
Root cause#
When a locked deposit unlocks, the discount it was granted must be socialized so each share's redemption value drops accordingly. Ostium's unlockDeposit() instead adds the per-token discount (accPnlDelta) to accPnlPerTokenUsed — the trader profit-and-loss accumulator — and then calls updateShareToAssetsPrice(), which only marks the price down while that accumulator is positive. The vulnerable block, reproduced verbatim:
int256 accPnlDelta = d.assetsDiscount.mulDiv(PRECISION_18, totalSupply(), Math.Rounding.Ceil).toInt256();
accPnlPerToken += accPnlDelta;
if (accPnlPerToken > maxAccPnlPerToken().toInt256()) {
revert NotEnoughAssets();
}
lockedDepositNft.burn(depositId);
@> accPnlPerTokenUsed += accPnlDelta;
updateShareToAssetsPrice();
accPnlPerTokenUsed tracks net trader PnL and is the healthy vault's negative number (traders net-losing ⇒ vault over-collateralized). updateShareToAssetsPrice() computes maxAccPnlPerToken() - (accPnlPerTokenUsed > 0 ? accPnlPerTokenUsed : 0), so while the accumulator stays <= 0 the discount added on the marked line changes the price by nothing. The discounted shares remain in totalSupply() but their redemption value is never priced down, so totalSupply() * shareToAssetsPrice exceeds the assets actually backing them. The fix is to distribute the discount through accRewardsPerToken, which feeds the price unconditionally.
Why it's exploitable here#
Following the reproduced scenario (all amounts 18-decimal):
- Alice LPs
800e18at price1.0→800e18shares; the vault holds800e18. - Traders net-lose
100e18; the vault now holds900e18backing800e18shares — a100e18solvency buffer,accPnlPerTokenUsed = -0.125e18, price clamps at1.0. - Bob makes a locked deposit of
100e18real assets plus a120e18discount →220e18shares are minted at price1.0. Supply is now1020e18; the vault holds1000e18. - Unlock runs the verbatim block:
accPnlDelta ≈ 0.1176e18is added toaccPnlPerTokenUsed, moving it to-0.0074e18— still<= 0, soupdateShareToAssetsPrice()leaves the price at1.0. - Now
supply * price = 1020e18while the vault holds only1000e18: a20e18insolvency shortfall. Correct socialization would have dropped the price to~0.882e18(market cap~900e18 <= 1000e18), keeping the vault solvent. The shortfall is minted to the sink as the concrete harm.
Attack path#
Marked-line walkthrough (Playground)#
The EVM Playground pins each step to the exact executed source line in 0x671d353a…:
- L58 — SafeCast provides the int cast: Setup: the SafeCast library reproduces OpenZeppelin's
.toInt256(), the exact cast the verbatim vulnerable line applies to the discount delta. - L151 — Rewards accumulator the fix targets: Setup:
accRewardsPerTokenfeeds the share price unconditionally viamaxAccPnlPerToken()— the accumulator the discount should have gone into. - L167 — Vault deploys locked-deposit NFT: Setup: the constructor deploys the locked-deposit NFT that is minted on
makeLockedDepositand burned when a matured deposit later unlocks. - L195 — Price drops only on positive PnL:
updateShareToAssetsPrice()subtractsaccPnlPerTokenUsedfrom the price only when it is positive, so a non-positive accumulator leaves the price frozen. - L219 — Trader losses drive PnL negative: Setup: socialized trader losses push
accPnlPerTokenUsedbelow zero — the healthy over-collateralized state where the price clamps at its max. - L252 — Compute the per-token discount: The verbatim unlock block computes
accPnlDelta: the unlocking deposit's discount spread across every share, ceil-rounded. - L261 — Discount added to trader-PnL accumulator: Root cause: the discount is added to
accPnlPerTokenUsed(trader PnL), notaccRewardsPerToken; while it stays <=0 the price never drops, so supply*price > assets.
PoC#
Registry (Foundry, local deploy — verbatim vulnerable source + harm-asserting test):
cd 65199-h-01-function-unlockdeposit-adds-discounted-assets-as-trader_exp && forge test -vvv
The browser Playground replays the same synthetic opcode-for-opcode and measures the harm: a matured discounted deposit unlocks, the discount is socialized into the trader-PnL accumulator, the share price is never marked down, and the vault is left insolvent by 20e18. 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: Ostium-security-review_2025-09-14.
- Upstream DeFiHackLabs PoC directory: src/test.
Alerts & third-party analyses
- DeFiHackLabs incident explorer: search "Ostium:
unlockDeposit()socializes deposit discounts into trader PnL, leaving the vault insolvent". - 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.