← All news

On-chain reconstructionBNB Chain

TENDRIL: how a sell-triggered burn emptied the pool

A sale triggered the controller to burn twice the sale amount from the pool and update its reserves. Only then did the remaining tokens enter for the final swap. We verified the function, the historical calls and the arithmetic behind the drain.

The mechanism in plain English

A liquidity pool holds two assets. In this case they were TENDRIL and USDT. The critical event was not simply that someone burned tokens: the tokens were taken from the pool’s own balance. That distinction changes what remains available for the next exchange.

On 4 November 2026 at 16:02:45 UTC, the transaction records almost the entire TENDRIL side being transferred to the dead address, followed by a reserve update. The pool then recorded approximately 4.42 million USDT against only 0.000000000000006678 TENDRIL. Next, approximately 27.34 billion TENDRIL entered the pool and almost all of those USDT were transferred out.

We verified the token source against its historical executable bytecode, then checked the call trace and the controller’s executed arithmetic. The critical ordering is built into the token’s sale path: the controller runs before the seller’s remaining tokens are credited to the pool.

Three stages: 78.106 billion TENDRIL leave the pool for the dead address; Sync records 6,678 raw token units against 4.421 million USDT; 27.337 billion TENDRIL enter and 4.421 million USDT leave, with two raw USDT units remaining.
Sequence reconstructed from logs 25–37. The balance before the burn includes the USDT return at log 27. Diagrams are schematic, not to scale.

The entry point: a sale activates the controller

The outer router call invokes transferFrom on TENDRIL to sell 39,053,180,124.060957132414461269 tokens into the main pool. In the token’s private _transfer function, a transfer to _uniswapV2Pair is taxed at 30% unless the sender is _poolControl or _markControl. The executing contract was neither exception.

The token first moves 11,715,954,037.218287139724338380 TENDRIL—the 30% tax—to _poolControl. It then calls _poolControl.sellToken(amount), passing the original sale amount, not just the tax. This call completes before _basicTransfer credits the remaining 70% to the pool.

That ordering explains why the controller can change the pool’s token balance and reserves before the router performs the final swap. The trace confirms the actual chain of external calls; the internal _transfer and _basicTransfer steps are established by the matching token source.

Observed call chain and matching token logic (simplified):
router → TENDRIL.transferFrom(seller, pool, saleAmount)
  _transfer: move 30% of saleAmount to poolControl
  → poolControl.sellToken(saleAmount)
      → TENDRIL.controlburn(2 × saleAmount)
      → pool.sync()
  _transfer resumes: credit remaining 70% to pool
router → pool.swap(...)

The exact permission: controlburn(uint256)

The function that removes the pool’s tokens is controlburn(uint256 amount). Its sole caller check is msg.sender == _poolControl. Once that check passes, it calls the private _basicTransfer with the pool as sender, the dead address as recipient and the supplied amount.

This directly subtracts from the token’s own balance mapping for the pool and adds to the dead address balance. It does not ask the pool for an allowance, burn LP tokens or invoke a liquidity withdrawal. The transfer is possible because the token contract itself controls its balance bookkeeping.

At the block immediately before the incident, _poolControl was 0xd00b…54fe and _uniswapV2Pair was 0x5de0…99e0. The historical call trace shows exactly that controller calling controlburn, with selector 0xa94485a3.

This was not an unrestricted public burn function: an arbitrary address would fail its caller check. The path observed here reaches the privileged function indirectly through the sale-triggered controller call.

Meaning of the verified token code (pseudocode):
controlburn(amount):
  require caller == poolControl
  balance[pool] -= amount
  balance[deadAddress] += amount
  emit Transfer(pool, deadAddress, amount)

1. What was in the pool immediately before the burn?

The last reserve update before this segment, log 25, recorded approximately 78.106 billion TENDRIL and 3.9567 million USDT. Log 27 then records 464,329.644545997389948674 USDT returning to the pool. No TENDRIL movement intervenes before the pool-to-dead transfer.

Combining that reserve event with the following USDT transfer reconstructs the position immediately before the burn: approximately 78.106 billion TENDRIL and 4.4210 million USDT. This is a reconstructed balance, not a separate Sync event.

Immediately before log 28Full token units
TENDRIL78,106,360,248.121914264828929216
USDT4,421,023.357469283419586501

2. The tokens leave the pool for the dead address

