Skip to content
BSC reserve manipulation exploit pattern — thin-liquidity pools, burn mechanics, and reserve-derived pricing audit checklist
researchMay 20, 20262 min read

Reserve Manipulation Isn't Dead

Alex Rybalko
Alex RybalkoCo-Founder & CEO

Updated on May 20, 2026

Between February and April 2026, attackers drained roughly $3 million from seven different BSC pools using the same primitive. None of the contracts had a logic bug in the conventional sense. None of them broke a Solidity guarantee. Each one shipped a token that did something — burn, fee, deferred destruction — to its own balance inside the LP pair, then someone called sync() afterward, and the pool happily re-read its own poisoned reserves and quoted a price.

The biggest of those was TMM/USDT on April 4, drained for $1.665M. It was not novel. It was not even original. It was the 2026 version of the bZx attack from February 2020 — the original DeFi reserve-manipulation case — running unchanged six years later on a different chain. And it is going to keep happening, because the audit class that catches it stopped being treated as a real surface area sometime around 2022, when the industry collectively decided "oracle manipulation" was a solved problem.

It isn't. This is the audit checklist that would have caught all seven.

TL;DR

  • Reserve manipulation is the dominant BSC exploit class of 2026 H1. SlowMist's Hacked archive lists at least seven incidents from Feb–Apr 2026 (LAXO, MT, AM, BCE, SAS, TMM, MONA, JUDAO) all sharing the same root cause.
  • The mechanism is unchanged since 2020. A token transfer/burn hook mutates the LP pair's balance without going through swap(). A subsequent sync() re-reads stale reserves. A flash-loaned swap extracts the difference. bZx, Harvest, Cream, Inverse, BonqDAO, EraLend, Polter, TMM. Different chain, different decade, same primitive.
  • The auditing industry mostly stopped looking. The mechanism feels solved because the canonical mitigation (Chainlink, TWAPs) is widely known. The number of tokens shipping reserve-derived pricing anyway suggests the knowledge has not propagated to the long tail of new launches.
  • The checklist is short. Eight items below. If you ship a token with a transfer hook, an LP pair, and a price-sensitive consumer, your auditor should be testing for every one of them.

What actually happened to TMM

The Halborn breakdown is the most complete public source, and the chain is worth following in detail because it generalizes.

The attacker started with no capital. They borrowed USDT from five flash-loan sources at once — ListaDAO Moolah, Venus, Aave V3, the PancakeSwap Vault, and Uniswap V4 PoolManager — for roughly $123M in working capital. With that, they swapped USDT for TMM through the pool at 0xc36c718e7d0af055092e5274f92f6511820ca041, accumulating 850M TMM tokens.

Then the trick. The attacker sent the TMM directly to the burn address 0x...dEaD. The pool was not notified. The TMM token contract did not call sync(). The pair's stored _reserve0 and _reserve1 still showed the pre-burn state. But the pair's actual balanceOf(TMM) was now one TMM. The reserves were one TMM and several hundred million USDT, on paper. On the blockchain, they were stale numbers that no longer described anything real.

A subsequent skim() (or in some variants, a manipulated swap that triggered the internal sync via _update) realigned the stored reserves to the actual balance. The price quote function — anything reading getReserves() directly — now reported TMM as worth roughly the entire USDT side of the pool divided by one. The attacker then bought back 272M USDT worth of TMM at the distorted price, repaid the flash loans, and walked away with ~$1.665M.

There is no overflow. There is no reentrancy. There is no privileged access. The contract behaves exactly as written. The problem is that the contract was written.

The 2026 pattern

TMM is not an isolated story. The same primitive ran in six other BSC pools in the eleven weeks before it, and twice in the weeks after.

