Reproduced Exploit
Buffer `BufferBinaryPool.send()` permanently locks LP funds on an early exercise
Chain
Other
Category
dos
Date
Jul 2023
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: 55634-h-02-bufferbinarypool-can-permanently-lock-funds-on-early-ex. Standalone Foundry PoC and full write-up: 55634-h-02-bufferbinarypool-can-permanently-lock-funds-on-early-ex_exp in the
evm-hack-registrymirror.
Vulnerability classes: vuln/dos/frozen-funds · impact/loss-of-funds/locked-funds · novelty/variant
Reproduction: the test deploys the REAL audited Buffer v2.5
BufferBinaryPool+BufferBinaryOptions+OptionsConfig, opens a real option that locks 100 USDC of LP liquidity, then exercises it EARLY through the realunlock → _exercise → pool.sendpath. The early exercise pays a partialprofit < lockedAmount, and the pool'ssend()unlocks only the amount paid — leaving the remainder counted as locked forever and blocking the LP's withdrawal.
Root cause#
When an option is exercised, BufferBinaryOptions._exercise
computes the payout. For an EARLY exercise (option.expiration > closingTime) the payout is a
partial Black–Scholes value, strictly less than the locked size:
if (option.expiration > closingTime) {
profit = (option.lockedAmount * OptionMath.blackScholesPriceBinary(...)) / 1e8; // < lockedAmount
} else {
profit = option.lockedAmount;
}
pool.send(optionID, user, profit);
The pool's send then decrements lockedAmount by the amount
sent, not by the full option size it is unlocking:
uint256 transferTokenXAmount = tokenXAmount > ll.amount ? ll.amount : tokenXAmount;
ll.locked = false; // the whole option is now closed
lockedPremium = lockedPremium - ll.premium;
lockedAmount = lockedAmount - transferTokenXAmount; // @> only the PARTIAL payout is unlocked
tokenX.safeTransfer(to, transferTokenXAmount);
The option is fully closed (ll.locked = false, NFT burned), yet lockedAmount is only reduced
by profit. The lockedAmount - profit remainder stays counted as locked forever. Because
availableBalance() = totalTokenXBalance() - lockedAmount, that remainder is permanently
subtracted from what LPs can withdraw — even though the tokens physically sit in the pool with no
option backing them.
Exploit walkthrough (numbers from the test)#
- An LP seeds the pool with 1,000 USDC.
- A trader opens an option locking 100 USDC (
lockedAmount = 100,premium = 50). - The trader exercises early (1,800s before expiry) at an at-the-money price, so the binary
pays a partial ~49.86 USDC (
profit < 100). send()unlocks only ~49.86, solockedAmountis stuck at ~50.14 USDC even though the option is fully closed.availableBalance()is now permanently short by ~50.14. The sole LP owns 100% of the pool but their full-sharewithdraw()reverts with"Pool: Not enough funds on the pool contract."The ~50.14 USDC is locked forever.
Negative control (in the same test): a FULL exercise (closingTime >= expiration, so
profit == lockedAmount) unlocks the entire amount and leaves lockedAmount == 0 — proving the
lock is caused specifically by the partial-payout branch of send().
Fix#
send() must unlock the full option size, not the amount sent. Buffer's remediation
(PR #7) redeems option.lockedAmount
to the option contract, sends profit to the user, and returns any remainder to the pool.
Reproduction#
- Registry PoC:
_shared/run-poc/run_poc.sh 55634-h-02-bufferbinarypool-can-permanently-lock-funds-on-early-ex_exp -vvvvv - Real audited source under
src/at commit84b6060b4447b2550de595202e8820c7f515988b.
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 55634-h-02-bufferbinarypool-can-permanently-lock-funds-on-early-ex_exp (evm-hack-registry mirror).
- AuditVault finding: 55634-h-02-bufferbinarypool-can-permanently-lock-funds-on-early-ex.
- Upstream DeFiHackLabs PoC directory: src/test.
Alerts & third-party analyses
- DeFiHackLabs incident explorer: search "Buffer
BufferBinaryPool.send()permanently locks LP funds on an early exercise". - 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.