BNB Chain
Reserve manipulation

TENDRIL's $556K Loss and How Olympix Would Have Prevented It

Loss
Loss
Exploit date
November 4, 2026

At 16:02:45 UTC on November 4, 2026, a single transaction on BNB Chain emptied the TENDRIL/USDT pool on PancakeSwap. The pair went into block 131,653,427 holding 555,670.54 USDT and came out holding 2 wei, a balance too small to buy anything. Nobody stole a key or forged a signature. The attacker sold TENDRIL, and the token's own sell logic did the rest.

TENDRIL charged a 30% tax on every sale into the pair and handed that tax to a helper contract called PoolControl. The trouble was the order of operations. The token called PoolControl in the middle of the sell, before the seller's tokens had reached the pair, and PoolControl had permission to burn TENDRIL straight out of the pair and resync its reserves. By the time the seller's tokens landed, the pool's books said it held almost no TENDRIL, and the swap math paid out almost all of its USDT. ExVul traced the drain within hours and put the pool's loss at about 550,000 USDT. The chain's own record is slightly higher, and that's the figure this post uses.

A Sell Tax With Access to the Pool

TENDRIL's contract is verified, and its transfer logic is short. Transfers out of the pair are only allowed to two of the protocol's own contracts, PoolControl and a marketing controller, so ordinary buys through the pair are closed. Transfers into the pair are sells, and every sell from an outside address carries a 30% tax.

} else if (to == _uniswapV2Pair) {
    if (from != _poolControl && from != _markControl) {
        tax = 30;
    }
}
...
if (tax > 0) {
    uint256 taxAmount = (amount * tax) / 100;
    _basicTransfer(from, _poolControl, taxAmount);
    IPancakeFactory(_poolControl).sellToken(amount);
    amount -= taxAmount;
}

_basicTransfer(from, to, amount);

That excerpt from TENDRIL's verified source shows the sequence. The tax goes to PoolControl, PoolControl's sellToken() runs, and only after it returns does the remaining 70% of the seller's tokens move into the pair. Whatever PoolControl does to the pool happens while the sale is half finished.

What PoolControl could do to the pool came from one more function in the token.

function controlburn(uint256 amount) external {
    require(msg.sender == _poolControl, "Mast PoolControl");
    _basicTransfer(
        _uniswapV2Pair,
        0x000000000000000000000000000000000000dEaD,
        amount
    );
}

controlburn() moves tokens directly out of the pair's balance and into the dead address. Its only guard checks that the caller is PoolControl. That check holds in a narrow sense, since only PoolControl can call it, but PoolControl runs on every sell, so any seller with enough TENDRIL could set the whole chain moving. The token authenticated the contract one hop away and never asked who started the transaction or what state the pair was in when the burn landed.

Reserves That Lied for One Swap

A PancakeSwap V2 pair prices a swap from its stored reserves, then checks the result against its actual balances. sync() overwrites the stored reserves with whatever balances the pair holds at that moment. If something removes tokens from the pair and calls sync(), the pair simply accepts the smaller number as the truth.

That's what makes the timing in TENDRIL's transfer so expensive. PoolControl's code isn't verified, so the exact rule it used to size the burn isn't public. The transaction log shows what it did. During the attacker's sell, 78,106,360,248 TENDRIL left the pair for the dead address, and the pair's next Sync event recorded a TENDRIL reserve of 6,678 wei against more than 4.4 million USDT. A moment later the seller's remaining tokens arrived, and the pair priced them against that hollowed-out reserve.

One Transaction, Start to Finish

The operator address 0xb81e...21ff called the attack contract, and the attack contract borrowed 30,641,402.47 USDT for the length of the transaction. It spent 14,546.35 USDT buying 5,990,704,684 TENDRIL through a second route that ran through two other PancakeSwap pools, topping up the TENDRIL it already held.

Next it pushed 4,097,517.64 USDT into the TENDRIL/USDT pair and took 488,339,316,313 TENDRIL out, with the tokens delivered to PoolControl, one of the two addresses the token lets the pair pay. That trade left the pair long on USDT and short on TENDRIL.

