All incidents

CS Pair Balance Burn Drain

Share
May 23, 2023 16:43 UTCAttackLoss: 954,285 USDTPending manual check1 exploit txWindow: Atomic
Estimated Impact
954,285 USDT
Label
Attack
Exploit Tx
1
Addresses
2
Attack Window
Atomic
May 23, 2023 16:43 UTC → May 23, 2023 16:43 UTC

Exploit Transactions

TX 1BSC
0x906394b2ee093720955a7d55bff1666f6cf6239e46bea8af99d6352b9687baa4
May 23, 2023 16:43 UTCExplorer

Victim Addresses

0x8bc6ce23e5e2c4f0a96429e3c9d482d74171215eBSC
0x6bc2823de2c3718d3669c2e7036e1d888c4107a1BSC

Loss Breakdown

954,285USDT

Similar Incidents

Root Cause Analysis

CS Pair Balance Burn Drain

1. Incident Overview TL;DR

On BNB Smart Chain block 28466977, transaction 0x906394b2ee093720955a7d55bff1666f6cf6239e46bea8af99d6352b9687baa4 drained the PancakeSwap CS/USDT pool by turning the CS token's own transfer hook into a public reserve-destruction primitive. An attacker-controlled helper contract flash-borrowed 80,000,000 USDT, manipulated the CS token's sellAmount state through repeated sells, and then used self-transfers to force the token contract to burn pair-held CS before each subsequent sale.

The root cause is a protocol bug in the CS token, not a PancakeSwap pricing bug. CS._transfer() allows any non-pair transfer after a sell to call sync(), and CS.sync() subtracts sellAmount * 80% directly from the pair's CS balance before calling PancakePair.sync(). That lets any public trader fabricate scarcity in the CS reserve and extract excess USDT from the pool. The victim pair lost 954,285.001826338883421706 USDT, while the attacker EOA finished with 714,285.001826338883421706 USDT after repaying the flash lender's exact 240,000 USDT premium. The same balance diff shows an additional gas cost of 0.232952607 BNB.

2. Key Background

CS (0x8bc6ce23e5e2c4f0a96429e3c9d482d74171215e) is a fee-on-transfer token paired against USDT in Pancake pair 0x6bc2823de2c3718d3669c2e7036e1d888c4107a1. The pair itself prices trades from token balances and trusts its own sync() function to checkpoint those balances into reserves.

The dangerous design choice sits in the token, not the pair. The verified CS source keeps a mutable sellAmount state variable, sets it on every sell, and later reuses it inside a private sync() function that can mutate pair-held CS balances. The owner address 0x382e9652ac6854b56fd41dabcfd7a9e633f1edd5 was fee-whitelisted in the incident pre-state, which mattered because the attacker could route a large public buy to that address without paying the 1% buy burn on that leg.

The collector's pre-state summary confirms the relevant configuration at block 28466976: limitBuy = 5000e18, limitSell = 3000e18, the owner was fee-whitelisted, the helper contract was not fee-whitelisted, and the pair was not fee-whitelisted. The seed balance diff also shows that the pair started with material liquidity: 1,772,966.659656957581005191 USDT before the exploit.

3. Vulnerability Analysis & Root Cause Summary

The vulnerability class is attacker-triggerable reserve corruption inside a fee-on-transfer token. The CS token stores the previous sell size in sellAmount, then allows any later non-pair transfer to call sync() as long as sellAmount >= 1. That sync() implementation computes burnAmount = sellAmount * 800 / 1000, subtracts that amount from _tOwned[uniswapV2Pair], credits the dead address, and immediately calls IUniswapV2Pair(uniswapV2Pair).sync(). Because PancakePair sync() blindly snapshots current token balances into reserves, the pair accepts the token contract's manipulated CS balance as ground truth.

