BNB Chain
Reserve manipulation

TMM's $1.7M Loss and How Olympix Would Have Prevented It

Loss
Loss
Exploit date
April 4, 2026

Date: April 4, 2026

Chain: BNB Chain

Loss: 1,665,255 USDT, about $1.7M, drained from the TMM/USDT PancakeSwap pool

Class: token transfer logic that burns from the AMM pair's own balance and forces a reserve sync

One block before the attack, the TMM/USDT pool on PancakeSwap held 6.95 billion TMM and 1,677,164 USDT. One block later, at 22:46:45 UTC on April 4, 2026, it held 0.85 TMM and less than a thousandth of a cent in USDT. A single transaction in between had sold the pool one TMM and taken back 272 million USDT for it, most of which was the attacker's own flash-borrowed money coming home, plus every dollar the pool's liquidity providers had left in it.

The pool didn't break on its own. TMM's token contract had a rule that, on every sell, queued a burn of twice the amount sold into the pool after fees, and on the next sell it took that burn directly out of the pool's TMM balance and told the pool to accept the new number as its reserve. Inside one transaction, that rule let the attacker shrink the pool's TMM side to a single wei, at which point a constant-product pool will hand over all of its USDT for almost nothing.

A Token With Opinions About Its Own Pool

TMM is an upgradeable BEP-20 token on BNB Chain. Its proxy at 0x1d6f...6666 was deployed on March 11, 2026, and the implementation it pointed to on April 4, 0x39ca...7fee, was deployed on March 20. The source for that implementation is verified on Sourcify. Its initializer mints 10 billion TMM, creates a TMM/USDT pair on PancakeSwap's V2 factory, and starts with a 3% buy fee and a 15% sell fee, with a vault contract and two fee receivers attached.

Like many tokens built around a single trading pair, TMM doesn't treat transfers as neutral. Its _transfer function checks who's on each side. Transfers out of the pair are buys or liquidity withdrawals, transfers into the pair are sells or liquidity deposits, and everything else is a wallet-to-wallet move with its own burn rule. A flag called burnTokenWhenSell turns on a deflationary mechanism: every sell adds to a counter called waitBurnTokenAmount, and the next sell burns that many tokens from the pair.

The pair at 0xc36c...a041 is a standard PancakeSwap V2 pool. Like Uniswap V2, it prices trades from two stored reserves and enforces that their product, x times y, doesn't fall during a swap. It also exposes sync(), which anyone can call to reset the stored reserves to whatever the pool's token balances actually are. The pool trusts its balances because it assumes a token's balance at the pool's address only moves when the pool itself moves it.

The Burn That Comes Out of the Pool