Log 28 records 78,106,360,248.121914264828922538 TENDRIL moving from the pool, 0x5de0…99e0, to 0x0000…dead. Subtracting that amount from the previous token reserve leaves only 6,678 raw units.

The controller’s historical opcode trace records a multiplication of the original sale amount by two. The resulting 78,106,360,248.121914264828922538 TENDRIL exactly matches both the controlburn call argument and the Transfer event. It is only 6,678 raw units smaller than the pool’s token balance at that point.

TENDRIL has 18 decimals. Those 6,678 units therefore mean 0.000000000000006678 TENDRIL, not 6,678 whole tokens. In practical terms, the token side has been emptied.

Here “burn” means a transfer to the dead address. The verified _basicTransfer code does not reduce _tTotal, and totalSupply was unchanged between the blocks immediately before and after this transaction. It is not a burn of LP tokens: the transferred asset is TENDRIL itself.

The pool-to-dead transfer has no matching USDT output at this step. Almost all the TENDRIL leaves while the USDT remains. The subsequent USDT payments at logs 30–32 come from the intermediate address, not from the pool.

3. The reserve update records the imbalance

Log 29 is a Sync event from the pool. It records the two reserves as 4,421,023.357469283419586501 USDT and 0.000000000000006678 TENDRIL.

The call trace resolves what the event alone could not: immediately after controlburn returns, the controller explicitly calls sync() on the pool (selector 0xfff6cae9). The resulting Sync event records the reduced token balance before the remaining sale tokens arrive.

The important order is visible: the token balance is reduced first; the new near-zero token reserve is recorded next; the final token input happens afterwards. The old pool proportions are no longer the recorded reference for that final swap.

At log 29Recorded reserve
USDT4,421,023.357469283419586501
TENDRIL0.000000000000006678

4. New tokens enter; almost all USDT leaves

Log 33 records 27,337,226,086.842669992690122889 TENDRIL entering the pool from the executing contract, 0xc8cb…e0f8. That input is enormous relative to the tiny token reserve just recorded.

Log 35 records 4,421,023.357469283419586499 USDT leaving the pool for that executing contract. Log 36 updates the USDT reserve to just two raw units: 0.000000000000000002 USDT. Log 37 records the matching Swap input and output.

The token input and USDT output therefore agree both with the transfer events and with the final reserve update. This is the drain shown in the diagram. It is not the same measurement as the net USDT outflow across the entire transaction.

Why can a tiny token reserve produce such a large output?

In a constant-product pool, the amount of one asset received depends on the input relative to the reserve of the other asset. Ignoring fees and rounding for illustration, output equals the USDT reserve multiplied by token input divided by token reserve plus token input.

If the recorded token reserve is almost zero, the input dominates that denominator. The fraction approaches one, so the output approaches the entire USDT reserve. This explains the economic effect of the observed reserve change; it is not a line-by-line reproduction of this pool’s implementation.

The tiny example below isolates this effect. It is not an estimate of the real transaction and does not use its fee settings.

Illustrative pool with 100 USDTToken reserveToken inputUSDT output, ignoring fees
Before a reserve reduction10010About 9.09
After a reserve reduction0.00000110About 99.99999

The issue is not that the tokens became more valuable through market demand. The recorded token reserve had collapsed before the next sale. A token burning mechanism can be dangerous when it removes the pool’s assets and changes the reserve reference used for subsequent swaps.

The setup and settlement around this sequence

The burn and final swap are only part of a longer transaction. Reading the earlier and later logs prevents the final 4.42 million USDT transfer from being mistaken for the net amount extracted.

LogsObserved movementWhy it matters
230,641,402.47 USDT enter the executing contractFunding is received before the trading sequence.
3–1414,546.35 USDT go to another pool; a route through other pools delivers about 5.99 billion TENDRILThis is an earlier route, separate from the main pool.
15–194,097,517.64 USDT enter the main pool; about 488.34 billion TENDRIL go to the intermediate addressThis increases the USDT available before the final drain.
20–26About 11.72 billion TENDRIL move through the intermediate address into the pool; 696,494.47 USDT move out to that addressAn intermediate swap occurs before the burn.
27–29464,329.64 USDT return; TENDRIL moves to dead; reserves updateThis creates the recorded state shown in the diagram.
30–32232,164.82 USDT leave the intermediate address for three recipientsThese transfers do not come directly from the pool.
33–37About 27.34 billion TENDRIL enter; 4,421,023.36 USDT leaveThe final swap empties almost all the remaining USDT.
38–39The original USDT funding amount is returned; 308,959.37 USDT go to the final recipientFunding return and payout must be separated from pool outflow.