The attacker exploited this in a fully permissionless way. They first accumulated CS with 100 public buys of 5,000 gross CS each and then sent one large buy of 2,257,285.265656402636078799 CS to the fee-whitelisted owner sink. After that setup, each 3,000 CS sell set sellAmount = 3000e18, and each following transfer(self, 2) triggered a 2,400 CS burn from the pair before the next sell. The trace shows this happened 164 times, plus one earlier burn of 132.936044619684695726 CS, causing the pair's CS inventory and reserves to collapse while USDT payouts remained temporarily favorable to the attacker.

The explicit invariant break is: AMM reserves must only move through economically matched pair operations, not through unilateral token-side edits of pair balances. The code-level breakpoint is the CS token's sync() path that destroys pair-held inventory and then commits the manipulated balance into Pancake reserves.

Verified CS token source:

bool canSell = sellAmount >= 1;
if (canSell && from != address(this) && from != uniswapV2Pair && from != owner() && to != owner() && !_isLiquidity(from,to)) {
    sync();
}

function sync() private lockTheSync {
    uint256 burnAmount = sellAmount.mul(800).div(1000);
    sellAmount = 0;
    if (_tOwned[uniswapV2Pair] > burnAmount) {
        totalBurnAmount += burnAmount;
        _tOwned[uniswapV2Pair] -= burnAmount;
        _tOwned[address(burnAddress)] += burnAmount;
        emit Transfer(uniswapV2Pair, address(burnAddress), burnAmount);
        emit Burn(uniswapV2Pair, address(burnAddress), burnAmount);
        IUniswapV2Pair(uniswapV2Pair).sync();
    }
}

Verified Pancake pair source:

function sync() external lock {
    _update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1);
}

4. Detailed Root Cause Analysis

The attacker path is deterministic from public state alone.

  1. The attacker flash-borrowed 80,000,000 USDT from Pancake pair 0x7efaef62fddcca950418312c6c91aef321375a00.
  2. They performed 100 small taxed buys, each requesting 5,000 gross CS. The collector summary shows those buys delivered 4,950 net CS each to the helper contract, for 495,000 gross CS acquired across the loop.
  3. They then executed one large buy that routed 2,257,285.265656402636078799 CS to the fee-whitelisted owner sink. Because the recipient was whitelisted, that leg bypassed the normal buy fee and stripped a large amount of CS from the pair into a passive sink.
  4. The attacker sold 3,000 gross CS once, which set sellAmount = amount inside CS._transfer().
  5. After that, every transfer(self, 2) entered _transfer() as a non-pair transfer while sellAmount >= 1, so the token contract called sync() before processing the transfer. sync() burned 2,400 CS from the pair and then forced PancakePair sync() to accept the reduced CS balance as the new reserve level.
  6. The attacker repeated that burn-then-sell sequence 164 times. Each iteration made the pair appear scarcer in CS than it really should have been, so selling the next 3,000 gross CS yielded more USDT than a fair reserve state would allow.
  7. Once the helper had withdrawn enough USDT, it repaid 80,240,000 USDT to the flash-loan source and forwarded the remaining balance to the initiating EOA.

The trace captures the reserve-burn signature directly:

emit Transfer(from: PancakePair: [0x6BC2823De2c3718D3669C2E7036E1D888C4107a1], to: 0x000000000000000000000000000000000000dEaD, value: 132936044619684695726)
emit Burn(from: PancakePair: [0x6BC2823De2c3718D3669C2E7036E1D888C4107a1], to: 0x000000000000000000000000000000000000dEaD, value: 132936044619684695726)
PancakePair::sync()
...
emit Burn(from: PancakePair: [0x6BC2823De2c3718D3669C2E7036E1D888C4107a1], to: 0x000000000000000000000000000000000000dEaD, value: 2400000000000000000000)
PancakePair::sync()

The collector counted those events as:

  • 164 burns of exactly 2,400 CS.
  • 1 additional burn of 132.936044619684695726 CS.
  • 164 self-transfer sync triggers of amount 2.

