Calculated from recorded token losses using historical USD prices at the incident time.
0xa94338d8aa312ed4b97b2a4dcb27f632b1ade6f3abec667e3bf9f002a75dabe00x5d78cfc8732fd328015c9b73699de9556ef06e8eBSC0xb7f1fff722e68a6fc44980f5d48d6d3dbc1fe9cfBSCOn BNB Chain block 38776240, an unprivileged adversary used public historical burnToken signatures plus an on-chain sell-fee bypass to drain value from the TCH/USDT Pancake pair. The exploit transaction was 0xa94338d8aa312ed4b97b2a4dcb27f632b1ade6f3abec667e3bf9f002a75dabe0, sent by EOA 0xb9596d6e53d81981b9f06ca2ca6d3e422232d575 through helper contract 0x258850ec735f6532fe34fe24ef9628992a9b7e84.
The root cause is two independent but composable logic flaws in TCHtoken at 0x5d78cfc8732fd328015c9b73699de9556ef06e8e. First, burnToken records usedSignatures[keccak256(signature)] before splitSignature canonicalizes v, so a previously used signature can be replayed by changing only its final byte from 27/28 to 0/1. Second, the sell-fee exemption treats any direct token0 reserve surplus greater than 100e18 as LP addition, so sending 101 USDT directly to the pair disables the intended 20 percent sell fee for the attacker’s exit.
TCHtoken is a fee-on-transfer token paired against USDT in Pancake pair 0xb7f1fff722e68a6fc44980f5d48d6d3dbc1fe9cf. The token contract exposes a privileged-signer path that burns 1 percent of the pair’s current TCH balance and then calls , mechanically increasing the TCH price relative to USDT.
burnToken(amount, nonce, signature)sync()The sell path is also non-standard. When transfer or transferFrom sends TCH into the pair, the token usually charges a 20 percent fee. That fee is skipped if isAddOrRemoveLpAdd concludes the transfer is adding liquidity. The heuristic does not verify LP minting; it only compares pair token0 balance against reserves.
Because the pair’s token0 is USDT, anyone can alter the heuristic by transferring USDT directly to the pair before selling TCH. That makes the next TCH-to-pair transfer look like liquidity addition even though no LP tokens are minted.
The vulnerability class is a concrete contract bug, not an economic-only MEV opportunity. TCHtoken implemented replay protection on raw signature bytes instead of on the canonical signed message, and it implemented sell-fee bypass logic using a transient reserve mismatch rather than actual LP mint behavior. Those choices created a permissionless buy-burn-sell strategy that any searcher could reproduce from public data.
The critical replay bug sits in burnToken and splitSignature:
function burnToken(uint256 amount, uint256 nonce, bytes memory signature) external {
bytes32 signatureHash = keccak256(signature);
require(!usedSignatures[signatureHash], "Signature has already been used");
require(isAuthorizedSigner(amount, nonce, signature), "Invalid or unauthorized signature");
usedSignatures[signatureHash] = true;
uint256 deflationAmount = balances[uniswapV2Pair] * deflationFee / 10000;
balances[uniswapV2Pair] -= deflationAmount;
balances[address(0xdead)] += deflationAmount;
IUniswapV2Pair(uniswapV2Pair).sync();
}
function splitSignature(bytes memory sig) internal pure returns (bytes32 r, bytes32 s, uint8 v) {
...
if (v < 27) v += 27;
return (r, s, v);
}
The fee-bypass bug sits in the sell path:
(bool isAdd,,uint addedAmount) = isAddOrRemoveLpAdd(msg.sender, recipient);
if (isAdd) {
if (addedAmount <= 100 * (10 ** 18)) revert("Added liquidity is too low");
fee = 0;
} else if (recipient == uniswapV2Pair) {
fee = numTokens * feePercentage / 10000;
}
function isAddOrRemoveLpAdd(address from, address to) private view returns (bool isAdd, bool isRemove, uint addedAmount) {
...
if (to == uniswapV2Pair && reserve0 < balance0) {
uint addedToken0 = balance0 - reserve0;
return (true, false, addedToken0);
}
return (false, false, 0);
}
The violated invariant is straightforward: one signed burn authorization should be consumable exactly once, and a sell into the pair should pay the configured sell fee unless real liquidity minting occurs. TCHtoken violates both invariants at explicit code-level breakpoints.
The first exploit primitive is replayability of already published burn authorizations. Historical transactions from 0x6117ef8cb0512f7cdd3e4f246a3bf6554ff0c3be show valid burnToken calls with canonical signatures ending in 0x1b or 0x1c. For example:
0x09cd1ee1ea50480b64176d36cf5565b00a7fb81dcaa3d2c89c3a9fdb601fbd0b
input: ...5b48ad16...4516d8b21c
0x1b2d836049713075b8e46076331332d0bfe8ecfa5dfa94f1c5b0b70700e68d53
input: ...6333e4f4...21cde3a1b
Those signatures remain valid if the attacker rewrites only the final v byte to 0x00 or 0x01, because splitSignature adds 27 back before ecrecover. Since replay protection keys on keccak256(signature) before normalization, the modified byte string produces a different usedSignatures key while recovering the same signer for the same (address(this), amount, nonce) message.
The second exploit primitive is deterministic price ratcheting. Every successful replay burns 1 percent of the pair’s TCH balance and calls sync(). That leaves the pair holding fewer TCH against the same USDT reserve and therefore increases the marginal TCH price against USDT. The attack trace for 0xa94338d8... shows repeated TCH::burnToken(...) calls, repeated Transfer events from the pair to the dead address, and repeated sync() calls updating pair reserves.
The third exploit primitive is sell-fee removal. TCHtoken decides whether a transfer into the pair is a sell or an LP add by checking whether pair.token0 balance exceeds reserve0. Because token0 is USDT, a direct transfer of 101 USDT to the pair makes the next TCH transfer look like liquidity addition even though no LP mint occurs. That bypasses the 20 percent sell fee exactly when the attacker needs to exit after the replay loop.
The attack transaction combines all three pieces in one end-to-end path:
burnToken authorizations with altered v bytes.sync().101 USDT directly to the pair.The on-chain balance diff confirms the realized effect. The pair lost 19839290285045719885772 raw USDT units, helper contract 0x4f31fa980a675570939b737ebdde0471a4be40eb gained 1250000000000000000000, and the attack contract gained 18589290285045719885772.
{
"holder": "0x258850ec735f6532fe34fe24ef9628992a9b7e84",
"token": "0x55d398326f99059ff775485246999027b3197955",
"delta": "18589290285045719885772"
}
The adversary-controlled cluster consists of EOA 0xb9596d6e53d81981b9f06ca2ca6d3e422232d575, attack contract 0x258850ec735f6532fe34fe24ef9628992a9b7e84, and funding helper 0x4f31fa980a675570939b737ebdde0471a4be40eb. All are identified directly from the attack transaction metadata, trace, and token balance movements.
The execution flow is:
burnToken signatures sourced from public chain history.0x000000000000000000000000000000000000dead and syncs the pair.101 USDT directly to the pair to trigger the fake-LP branch.1250 USDT and the attack contract retains 18589.290285045719885772 USDT profit.The trace evidence is consistent with that flow. The attack transaction metadata shows the top-level call into attack contract 0x258850ec...; the trace contains repeated TCH::burnToken invocations and a direct USDT transfer to the pair; the balance diff shows the final USDT extraction from the pair.
The measurable asset loss is the USDT extracted from the TCH/USDT pair:
USDT198392902850457198857721819839.290285045719885772 USDTThe pair also lost TCH inventory during the replay loop because every replayed authorization burned 1 percent of the pair’s then-current TCH balance to the dead address. That reserve depletion is part of the price-manipulation mechanism, but the direct quantified economic loss in the collected artifacts is the USDT drain above.
0xa94338d8aa312ed4b97b2a4dcb27f632b1ade6f3abec667e3bf9f002a75dabe00x09cd1ee1ea50480b64176d36cf5565b00a7fb81dcaa3d2c89c3a9fdb601fbd0b0x1b2d836049713075b8e46076331332d0bfe8ecfa5dfa94f1c5b0b70700e68d530x5d78cfc8732fd328015c9b73699de9556ef06e8e0xb7f1fff722e68a6fc44980f5d48d6d3dbc1fe9cf0x6117ef8cb0512f7cdd3e4f246a3bf6554ff0c3be, which exposes the public burn authorizations replayed by the attacker