At 16:02:45 UTC on 4 November 2026, one transaction on BNB Smart Chain emptied nearly all of the USDT sitting in the TENDRIL/USDT PancakeSwap V2 pair. Public alerts from CertiK, BlockSec Phalcon, ExVul, ScaleBit and TenArmor put the pool’s loss around $550K–$556K. The same transaction repaid a ~30.64M USDT flash loan and sent 308,959.37 USDT to a profit address. There was no compromised admin key and no bug in PancakeSwap itself. The token’s own sell-tax path burned TENDRIL out of the pair, called sync() while the seller’s remaining tokens were still in flight, and left the pool with a near-zero TENDRIL reserve that a follow-up swap could cash out as USDT.
This is the same family of failure as other BNB Chain tax tokens that rewrite pair balances mid-transfer. The NEM skim-and-burn exploit burned pair inventory through a faked sell counter. TENDRIL’s version was simpler: every taxed sell asked a helper contract to burn from the pair and sync before the outer transfer finished.
0xb1afdfd850da168f2cc9f742e11cdda5273973d2) and its PancakeSwap V2 USDT/TENDRIL pair (0x5de082471f2a9f01a61e467a32837dee830d99e0) on BNB Chain.0xfdff4fba4327821c204af07944076ecaaee163a994398fdf4063d08712e40059, block 131,653,427, 4 Nov 2026, 16:02:45 UTC.0xb81e4aafa347b27112b7f6f5de6cf7cd516021ff calling exploit contract 0xc8cb6a2aa825a576e1470479046056fe414ee0f8.0xb87cf5ab84d0ab9545358cd30229627305331373 received 308,959.37 USDT.0x238a358808379702088667322f80ac48bad5e6c4, repaid in full in the same transaction.PoolControl.sellToken → controlburn debiting the pair → pair.sync() before the seller’s remaining (~70%) transfer arrives, collapsing the recorded TENDRIL reserve to 6,678 wei.We pulled the receipt and decoded ERC-20 Transfer and pair Sync events from a public BSC node. Token0 of the pair is USDT (0x55d39832...197955); token1 is TENDRIL.
The flash-loan bookends are exact:
0x238a...e6c4 sends 30,641,402.469693 USDT to the exploit contract.PoolControl at 0xd00be871eb7a5cb8a69aea41e4c95affc92e54fe. During that path the pair’s TENDRIL balance is burned (large transfer to 0xdead) and sync() is called.Sync shows USDT reserves at 2 wei.0xb87c...1373.That profit figure matches BlockSec Phalcon’s published take of about 308.96K USDT. The ~$550K–$556K figures from CertiK, ExVul, ScaleBit and Phalcon’s first alert describe the pool-side loss framing, not the single profit transfer above. Different desks also count prior setup buys differently, which is why TenArmor’s early alert sat near $309K.
Fee-on-transfer tokens on BNB Chain often take a cut on sells and push that cut through a helper. According to ExVul’s public breakdown (function names as reported there; we did not verify TENDRIL source ourselves):
A calls PoolControl.sellToken(A).PoolControl then calls sender-restricted TENDRIL.controlburn(2A), which debits the V2 pair itself.PoolControl calls pair.sync() before the seller’s outer remaining transfer (described as the remaining ~70%) lands in the pair.msg.sender checks only authenticated the immediate Token and PoolControl contracts. An unprivileged seller still controlled the full Token → PoolControl → Token → Pair path. Once sync() had written a near-zero TENDRIL reserve, Uniswap V2-style math treated the delayed inbound TENDRIL as enough input to justify withdrawing essentially all of the USDT side.
A simplified sketch of that ordering bug, written for illustration only and not TENDRIL’s actual source:
// Simplified illustration of the pattern. Not TENDRIL source.
function sellToken(uint256 taxedAmount) external onlyToken {
// Burns from the pair balance, not from the seller's wallet.
tendril.controlburn(taxedAmount * 2); // debits pair
IUniswapV2Pair(pair).sync(); // locks in the burned reserve
// DANGER: seller's remaining transfer has not arrived yet.
// A later swapExact... call can drain USDT against the tiny reserve.
}
function _transfer(address from, address to, uint256 amount) internal {
uint256 tax = amount * 30 / 100;
uint256 send = amount - tax;
// Tax path runs (and may sync) before `send` hits the pair.
if (isSell(from, to)) {
_poolControl.sellToken(tax);
}
_balances[to] += send;
}
A safer ordering finishes every balance update that belongs to the swap before any burn/sync, or better, never lets tax logic burn the pair’s own inventory at all:
// Simplified illustration of a fail-closed shape.
function sellToken(uint256 taxedAmount) external onlyToken {
// Burn from a dedicated tax treasury, never from the pair address.
tendril.burnFrom(taxTreasury, taxedAmount);
// If pair accounting must move, do it only after the outer
// transfer has updated balances, and never mid-swap.
}
PancakeSwap V2 prices from the ratio of the two reserves it last synced. It has no opinion about whether a token burned its own side of the pool. When a custom token can change those balances and force a sync in the middle of a sell, the AMM’s invariant becomes a weapon against the remaining liquidity. Auditing the router does not help; the dangerous code lives in the token and its tax helper.
The flash loan only rented size. With ~30.64M USDT of temporary inventory the attacker could push the thin TENDRIL pool around, trigger the taxed sell path on purpose, and still repay inside one transaction. Our flash loan attack overview covers that funding pattern; the novel piece here is the mid-sell burn-sync, not the loan itself.
Public numbers do not all measure the same thing:
| Framing | Figure | Source |
|---|---|---|
| Pool / incident loss | ~$550K–$556K | CertiK, ExVul, ScaleBit, BlockSec Phalcon first alert |
| Early monitoring estimate | ~$309K | TenArmor Alert |
| Profit transfer in exploit tx | 308,959.37 USDT | on-chain Transfer we decoded; matches Phalcon |
| ExVul campaign-profit upper bound | ~181,910 USDT after observed buys/gas | ExVul (excludes unpriced LP basis) |
| Burn-sync reserve after sync | 6,678 wei TENDRIL | ExVul; matches pair Sync in the receipt |
BlockSec Phalcon later posted an update arguing that most overall campaign profit came from roughly 33.06B TENDRIL acquired cheaply earlier through an attacker-created V3 pool, and that the burn-sync step may have added only on the order of $10K+ of incremental profit on top of that inventory. That does not erase the V2 drain mechanism above; it does mean the attacker’s full P&L story may include setup trades outside the single exploit hash. We have not independently reconstructed that earlier V3 activity for this post.
We also did not find a project post-mortem or a patched token implementation at the time of writing. Treat the function names (sellToken, controlburn, _poolControl) as third-party labels from ExVul and BlockSec unless and until verified source is published.
Never burn the pair’s own balance inside a sell hook. If tokenomics need a burn, burn from a treasury or from the seller’s taxed amount after accounting is final. Debiting the LP pair turns every sell into a potential reserve rewrite.
Do not call sync() until inbound transfers that belong to the swap have landed. Ordering bugs are easy to miss in review because each step looks locally correct. Write a fork test that sells while a hook burns and syncs early, and assert that USDT (or the quote asset) cannot leave the pair beyond the fair amount.
Authenticate the whole economic path, not just msg.sender. TENDRIL’s checks stopped at Token and PoolControl. The unprivileged seller still drove Token → PoolControl → burn → sync. If a privileged burn exists, the only safe callers are tightly scoped and cannot be reached from arbitrary sells.
Assume fee-on-transfer + flash liquidity. Any tax token paired against a deep quote asset is a candidate for the same shape. Our smart contract security checklist is a practical place to park review items for custom _transfer hooks.
Bound damage even when pricing is wrong. Per-tx withdraw caps and circuit breakers on pair USDT outflows would not fix a bad burn, but they turn a full drain into a partial one and give monitors time to react.
| Item | Value |
|---|---|
| Chain | BNB Smart Chain |
| Exploit transaction | 0xfdff4fba4327821c204af07944076ecaaee163a994398fdf4063d08712e40059 |
| Block / time | 131,653,427 / 4 Nov 2026, 16:02:45 UTC |
| Operator (tx.from) | 0xb81e4aafa347b27112b7f6f5de6cf7cd516021ff |
| Exploit contract | 0xc8cb6a2aa825a576e1470479046056fe414ee0f8 |
| Profit recipient (in-tx) | 0xb87cf5ab84d0ab9545358cd30229627305331373 |
| TENDRIL token | 0xb1afdfd850da168f2cc9f742e11cdda5273973d2 |
| TENDRIL/USDT pair | 0x5de082471f2a9f01a61e467a32837dee830d99e0 |
| PoolControl | 0xd00be871eb7a5cb8a69aea41e4c95affc92e54fe |
| Flash loan | 30,641,402.47 USDT (PancakeSwap Infinity Vault) |
| Final USDT pulled from pair | 4,421,023.36 USDT |
| In-tx profit transfer | 308,959.37 USDT |
| Reported pool loss | ~$550K–$556K (multiple alert desks) |
PoolControl.sellToken / controlburn / early sync; 6,678 wei reserve; addressesTransfer and Sync events read directly from a public RPC nodeTENDRIL did not lose its USDT liquidity because PancakeSwap mispriced a swap. It lost it because a sell hook was allowed to rewrite the pair’s own reserves before the sell finished. Any token that taxes, burns or “recycles” against the LP pair needs the same review posture as a vault that pays from an oracle: fail closed on ordering, never let unprivileged callers drive privileged burns, and talk to our team or check our audit pricing before that hook goes live against real liquidity.
How a flash-loaned WBNB dump cut PancakeSwap's BNB price ~98x, inflated a CoreVault redemption ~33x and pulled 259 BNB from its InsurancePool on BSC.
How 53M MAMO sent straight to Moonwell's market on Base bypassed a 20M supply cap, inflated collateral 3.68x and unlocked $11.03M of loans.
How 99 withdrawal requests for 0 CORE emptied 91 b14g staking orders on Core, why zero meant 'everything', and what the hard-fork recovery changes.
How an attacker used freshly created option pools and Balancer flash loans to pull about $437K of collateral out of six Premia v3 vaults on Arbitrum.