DEV Community

PandaSecurity
PandaSecurity

Posted on Originally published at panda-security.xyz

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

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

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

  • Target: TENDRIL (0xb1afdfd850da168f2cc9f742e11cdda5273973d2) and its PancakeSwap V2 USDT/TENDRIL pair (0x5de082471f2a9f01a61e467a32837dee830d99e0) on BNB Chain.
  • Exploit transaction: 0xfdff4fba4327821c204af07944076ecaaee163a994398fdf4063d08712e40059, block 131,653,427, 4 Nov 2026, 16:02:45 UTC.
  • Operator / caller: 0xb81e4aafa347b27112b7f6f5de6cf7cd516021ff calling exploit contract 0xc8cb6a2aa825a576e1470479046056fe414ee0f8.
  • Profit address (in-tx): 0xb87cf5ab84d0ab9545358cd30229627305331373 received 308,959.37 USDT.
  • Flash loan: 30,641,402.47 USDT from PancakeSwap Infinity Vault 0x238a358808379702088667322f80ac48bad5e6c4, repaid in full in the same transaction.
  • Reported root cause: sell-tax logic → 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.

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;
}
Enter fullscreen mode Exit fullscreen mode

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.
}
Enter fullscreen mode Exit fullscreen mode

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:

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.

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

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)

Sources

  • CertiK Alert: sell hook debited pair and synced before remaining input; ~$555K net loss
  • TenArmor Alert: same attack tx; early ~$309K estimate
  • ExVul Alert: PoolControl.sellToken / controlburn / early sync; 6,678 wei reserve; addresses
  • ScaleBit Alert: ~550k USDT drained; pair, token, exploit, attacker addresses
  • BlockSec Phalcon: ~$556K losses; 30.64M flash loan; ~308.96K profit; sell-tax burn-sync root cause
  • BlockSec Phalcon update: later note on prior V3 accumulation vs burn-sync incremental profit
  • 0xCERT-2026-0313: advisory summarizing CertiK and TenArmor with the exploit hash
  • CryptoBriefing: narrative summary citing ExVul / ScaleBit disclosures
  • BNB Smart Chain receipt, Transfer and Sync events read directly from a public RPC node

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.

Top comments (0)