DateTokenLossMechanism
2026-02-22LAXO/USDT~$137KBurn executed sync() after transferring LAXO to dead address; flash-loaned 350K USDT, withdrew ~487K.
2026-03-10MT/WBNB~$242KdistributeFees() burned MT from the pair directly.
2026-03-12AM/USDT~$131KBurn-mechanism flaw deflated AM reserves.
2026-03-23BCE/USDT~$679KscheduledDestruction burned BCE from pair + called sync(); $123.5M flash loan.
2026-04-02SAS~$12KsellBurn global counter burned on subsequent transfers.
2026-04-04TMM/USDT~$1,665KBurn to dead address; pool reserves not resynced before swap.
2026-04-14MONA~$61KDeferred LP-burn allowed direct pool draining.
2026-04-28JUDAO/USDT~$228KCustom balance-update logic miscounted reserves after a $2.3M flash-loaned swap.

Total: ~$3.16M across eight incidents in ten weeks. None of these tokens were major. All of them shipped. Most of the postmortems were filed by Verichains, BlockSec, and SlowMist within days, and read like carbon copies of each other.

The shared root cause is worth stating cleanly because it is the same sentence every time: a token contract mutates the LP pair's balance through a side channel that is not the AMM swap() interface, and the pair's reserves are subsequently re-synced from those mutated balances. Once that primitive exists, the rest of the attack is mechanical — a flash loan, a swap into the pool, the side-channel burn, the resync, the swap back out.

Why reserve manipulation never went away

The mechanism is older than half the protocols in the top 100.

bZx (February 2020, ~$630K) is the canonical case. The attacker flash-loaned ETH, used it to pump sUSD on Kyber (which sourced its price from Uniswap reserves), borrowed against the inflated collateral, and exited. The mechanism: a price oracle that read spot reserves from a thin DEX pool. It was the second of two bZx attacks that week. Both had the same root cause.

Harvest Finance (October 2020, ~$24M) ran the Curve y-pool version. Flash-loan in, skew the get_virtual_price, deposit Harvest fUSDC at the favorable rate, unwind. Same primitive, fancier pool math.

Cream Finance (October 27, 2021, ~$130M) made the share-price version explicit. Cream's PriceOracleProxy computed yUSDVault share price as totalValue / totalSupply. The attacker shrank the supply, donated yUSD to the vault to inflate totalValue, and pushed perceived crYUSD value to roughly 2× actual. They used the inflated collateral to borrow $130M and exit.

Inverse Finance (April 2022, ~$15.6M) made the TWAP version explicit. INV/WETH on SushiSwap was thin; the Keep3r TWAP sampled too few slots; a capital-intensive (not flash-loaned) push of the SushiSwap reserves moved the TWAP enough to over-borrow DOLA, ETH, WBTC, and YFI before the average could catch up.

Mango Markets (October 2022, ~$117M) — different surface, same pattern. Eisenberg cross-collateralized MNGO perps, pumped MNGO spot on three thin oracle sources (FTX, AscendEX, Serum), and borrowed against the inflated collateral. Centralized exchanges with depth are still thin if your protocol uses a low-volume venue as a price source.

BonqDAO (February 2023, ~$1.7M realized, ~$120M notional) was the Tellor variant — anyone with a 10 TRB stake (~$175 at the time) could submit price data, attacker set WALBT to $5M, minted 100M BEUR and liquidated 30+ troves. The "$120M" headline number was notional; the protocol's TVL was too thin to actually realize. The protocol died anyway.

Hundred Finance (April 2023, ~$7M) was the donation variant on Optimism. A near-empty hWBTC market, a 200 WBTC donation, a rounding error that let ~2 wei of hWBTC redeem the entire pool.

EraLend (July 2023, ~$2.7M) was the read-only reentrancy variant. SyncSwap LP burn, callback fires before _updateReserves, price computed from stale reserves. Same mechanism, with a reentrancy primitive added.