The source and trace now establish that the 11.72 billion TENDRIL intermediate transfer is the 30% sale tax and that the controller performs the burn and sync within sellToken. The other receiving addresses are still address roles, not established identities. This transaction alone does not reconstruct every earlier TENDRIL acquisition.

Why renounced ownership did not prevent this path

The historical owner() getter returned the zero address at block 131653426, immediately before the incident. However, controlburn checks the controller address, not owner(). Renouncing ownership therefore did not remove this already-configured permission or stop the token from calling that controller during a sale.

The observed transaction did not require an owner call or a pool approval to move the pool’s TENDRIL. This establishes how this particular path remained available; it does not establish who controlled the recipient wallets or whether any party colluded.

How much actually left the pool?

Across the whole transaction, 5,117,517.824288279504509510 USDT left the pool and 4,561,847.285535350596034371 USDT entered. The net difference is 555,670.538752928908475139 USDT. The final 4.42 million transfer includes USDT introduced earlier in the same transaction.

DestinationUSDTReceipt logs
Final recipient · 0xb87c…1373308,959.3739
Three other recipient addresses232,164.8230–32
Another trading pool · 0x893e…cf5014,546.353
Total, rounded to cents555,670.54Reconciled

The final payout of 308,959.37 USDT is not total campaign profit. Earlier purchases, gas and other asset positions remain outside this calculation. The exact reconciliation includes a one-raw-unit USDT residue in the intermediate address.

How we verified the code and execution

We retrieved the explorer-verified TENDRIL source through the Etherscan API and recompiled it with Solidity v0.8.24+commit.e11b9ed9, optimization disabled. All executable runtime bytes match the token code read at block 131653426. The compiler metadata trailer is excluded from that comparison because the local source-file name changes the metadata hash.

The historical call trace confirms transferFrom → sellToken → controlburn → sync, followed by the final swap. We decoded the burn argument and matched it exactly to the Transfer event. A separate custom opcode trace of the controller confirms saleAmount × 2 and the resulting value on the execution stack. The 30% tax, remaining 70%, burn amount and final reserves all reconcile using integer arithmetic.

The controller itself has no explorer-verified source in the API response. Its executed calculation and calls are nevertheless visible in the historical traces. We do not claim to have audited every controller branch, its full security model or the entire campaign’s economics. The causal path described here is the path actually executed in this transaction.

The original receipt was also checked through two public RPC endpoints. This remains a retrospective reconstruction, not a claim that Archalis detected the incident in advance.

Verify the evidence

Incident transaction: 0xfdff4fba…e40059. Block 131653427; receipt status successful. Log numbers refer to the logIndex values returned in the transaction receipt.

TENDRIL contract: 0xb1af…73d2. Pool: 0x5de0…99e0.

Data rechecked on 6 November 2026 using two public BNB Chain RPC endpoints. The receipts matched; all USDT address-level net flows reconciled exactly. Token and pair metadata were checked at capture time. Amounts are denominated in the USDT token at 0x55d3…7955; no fiat valuation is implied.

Address-level USDT ledger

Net flows below cover only USDT Transfer events in this transaction. Positive values mean net inflows; the TENDRIL pool is the sole net source. Zero-net funding and execution addresses are included to make the perimeter explicit. The verification used raw integers; these amounts preserve all 18 decimals.

Address / roleNet USDT
0x238a…e6c40.000000000000000000
0xc8cb…e0f80.000000000000000000
0x893e…cf5014546.348936756980821733
0x5de0…99e0-555670.538752928908475139
0xd00b…54fe0.000000000000000001
0xc161…d1c646432.964454599738994867
0x8137…26eb69649.446681899608492301
0xad8f…deb0116082.411136499347487168
0xb87c…1373308959.367543173232679069

Sources

Investigate a token of your own

Start with its public risk findings. A free account adds saved tokens, comparison and five fresh checks per UTC day.

Check a BNB Chain token → · Create a free account

Ratings are automated assessments of structural risk from public blockchain data, not investment advice. Methodology.