The balance diff confirms the economic consequence. The CS/USDT pair lost 954,285.001826338883421706 USDT and 2,656,100.508356104823230755 CS, while the dead address gained 398,732.936044619684695726 CS. The owner sink accumulated 2,257,285.265656402636078799 CS from the fee-free buy leg. These state transitions are exactly what the broken invariant predicts.

5. Adversary Flow Analysis

The adversary cluster consists of EOA 0x2cdeee9698ffc9fcabc116b820c24e89184027ba and helper contract 0x90fa57d23b85cdd52c46b85636f44c47926ee2e3. The EOA sent the seed transaction and ultimately received the profit; the helper contract executed the flash borrow, swaps, self-transfers, and repayment.

The on-chain flow was:

  1. 0x2cdeee... called helper 0x90fa57....
  2. The helper borrowed 80,000,000 USDT from the public USDT/BUSD Pancake pair.
  3. It approved PancakeRouter 0x10ED43C718714eb63d5aA57B78B54704E256024E for CS and USDT.
  4. It bought CS 100 times into itself, then bought 2,257,285.265656402636078799 CS into owner sink 0x382e9652ac6854b56fd41dabcfd7a9e633f1edd5.
  5. It sold 3,000 gross CS once to seed sellAmount.
  6. It repeated transfer(self, 2) followed by another 3,000 gross CS sell 164 times, harvesting inflated USDT proceeds after each reserve burn.
  7. It repaid the flash lender and transferred residual USDT to the initiating EOA.

The repayment is visible in the trace:

BEP20USDT::transfer(0x7EFaEf62fDdCCa950418312c6C91Aef321375A00, 80240000000000000000000000)
emit Transfer(from: 0x90Fa57D23b85cdD52C46b85636f44c47926ee2e3, to: 0x7EFaEf62fDdCCa950418312c6C91Aef321375A00, value: 80240000000000000000000000)

The collector's receipt-derived metrics align with that flow:

  • usdt_received_total from attacker sells: 80,954,285.001826338883421706
  • flash-loan premium paid to lender: 240,000
  • self_transfer_sync_triggers.count: 164
  • attacker_sells.count: 165

This is ACT because every step uses public contracts, public liquidity, and attacker-deployed code only. No privileged signature, admin action, or compromised key is involved.

6. Impact & Losses

The measurable victim loss is the USDT drained from the CS/USDT pair:

  • USDT loss from pair 0x6bc2823de2c3718d3669c2e7036e1d888c4107a1: 954,285.001826338883421706 USDT

Additional state damage is visible in the CS side of the pool:

  • Pair CS balance delta: -2,656,100.508356104823230755 CS
  • Dead-address CS gain: 398,732.936044619684695726 CS
  • Owner-sink CS gain: 2,257,285.265656402636078799 CS

Attacker-side realization:

  • Attacker EOA final USDT delta: +714,285.001826338883421706 USDT
  • Flash-lender USDT delta: +240,000 USDT
  • Attacker gas cost: 0.232952607 BNB

The loss is confined to the CS/USDT market in the observed transaction, but the underlying issue is broader: any public trader able to set sellAmount and then trigger a non-pair transfer can repeat the reserve-burn primitive until the token's burn cap or available pair inventory limits the strategy.

7. References

  1. Seed transaction: 0x906394b2ee093720955a7d55bff1666f6cf6239e46bea8af99d6352b9687baa4
  2. CS token: 0x8bc6ce23e5e2c4f0a96429e3c9d482d74171215e
  3. Pancake CS/USDT pair: 0x6bc2823de2c3718d3669c2e7036e1d888c4107a1
  4. Flash-loan source pair: 0x7efaef62fddcca950418312c6c91aef321375a00
  5. Fee-whitelisted owner sink: 0x382e9652ac6854b56fd41dabcfd7a9e633f1edd5
  6. Verified CS token source and Pancake pair source from the collected artifacts and BscScan code pages
  7. Collector artifacts used for validation: transaction metadata, balance diff, full trace, and pre-state summary