From the verified TMM implementation source on Sourcify (TMM.sol), this is the branch of _transfer that runs when tokens are sent to the pair:

        } else if (to == pair) {
            uint256 liquidity = _isAddLiquidity(amount);

            if (!freeTrade && liquidity == 0 && waitBurnTokenAmount > 0) {
                super._transfer(pair, address(0xdead), waitBurnTokenAmount);
                try IPair(pair).sync() {} catch {}
                waitBurnTokenAmount = 0;
            }

            uint256 feeAmount = (amount * sellFeeRate) / 10000;
            if (liquidity > 0) {
                feeAmount = 0;
            }

            _handleFeeAmount(from, feeAmount, true, liquidity > 0);
            super._transfer(from, to, amount - feeAmount);

            if (!freeTrade && liquidity == 0 && burnTokenWhenSell) {
                waitBurnTokenAmount += ((amount - feeAmount) * 2);
            }

The first line decides this is a transfer into the pool. _isAddLiquidity compares the pool's USDT balance against its stored reserve to tell a liquidity deposit from a sell, and for a sell it returns zero.

The next block is the problem. If the token is still in its restricted trading phase (freeTrade is false), this is a sell, and a burn is pending, the token calls its parent ERC-20 _transfer to move waitBurnTokenAmount tokens from the pair to the dead address. The pair never approved that and never sees it coming, because the token contract writes the balances directly. The token then calls sync() on the pair, so the pool's stored TMM reserve drops to match its smaller balance, and resets the counter.

After that comes the ordinary sell: a 15% fee is skimmed through _handleFeeAmount and the rest of the seller's TMM goes to the pool. The last block is what loads the next burn. When burnTokenWhenSell is on, the counter grows by twice what the seller just put into the pool, and nothing ties that number to how much TMM the pool will actually hold when the next sell arrives.

Each part was written with a purpose. Burning from the pool after a sell pushes the TMM reserve down and the price up, which is the whole point of a deflationary sell tax. Put together, though, it means the token contract can delete TMM from the pool's reserve in an amount set by earlier trades, and the pool's x times y invariant only holds for swaps, never for a reserve someone else rewrote with sync().

The teaching point is that an AMM pair's reserves belong to the pair. A token that moves the pair's balance and then forces a sync is changing the price without a trade, and whoever controls the size or timing of that change controls the price. It's the equivalent of a bank teller who, on every withdrawal, also shreds some of the cash left in the vault, and then updates the ledger to match.

Building a Burn, Then Spending It

The exploit transaction 0xdee4...7c2d landed in block 90651458 at 22:46:45 UTC on April 4, 2026. It was sent by 0xdd53...766f to an attack contract at 0x1c5e...7203, used 8.88 million gas and emitted 300 event logs. According to Halborn's analysis, the USDT it worked with came from flash loans across ListaDAO Moolah, Venus, Aave V3, the PancakeSwap Vault and Uniswap's V4 PoolManager, spread out to stay under each lender's caps. The transaction's logs show hundreds of millions of USDT arriving at the attack contract from those sources before the pool was touched.

The attack contract started by redeeming 1,899,257.79 LP tokens it held for the TMM/USDT pair, which returned 26,546.14 USDT and 109,962,302 TMM. TMM's liquidity-withdrawal rule sent that TMM straight on to the dead address.

Next it sold 20,798,151.86 TMM into the pool for 4,989.65 USDT. As a sell during the restricted phase, that trade added twice the post-fee amount to TMM's pending burn counter. The pool's state after the sale still looked normal, with about 6.86 billion TMM against 1.645 million USDT.

Then came the large trade. The attack contract pushed 270,449,564.49 USDT into the pool and bought 6,820,238,188.81 TMM, with the output delivered to the dead address. TMM's _transfer lets any transfer to 0xdead skip its rules entirely, so this buy paid no fee and loaded nothing. After it, the pool's Sync event shows 41,596,303.73 TMM against 272,094,899.69 USDT, the attacker's borrowed USDT plus the liquidity providers' 1.65 million.

The final sell was one TMM. Before the token moved that one TMM into the pool, the pending-burn branch fired: it took 41,596,303.73 TMM from the pair to the dead address, which was all of the pool's TMM except a single wei, and called sync(). The pool's next Sync event reads 1 wei of TMM against 272,094,899.69 USDT. The 15% fee came off, 0.85 TMM went into the pool, and the pool, pricing a token it held almost none of, paid out 272,094,899.69 USDT for it.

The attack contract then repaid its lenders in the same transaction and sent 1,665,255.44 USDT to 0xe4ed...cd0c. The pool was left with 0.85 TMM and 320,913,932 base units of USDT, well under a billionth of a dollar.

What 850 Million TMM Really Was

Halborn's write-up and the Wu Blockchain report relayed by Phemex both describe the attacker swapping "850 million TMM" for about 272 million USDT, figures that sit oddly next to a $1.665M loss. The on-chain logs settle both points. The final swap's input was 0.85 TMM, 850,000,000,000,000,000 in the token's 18-decimal base units, which is where the 850 million reading comes from. The 272 million USDT out was almost entirely the attacker's own flash-loaned capital, returned to lenders in the same transaction, and only the 1,665,255.44 USDT that stayed behind was profit taken from the pool.

That profit is close to the full USDT side of the pool before the attack. One block earlier, at block 90651457, getReserves on the pair returned 6,947,333,139.46 TMM and 1,677,164.87 USDT.

After the Pool Emptied

Halborn reports that the profit was later distributed to various addresses. Neither Halborn's analysis nor the Wu Blockchain report cited any statement from TMM's developers, and Halborn's April 2026 roundup lists the incident at about $1.6M among that month's fourteen DeFi hacks.

The pool's state tells the rest. With 0.85 TMM and essentially no USDT left in its only trading pair, TMM had no market to sell into, and the liquidity providers who'd supplied the 1.68 million USDT were holding LP tokens backed by almost nothing.

Why the Rule Looked Safe

The pending-burn mechanism is easy to read as a tokenomics feature rather than a security surface. It runs only on sells, during a restricted phase, and on paper it only ever makes TMM scarcer, which holders want. Each line passes a local check: the burn amount comes from real past sells, the sync keeps the pair's reserves consistent with its balances, and the fee logic matches the documented rates.

What no single line shows is the interaction with the pair's pricing. The counter grows with sells and is spent on the next sell, but the pool's TMM balance can change by billions of tokens between those two moments through buys, including buys that go to the dead address and skip TMM's rules. By the time the burn executes, its size has no relationship to what the pool can afford to lose. The implementation also changed during TMM's short life: the proxy was deployed on March 11 and the implementation live on April 4 was deployed nine days later, so whatever review the token received had to track a moving target. Audits cover the scope they're given in the time they have, and tokenomics code that reaches into a third-party AMM is exactly where two separately sensible designs meet.

How Olympix Follows a Queued Burn Into the Pool

Olympix reads a contract the way an attacker does, as states, transitions and entry points, and it takes in what surrounds the code, here a token built to trade against one PancakeSwap pair. For TMM, the states that matter are the pair's TMM balance, its stored reserves and the pending burn counter, and every transfer path is an entry point that can change them. Olympix's static analysis surfaces the signal that ranks this path first: a token function that moves tokens out of the AMM pair's address without going through the pair, then calls sync() on the pair, with the amount taken from a storage counter that other users' trades increase.

The engine narrows from all transfer paths to the feasible ones, which here means any unprivileged account during the restricted phase with burnTokenWhenSell on, and then to the ones that touch the invariant. That invariant fits in one sentence: the pool's TMM reserve should only fall when someone takes TMM out of the pool through a swap or liquidity withdrawal that pays for it, so the product of the reserves never drops outside of a withdrawal.

BugPoCer turns that rule into a concrete break. It runs Counterexample-Guided Invariant Refutation, working outward from the token and its pair as one system, inferring the invariant and executing sequences of buys and sells until one violates it. The counterexample for TMM is short and specific: a sell that loads the pending burn, a buy that reduces the pool's TMM balance toward the queued amount, and a final small sell whose burn-and-sync leaves the pool's TMM reserve near zero and its USDT reserve intact, followed by a swap that empties the USDT. BugPoCer reports that sequence with the state changes at each step, the minimal conditions (restricted phase active, burn-on-sell enabled, enough USDT to move the pool), and a generated Forge test that replays it against a forked pool. That's the April 4 transaction, produced in development.

Because the engine runs automatically on every merge and before release, the finding would have surfaced at the point the pending-burn branch was written or at the upgrade that put this implementation live on March 20, with a failing test attached rather than a note about tokenomics risk.

Reserves Are the Pool's Business

TMM's designers wanted a token that got scarcer every time someone sold it, and they got one that let a single transaction delete the pool's entire TMM supply and buy its USDT for 0.85 TMM. Any token that writes to a pair's balance and calls sync() is setting the pair's price directly, so the amount and timing of that write need the same scrutiny as a price oracle. To see how Olympix tests every path where your token reaches into its own liquidity, book a technical walkthrough.

On-Chain References

Protocol: TMM token and its TMM/USDT PancakeSwap V2 pool

Chain: BNB Chain

Date: April 4, 2026, 22:46:45 UTC

Loss: 1,665,255.44 USDT to the attacker's profit address (pool held 1,677,164.87 USDT one block earlier)

Attacker EOA: 0xdd53...766f

Attack contract: 0x1c5e...7203

Profit recipient: 0xe4ed...cd0c

TMM token (proxy): 0x1d6f...6666

TMM implementation: 0x39ca...7fee

TMM/USDT pair: 0xc36c...a041

Exploit tx: 0xdee4...7c2d

Block: 90651458

Implementation deployment tx: 0x513b...567b

Sources

Halborn: Explained, The TMM Hack (April 2026)

Phemex News: BSC Network Hit by TMM/USDT Reserve Manipulation Attack, $1.665 Million Lost

Halborn: Month in Review, Top DeFi Hacks of April 2026

Sourcify: Verified TMM Implementation Source (BNB Chain)

See what Olympix finds on your own code

Send us a repo that's already been audited, and we'll run Olympix at the same commit and show you what comes back.