Then came the sell. The attack contract sent TENDRIL toward the pair, and the 30% tax, 11,715,954,037 TENDRIL, went to PoolControl first. PoolControl sold that tax into the pair for 696,494.47 USDT, sent 464,329.64 USDT back into the pair, and burned 78,106,360,248 TENDRIL out of it. The Sync that followed left 6,678 wei of TENDRIL on the books. PoolControl also paid out 232,164.82 USDT to three addresses during this step, including the token's marketing controller.

Only then did the seller's remaining 27,337,226,087 TENDRIL reach the pair. Against a reserve of 6,678 wei, the pair paid 4,421,023.36 USDT for them, everything it held except 2 wei. The attack contract repaid the 30.64 million USDT loan in the same transaction and sent 308,959.37 USDT to the beneficiary address 0xb87c...1373.

Counting the Loss

The pool's side of the ledger is the cleanest measure. It started the transaction with 555,670.54 USDT and ended with effectively nothing, so liquidity providers lost $555,671. CertiK put the net loss at about $555,000, which matches. TenArmor's early alert of roughly $309,000 lines up with the 308,959.37 USDT the attacker's beneficiary received. That's the attacker's take, and the pool's loss is larger. The difference is the 232,164.82 USDT that PoolControl paid out mid-sell and the 14,546.35 USDT the attacker spent on its purchase route.

As of November 5, 2026, no post-mortem, recovery plan or bounty offer from the TENDRIL team had been reported.

How Olympix Would Have Traced the Sell Into the Pair

Olympix reads a contract as states, transitions and entry points, and takes in the surrounding context to infer the invariants the system depends on. It maps the paths through the code and uses signals to narrow them, from every path to the feasible ones and then to the feasible paths that touch an invariant.

In TENDRIL, the state worth protecting is the pair's stored reserve, and the entry point is the most ordinary one a token has, a transfer into the pair. Static analysis would have surfaced the signal here: a public transfer path that calls out to an external contract before it finishes moving the caller's tokens, alongside a function that debits the pair's balance and is reachable from that same external contract. Either piece alone can be harmless. Together they mean a seller can change the pair's books while the sale is still in flight.

The invariant is short. A sell into the pair can't leave the pair's reserves in a state the seller's own swap then prices against, and nothing outside a completed swap should move the pair's balance. BugPoCer, Olympix's tool for Counterexample-Guided Invariant Refutation, works from the system outward. It starts from that rule and searches for a sequence of calls that breaks it, refining each time a candidate fails.

BugPoCer would have returned the November 4 sequence as a counterexample: skew the pair with a USDT trade routed to PoolControl, sell TENDRIL from an ordinary address, let the tax path burn from the pair and sync, then settle the rest of the sale against a reserve of a few thousand wei. The output would have been the exact state changes, the minimal conditions that trigger them and a generated Forge test that replays the drain. Had the team run the analysis at every merge and before release, that test would have failed in development, long before the pool held anyone's liquidity.

The pattern isn't new. A sell tax that lets a controller burn from the pair and sync mid-sale is the same mechanism that cost SKYDAO $183K in September.

Sell Hooks Should Finish Before They Touch the Pool

The fix TENDRIL needed was about ordering and reach. Move the seller's tokens into the pair before any tax processing runs, or defer tax sales and burns to a separate transaction, and the swap prices against honest reserves. Better still, never give any contract a function that pulls tokens out of a live AMM pair and resyncs it, because whoever can trigger that function can rewrite the pool's prices. For teams building tokens with taxes, burns or reflections wired into transfers, the path worth testing is every route from a public transfer to the pair's balance. To see how Olympix maps those routes and hands back a runnable counterexample for each one that breaks, book a technical walkthrough.

On-Chain References

Exploit tx (Nov 4, 16:02:45 UTC): 0xfdff...0059

Vulnerable TENDRIL token: 0xb1af...73d2

Victim, TENDRIL/USDT pair: 0x5de0...99e0

PoolControl: 0xd00b...54fe

Attack contract: 0xc8cb...e0f8

Operator: 0xb81e...21ff

Profit beneficiary: 0xb87c...1373

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.