All incidents

CoinToken Burn Reserve Drain

Share
Sep 07, 2023 07:44 UTCAttackLoss: 30.5 BNBPending manual check1 exploit txWindow: Atomic
Estimated Impact
30.5 BNB
Label
Attack
Exploit Tx
1
Addresses
2
Attack Window
Atomic
Sep 07, 2023 07:44 UTC → Sep 07, 2023 07:44 UTC

Exploit Transactions

TX 1BSC
0x84bd77f25cc0db493c339a187c920f104a69f89053ab2deabb93c35220e6dfc0
Sep 07, 2023 07:44 UTCExplorer

Victim Addresses

0x0fdfcfc398ccc90124a0a41d920d6e2d0bd8ccf5BSC
0xdbe783014cb0662c629439fbbba47e84f1b6f2edBSC

Loss Breakdown

30.5BNB

Similar Incidents

Root Cause Analysis

CoinToken Burn Reserve Drain

1. Incident Overview TL;DR

On BSC block 31528198, transaction 0x84bd77f25cc0db493c339a187c920f104a69f89053ab2deabb93c35220e6dfc0 executed a single-transaction drain against the CoinToken/WBNB PancakeSwap V2 pool at 0xdbe783014cb0662c629439fbbba47e84f1b6f2ed. The attacker bought CoinToken, repeatedly called CoinToken's public burn(uint256), forced the pair's apparent CoinToken balance down to 1, called sync(), and then sold 63 CoinToken for 2230503636641143188923 wei of WBNB. The pool lost 30503636641143188923 wei of BNB-equivalent WBNB, while the attacker EOA realized 30478281508143188923 wei net after gas.

The root cause is a code bug in CoinToken's reflection burn accounting. CoinToken._burn(address,uint256) subtracts the same raw _value from both _rOwned[_who] and _tTotal, mixing reflected units with token units. That breaks the reflection invariant, lets any holder distort tokenFromReflection(_rOwned[pair]), and therefore lets an attacker falsify the pair's balanceOf(pair) without removing the pair's reflected position.

2. Key Background

CoinToken is a reflection-style token. For non-excluded accounts, balanceOf(account) is derived from tokenFromReflection(_rOwned[account]), and tokenFromReflection divides reflected balances by the current rate returned from _getRate(). This means supply accounting must preserve a strict unit-consistency relationship between reflected supply and token supply.

PancakeSwap V2 trusts token balances observed from the token contract. When sync() runs, the pair records current token balances as reserves. If a token reports a manipulated balanceOf(pair), PancakeSwap will write that manipulated balance into reserves and price the next swap against it.

The exploit therefore depends on the interaction of two permissionless components:

  • CoinToken's public burn(uint256) entrypoint.
  • PancakeSwap V2 reserve synchronization and swap math based on balanceOf(pair).

3. Vulnerability Analysis & Root Cause Summary

The vulnerability class is an accounting bug in a reflection token integrated with an AMM. CoinToken exposes a public burn path that is callable by any holder, but the implementation updates state in inconsistent units. A correct reflection burn would reduce the burner's reflected balance and the reflected global supply proportionally to the token amount being burned. CoinToken instead subtracts only the raw token amount from _rOwned[_who] while also subtracting that same raw amount from _tTotal. Because _tTotal falls faster than reflected balances, the conversion rate becomes artificially large and tokenFromReflection(_rOwned[pair]) collapses. That false balance is then adopted by PancakeSwap when sync() is called. Once the pair reserve is rewritten to 1 CoinToken, a tiny token input can drain almost all WBNB from the pool.

4. Detailed Root Cause Analysis

The critical victim-side code is the public burn entrypoint and the internal _burn implementation:

function burn(uint256 _value) public {
    _burn(msg.sender, _value);
}

function _burn(address _who, uint256 _value) internal {
    require(_value <= _rOwned[_who]);
    _rOwned[_who] = _rOwned[_who].sub(_value);
    _tTotal = _tTotal.sub(_value);
    emit Transfer(_who, address(0), _value);
}

This logic is broken because _rOwned is tracked in reflected units, while _tTotal is tracked in token units. Subtracting the same raw _value from both variables violates the invariant that reflected balances must remain convertible to token balances through a consistent rate.