Polter Finance (November 2024, ~$8.7M) is the cleanest recent echo. A Geist/Aave v2 fork on Fantom listed BOO using SpookySwap V2/V3 spot price as oracle. Flash-loan, drain the SpookySwap pool, BOO is now valued at $1.37 trillion per token by the lending protocol's oracle, attacker borrows everything. This is bZx 2020 unchanged. Four and a half years later. Same chain category, same primitive, eight figures.

Then TMM, four months later, on BSC. Then JUDAO three weeks after that.

The lesson is not that auditors do not know this attack class exists. They do. The Chainlink documentation has warned about exactly this since at least 2021, and the parody domain shouldiusespotpriceasmyoracle.com has been making the rounds for nearly as long. The lesson is that the cohort of teams launching new tokens has churned over twice since 2020, the surface area has migrated to fee-on-transfer / burn-mechanic / rebase token families that introduce new variants of the same bug, and the per-incident loss numbers are small enough (six and seven figures, not nine) that they don't drive industry-wide attention. The pattern just keeps grinding.

The audit checklist

What an audit pass that actually catches this looks like. Eight items, in roughly the order they should run.

1. Trace every price read

Find every call site in your contracts that reads a price — getReserves(), balanceOf(pair), totalSupply() / totalShares, IUniswapV2Pair.token0/token1, anything that computes a ratio. For each, answer in writing: where does this number come from, what is the manipulation cost, and what is the response time. If the answer is "spot DEX reserves" and the response time is "the next call in the same transaction," that is the bug. (Chainlink's Selecting Quality Data Feeds page is the canonical reference for the alternative; the Uniswap V2 oracle docs explicitly warn against using getReserves() for pricing.)

2. Read every transfer hook in the listed token

For every token your protocol prices, deposits, or accepts as collateral, read the actual transfer / _transfer / _update / _beforeTokenTransfer / _afterTokenTransfer implementation. You are looking for any code path that mutates balanceOf(LP pair address) outside the AMM's swap() flow. That includes:

  • burn or _burn on the pair's balance
  • fee-on-transfer that sends a portion to a dev wallet, charity, or burn address
  • reflection tokens that redistribute to all holders
  • "anti-MEV" or "anti-snipe" logic that imposes asymmetric costs on the pair
  • distributeFees, scheduledDestruction, sellBurn, or any named function with a global counter that triggers under specific conditions

The Trail of Bits token integration checklist and the d-xo weird-erc20 catalogue are the standard references for what to look for. Every one of the 2026 BSC incidents shipped at least one of these.

3. Test the sync() interaction

For each transfer hook identified in step 2, ask: does this hook eventually trigger IUniswapV2Pair.sync(), either directly or via a subsequent swap()? If yes, the pool will re-read its own balance and adopt the post-hook state as the new reserves. That is the active ingredient in every one of the 2026 incidents. Build a Foundry / Hardhat test that runs the hook, calls sync(), then quotes the spot price. If the price moves materially, the bug is live.

4. Audit your TWAP windows

If you use a TWAP, write down the window length and the cost-to-move calculation. A one-block TWAP is not a TWAP. A 30-second TWAP on a thin pool is not meaningfully better than spot. The Inverse Finance attack worked against a Keep3r TWAP because the sampling window was short relative to the cost of holding the pool skewed for one sample. The Uniswap V2 building-an-oracle guide and the V3 oracle library docs both make the cost-of-manipulation tradeoff explicit. Your audit report should state the manipulation cost in dollar terms at current liquidity.

5. Check for donation / inflation attacks on share-based pools

Any vault, lending market, or LP that computes shares = assets * totalShares / totalAssets is vulnerable to a donation attack: deposit 1 wei, mint 1 share, then transfer() a large amount directly to the vault (bypassing mint), inflating shares-to-assets so that subsequent honest deposits round down to 0 shares. This is the Cream October 2021 mechanism, and it is the same class of bug that OpenZeppelin's ERC-4626 implementation explicitly mitigates using virtual shares + decimal offset. If you are on a homebrew vault, you need the equivalent mitigation. Hundred Finance died because they didn't have it.

6. Rebasing and fee-on-transfer compatibility

Any contract that caches a balance (Uniswap V2 reserves, lending market totalSupply snapshots, Balancer pool weights) is operating with outdated data when a token rebases or burns silently. Either the token must be excluded from the protocol's accepted set, or every cached balance must be refreshed before use. There is no accepted EIP for fee-on-transfer tokens — integrators have to detect this manually by measuring balanceOf(this) before and after each transfer. Treat the absence of this pattern in the code as an audit finding.

7. Read-only reentrancy on AMM pairs

EraLend went down because SyncSwap's LP burn flow had a callback that fired before _updateReserves. Any contract that reads a pool's state during a callback — Uniswap V3 swap callback, V4 hooks, Balancer's onSwap, Curve's exchange with a re-entrant token — needs to be audited for stale-state reads. The OpenZeppelin reentrancy guard catches mutating reentrancy; it does not catch read-only reentrancy.

8. Single-pool oracle dependency

For each price feed your protocol consumes, ask: how many independent venues contribute? Polter Finance had one. Mango Markets had three thin ones. The 2026 BSC pattern had one each. If the answer is one or two thin venues, the audit finding writes itself. The mitigation is either Chainlink (which aggregates across many venues with explicit volume-weighted end-of-block sampling) or a multi-venue TWAP with sufficient depth that the cost to move all venues simultaneously exceeds the loss bound on the consuming protocol.

What good looks like

There is a defensible 2026 design pattern. None of it is new. All of it is the difference between the protocols on this list and the ones that aren't.

For pricing-sensitive logic: use Chainlink Price Feeds or a Pyth equivalent. Do not derive prices from a single pool's reserves. If you have to, use a Uniswap V3 TWAP with observe() over a window where the cost to hold the pool skewed exceeds the loss the consuming protocol can incur. State the manipulation-cost bound in the audit report.

For token listing: if you list a fee-on-transfer or rebasing or burn-on-transfer token, the contract that holds it must use the balanceOf(this) before-and-after pattern, never transfer(amount) followed by an assumption that amount is what arrived. Uniswap V2 reverts on K-violation under this pattern, which is why so many tokens require swapExactTokensForTokensSupportingFeeOnTransferTokens — that should be a flag, not a workaround.

For share-based vaults: use OpenZeppelin's ERC-4626 implementation with virtual shares + decimal offset, or implement the equivalent. Hundred Finance and Cream both shipped without it. Both died.

For LP-pair integration: if your token's transfer logic touches the pair's balance at all — even indirectly through a fee-collector contract that holds a portion of LP tokens — the audit must include a test that triggers the hook and confirms the pair's spot price has not moved relative to a TWAP reference. Every 2026 incident on the BSC table above would have failed that test.

FAQ

Why does reserve manipulation keep happening?

Two structural reasons. First, the auditing industry treats it as a solved problem, so the long tail of new token launches — particularly fee-on-transfer and burn-mechanic tokens on BSC — does not get the deep AMM-integration review the top-100 protocols would get. Second, the per-incident loss numbers (six and seven figures) are small enough not to drive industry-wide attention; the cumulative figure across many small incidents is what's growing.

Isn't this just an oracle problem?

It's both. Reserve manipulation is the attack primitive; oracle manipulation is the failure mode for the consuming protocol. A pool with thin liquidity and a flawed transfer hook is the vulnerability source. A lending market, vault, or pricing-sensitive contract that reads getReserves() (directly or transitively through a single-pool TWAP) is the victim — Aave's $27M wstETH liquidation in March 2026 is the recent reminder that even top-tier protocols can ship oracle-side configuration drift. Either side fixed in isolation closes the bug; neither side fixed has been the dominant 2026 reality.

Why is this concentrated on BSC?

BSC pairs PancakeSwap V2 (a Uniswap V2 fork) with a long tail of tokens shipping non-standard transfer logic — burn mechanics, deflationary, reflection. PancakeSwap V2 is a fork of Uniswap V2; both expose sync() as a public function that re-reads balanceOf(pair). The combination of permissive token deployment + heavy use of V2-style AMMs + low-liquidity pools is the ecosystem-level vulnerability. Other chains with the same mix (some Polygon V2 forks, some Arbitrum projects) are exposed too — BSC just has more of it.

Is sync() the real bug?

sync() is doing what it was designed to do: realign the pool's internal accounting with its actual token balance. That is necessary for the AMM to function correctly when a transfer happens out-of-band. The bug is on the token side — shipping a transfer hook that mutates the pair's balance without going through swap() — combined with the consumer side reading the post-sync price as ground truth. sync() is just the mechanism by which the two sides connect.

What's the smallest change a token issuer can make to prevent this?

Do not put any logic in _transfer (or hooks called from it) that mutates the LP pair's balance. If you need a burn or fee mechanism, route it through user transfers only — exempt the pair address from the burn/fee logic, or maintain the burn in a separate accounting structure that the pair never sees. The d-xo weird-erc20 catalogue documents this pattern under the "fee on transfer" and "balance modifications outside of transfers" sections.

How do I audit a token I didn't write?

Three steps. First, read the _transfer / _update implementation top to bottom. Second, trace every conditional branch — many burn-mechanic tokens only fire the burn under specific conditions (sells over a threshold, transfers from specific addresses, after a global counter increments). Third, in a fork, simulate every branch and check whether it changes balanceOf(pair). If you find any path that does and sync() is callable afterward (it always is — it's public on every Uniswap V2 pair), the integration is unsafe.

Where SigIntZero fits

The audit boundary for a Web3 protocol does not end at the contract's pragma line. The transfer logic of every token you list, the LP pair you depend on for pricing, the TWAP window length, the oracle dependency graph — all of it is in scope. The 2026 BSC pattern is a long string of protocols that treated this as out of scope and paid for it.

  • Sentinel — AI-assisted smart contract auditing that documents the deployment, upgrade, and operator assumptions a contract relies on. For DeFi protocols, that explicitly includes the token integration boundary: which tokens you accept, what their transfer logic does, and how your pricing surfaces consume reserve data.
  • Tripwire — runtime monitoring of on-chain symptoms. Cannot prevent a reserve-manipulation attack mid-block. Can detect unusual reserve movements, large flash-loan-funded swaps against thin pools, and abnormal price quotes from consuming protocols. The detection window for the 2026 BSC incidents was generally under five minutes from attack to forensic post; runtime monitoring closes more of that gap.
  • Services — protocol security review beyond the contract scope: token integration, oracle architecture, deployment assumptions, incident response.

If your protocol prices an asset using a pool you didn't write, the integration boundary is part of your audit scope.

The lesson outlasts the pool

The 2026 BSC pool drains will get cleaned up. Liquidity will move elsewhere, the specific tokens will lose interest, the postmortems will get filed in the SlowMist archive next to Polter and EraLend. The underlying lesson will not change.

Reserve manipulation is not a 2020 attack class. It is a present-tense audit surface that has been quietly grinding through the industry for six years, mostly under the per-incident threshold that drives industry attention. The cohort of teams shipping new tokens has churned faster than the audit checklists have propagated. The mechanism is older than half the protocols using it. The fix is older than the mechanism.

Audit the integration boundary. Read the transfer hook. Trace every price read. Use Chainlink for anything pricing-sensitive, or a TWAP with a window that survives the loss bound on your protocol. None of this is hard. All of it is the difference between the protocols on the table above and the ones that aren't.


Sources and further reading:

Alex Rybalko
Alex Rybalko

Co-Founder & CEO

Co-Founder of SigIntZero. Security architecture and threat modeling for protocols and distributed systems.