TENDRIL Exploit: Sell-Hook Burn-Sync Drained ~$555K | PandaSecurity
Web3 Security

TENDRIL Exploit: Sell-Hook Burn-Sync Drained ~$555K

PandaSecurity Team •
#Web3 Security#DeFi Exploit#BNB Chain#Smart Contract Audit
Diagram of the TENDRIL exploit: sell tax burns pair TENDRIL, sync collapses reserves, delayed transfer drains USDT

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.

TL;DR

What the Chain Shows

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:

  1. Borrow. Vault 0x238a...e6c4 sends 30,641,402.469693 USDT to the exploit contract.
  2. Manipulate and tax. The contract buys into the thin pair, triggers TENDRIL’s 30% sell tax, and routes through PoolControl at 0xd00be871eb7a5cb8a69aea41e4c95affc92e54fe. During that path the pair’s TENDRIL balance is burned (large transfer to 0xdead) and sync() is called.
  3. Drain. After the burn-sync leaves TENDRIL reserves at 6,678 wei against roughly 4.42M USDT, the seller’s remaining TENDRIL arrives and a swap pulls 4,421,023.357469 USDT out of the pair to the exploit contract. The next Sync shows USDT reserves at 2 wei.
  4. Repay and keep. Exactly 30,641,402.469693 USDT goes back to the vault. 308,959.367543 USDT goes to 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.

How the Sell Hook Broke the Pair

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):

  1. An ordinary sell of amount A calls PoolControl.sellToken(A).
  2. PoolControl then calls sender-restricted TENDRIL.controlburn(2A), which debits the V2 pair itself.
  3. 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.
}

Why PancakeSwap Did Not Protect LPs

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.

Loss Figures and Open Questions

Public numbers do not all measure the same thing:

FramingFigureSource
Pool / incident loss~$550K–$556KCertiK, ExVul, ScaleBit, BlockSec Phalcon first alert
Early monitoring estimate~$309KTenArmor Alert
Profit transfer in exploit tx308,959.37 USDTon-chain Transfer we decoded; matches Phalcon
ExVul campaign-profit upper bound~181,910 USDT after observed buys/gasExVul (excludes unpriced LP basis)
Burn-sync reserve after sync6,678 wei TENDRILExVul; 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.

Lessons for Builders and Auditors

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.

Key Addresses and Figures

ItemValue
ChainBNB Smart Chain
Exploit transaction0xfdff4fba4327821c204af07944076ecaaee163a994398fdf4063d08712e40059
Block / time131,653,427 / 4 Nov 2026, 16:02:45 UTC
Operator (tx.from)0xb81e4aafa347b27112b7f6f5de6cf7cd516021ff
Exploit contract0xc8cb6a2aa825a576e1470479046056fe414ee0f8
Profit recipient (in-tx)0xb87cf5ab84d0ab9545358cd30229627305331373
TENDRIL token0xb1afdfd850da168f2cc9f742e11cdda5273973d2
TENDRIL/USDT pair0x5de082471f2a9f01a61e467a32837dee830d99e0
PoolControl0xd00be871eb7a5cb8a69aea41e4c95affc92e54fe
Flash loan30,641,402.47 USDT (PancakeSwap Infinity Vault)
Final USDT pulled from pair4,421,023.36 USDT
In-tx profit transfer308,959.37 USDT
Reported pool loss~$550K–$556K (multiple alert desks)

Sources

Closing Thoughts

TENDRIL 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.

Related articles

← Back to Blog
Need an audit? Contact us