The collected trace shows the exploit realizing that bug directly. The attacker-controlled holder repeatedly burns its CoinToken until the pair's reported balance collapses:

CoinToken::burn(99)
...
CoinToken::balanceOf(PancakePair) -> 2
CoinToken::burn(70)
...
CoinToken::balanceOf(PancakePair) -> 1

After the balance collapse, the attacker transfers the last 63 CoinToken to the selling helper and calls PancakePair::sync():

PancakePair::sync()
  CoinToken::balanceOf(PancakePair) -> 1
  WBNB::balanceOf(PancakePair) -> 2265997190154150201517
  emit Sync(reserve0: 1, reserve1: 2265997190154150201517)

At that point the pair's reserve state is corrupted. The next swap is priced against the fake reserve of 1 CoinToken rather than the pool's original reflected position. The same trace then shows the drain:

PancakePair::swap(0, 2230503636641143188923, attackerHelper, 0x)
  WBNB::transfer(attackerHelper, 2230503636641143188923)

The balance-diff artifact confirms the economic result. The WBNB contract balance held by the pair decreases by 30503636641143188923 wei, and the sender EOA's native balance increases by 30478281508143188923 wei net after gas. Flash liquidity is not the root cause here; the exploit is enabled by the malformed burn accounting and the AMM's trust in balanceOf(pair).

5. Adversary Flow Analysis

The adversary flow is fully permissionless and fits the ACT model.

  1. The attacker EOA 0xc892d5576c65e5b0db194c1a28aa758a43bb42a5 deploys helper contracts and funds the strategy.
  2. The attacker spends 2200 WBNB to buy 356286368692880972729922 CoinToken through PancakeSwap.
  3. An attacker-controlled holder contract 0x86105b623dec159d69dc010682f2f87a76380b4a repeatedly calls CoinToken's unrestricted burn(uint256) to manipulate the reflection rate and force CoinToken.balanceOf(pair) down to 1.
  4. The remaining 63 CoinToken are moved to attacker helper 0xe4fdb3f2ed8f0f755842b6ad7ce0c97969cb4b42.
  5. That helper calls PancakePair.sync(), which writes the corrupted reserve state into the pair.
  6. The helper sells 63 CoinToken back through PancakeSwap and receives 2230503636641143188923 wei of WBNB.
  7. Profit is withdrawn back to the originating EOA, which ends the transaction up 30478281508143188923 wei net after gas.

The key adversary-controlled addresses identified in the trace are:

  • 0xc892d5576c65e5b0db194c1a28aa758a43bb42a5: originating EOA and final profit recipient.
  • 0x902aba5b1c299fa7e7707d6e5ba9dc7723c1982d: attacker-created coordinator.
  • 0xe4fdb3f2ed8f0f755842b6ad7ce0c97969cb4b42: attacker helper that syncs the pair and executes the final sale.
  • 0x86105b623dec159d69dc010682f2f87a76380b4a: attacker helper that holds CoinToken and executes the burn sequence.

6. Impact & Losses

The immediate victimized component is the CoinToken/WBNB PancakeSwap V2 pair at 0xdbe783014cb0662c629439fbbba47e84f1b6f2ed, with the underlying logic bug residing in CoinToken at 0x0fdfcfc398ccc90124a0a41d920d6e2d0bd8ccf5.

Measured losses:

  • Pool WBNB depletion: 30503636641143188923 wei (30.503636641143188923 BNB-equivalent).
  • Attacker net profit after gas: 30478281508143188923 wei (30.478281508143188923 BNB).

Beyond direct value loss, the exploit demonstrates that the pool's reserves can be desynchronized from true reflected ownership using only public calls, making the market unsafe as long as the broken CoinToken burn logic remains deployed.

7. References

  • Seed transaction: 0x84bd77f25cc0db493c339a187c920f104a69f89053ab2deabb93c35220e6dfc0
  • CoinToken: 0x0fdfcfc398ccc90124a0a41d920d6e2d0bd8ccf5
  • CoinToken/WBNB PancakeSwap V2 pair: 0xdbe783014cb0662c629439fbbba47e84f1b6f2ed
  • Pancake Router: 0x10ed43c718714eb63d5aa57b78b54704e256024e
  • Evidence used:
    • Collected CoinToken source code
    • Seed transaction trace
    • Seed transaction metadata
    • Seed transaction balance-diff artifact