Reproduced Exploit

[H-01] Buffer Finance v2.5 — payments for coupons to the Booster are irretrievable

Jul 2023Otherlogic3 min read

Chain

Other

Category

logic

Date

Jul 2023

Source

AuditVault

EVM Playground

Source-level debugger — step opcodes and Solidity in sync

evm-hack-analyzer

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.

Loading fork state…

Source & credit. Reproduction of a public audit finding curated by AuditVault — the original finding: 55633-h-01-payments-for-coupons-to-booster-are-irretrievable-0x52. Standalone Foundry PoC and full write-up: 55633-h-01-payments-for-coupons-to-booster-are-irretrievable-0x52_exp in the evm-hack-registry mirror.


Vulnerability classes: vuln/logic/locked-funds · vuln/accounting/fee-sink · frozen-funds

Reproduction: the test deploys the historical Buffer Booster contract and calls its real buy entry point. The coupon payment is safeTransferFrom'd into the Booster, and the contract exposes no function that can ever move that ERC20 back out — the money is locked forever.

Root cause#

The audited Booster at commit 84b6060 collects coupon payments into itself:

SOLIDITY
function buy(address tokenAddress, uint256 traderNFTId) external {
    ...
    uint256 price = couponPrice - discount;
    require(token.balanceOf(user) >= price, "Not enough balance");

    token.safeTransferFrom(user, address(this), couponPrice); // <-- payment lands in the Booster
    userBoostTrades[tokenAddress][user].totalBoostTrades += MAX_TRADES_PER_BOOST;

    emit BuyCoupon(tokenAddress, user, couponPrice);
}

The full Booster surface is: setConfigure, getUserBoostData, updateUserBoost, getBoostPercentage, setPrice, setBoostPercentage, buy, plus the inherited Ownable / AccessControl role helpers. None of them transfer an ERC20 out of the contract, and there is no fallback/receive. Every unit paid for a coupon is therefore permanently frozen — neither the payer, the owner, nor the admin can recover it.

The remediation (PR #8) routes the fee to the admin instead of the Booster, confirming the destination address is the bug.

The exact historical contract is vendored at src/core/Booster.sol; its interfaces at src/interfaces/Interfaces.sol. OpenZeppelin 4.9.3 (Ownable/AccessControl/SafeERC20/ERC20) is vendored under src/oz.

flowchart LR U["User"] -->|"buy(): safeTransferFrom(user, this, couponPrice)"| B["Booster (real)"] B -->|"credits totalBoostTrades"| B B -.->|"withdraw / sweep / recover / claimFees"| X["(no such function — reverts)"] B ==>|"5000 PAY held forever"| L["Locked funds"]

Reproduction#

The fixture deploys the real Booster, a minimal real ERC20 as the opaque coupon-payment token, and a minimal trader-NFT stub (an out-of-scope external boundary whose owner never matches the buyer, so the full couponPrice flows in). A user funds, approves, and buys one coupon.

BASH
cd 55633-h-01-payments-for-coupons-to-booster-are-irretrievable-0x52_exp
forge test -vvv

Expected result: 1 passed. The assertions in test/55633-h-01-payments-for-coupons-to-booster-are-irretrievable-0x52_exp.sol verify that:

  1. the buyer pays the full couponPrice (5,000 PAY) and the Booster holds it;
  2. the boost credit is granted (the payment was a legitimate, expected one); and
  3. every plausible retrieval selector (withdraw, sweep, recoverERC20, rescueTokens, emergencyWithdraw, transferToken, claimFees, …) reverts with "unrecognized function selector … no fallback" — so no egress path exists and the 5,000 PAY remains locked in the Booster after all attempts. Harm: profit == 0 attacker-side; the concrete loss is the permanently frozen coupon revenue.

Sources#


Sources & further analysis#

Reproductions & code

Alerts & third-party analyses

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.