All incidents

CFC Reserve Collapse

Share
Jun 15, 2023 07:06 UTCAttackLoss: 3,466.66 SAFE, 6,124.4 USDTPending manual check1 exploit txWindow: Atomic
Estimated Impact
3,466.66 SAFE, 6,124.4 USDT
Label
Attack
Exploit Tx
1
Addresses
2
Attack Window
Atomic
Jun 15, 2023 07:06 UTC → Jun 15, 2023 07:06 UTC

Exploit Transactions

TX 1BSC
0xa3c130ed8348919f73cbefce0f22d46fa381c8def93654e391ddc95553240c1e
Jun 15, 2023 07:06 UTCExplorer

Victim Addresses

0x595488f902c4d9ec7236031a1d96cf63b0405cf0BSC
0xdd9b223aec6ea56567a62f21ff89585ff125632cBSC

Loss Breakdown

3,466.66SAFE
6,124.4USDT

Similar Incidents

Root Cause Analysis

CFC Reserve Collapse

1. Incident Overview TL;DR

Tx 0xa3c130ed8348919f73cbefce0f22d46fa381c8def93654e391ddc95553240c1e at BNB Chain block 29116479 is an anyone-can-take attack against the CFC ecosystem. An unprivileged actor used one public transaction to borrow USDT through DODO flash liquidity, buy SAFE and then CFC, repeatedly collapse the CFC reserve of the SAFE/CFC PancakePair, drain almost all SAFE from that pool, swap the SAFE back into USDT, repay the loans, and keep the spread.

The root cause is in the CFC token itself. When a user sells into the SAFE/CFC pair, CFC._transfer computes sellAmount, directly debits the pair's internal token balance through CFC.sync(), and then calls the pair's public sync() before the seller's transfer settles. That breaks the AMM invariant that stored reserves must reflect real pair balances, and the resulting excess CFC becomes publicly recoverable through PancakePair skim().

The opportunity is ACT because the full pre-state is publicly reconstructible from chain state before block 29116479, the entire exploit fits in one adversary-crafted transaction, and the attacker uses only public liquidity and public AMM entrypoints. The validated monetary success predicate is profit in USDT for adversary contract 0x8213e87bb381919b292ace364d97d3a1ee38caa4: 0 USDT before, 6124.398799521459371489 USDT after, with 8.106010542430563665 USDT worth of gas paid by the same adversary cluster, for a net delta of 6116.292788979028807824 USDT.

2. Key Background

CFC (0xdd9b223aec6ea56567a62f21ff89585ff125632c) is a fee-on-transfer token paired against SAFE (0x4d7fa587ec8e50bd0e9cd837cb4da796f47218a1) in PancakePair 0x595488f902c4d9ec7236031a1d96cf63b0405cf0. The exploit relies on the fact that PancakePair stores reserves separately from live token balances and exposes two public maintenance functions:

// Verified PancakePair implementation
function skim(address to) external lock {
    _safeTransfer(_token0, to, IERC20(_token0).balanceOf(address(this)).sub(reserve0));
    _safeTransfer(_token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1));
}

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

If a third-party token can make the pair's stored reserve smaller than the pair's actual balance, skim() transfers the difference to any caller. That is exactly what CFC allows.

The attacker also routes through SAFE/USDT pair 0x400db103af7a0403c9ab014b2b73702b89f6b4b7. Validator RPC checks confirm that this pool has the same runtime code hash as the verified PancakePair artifact for the victim pool, so both pools run the same PancakePair implementation. Before the exploit, the SAFE/CFC pair held material liquidity: 3466655815335789642356 SAFE and 100246518127533892079722 CFC in the validated pre-state.

3. Vulnerability Analysis & Root Cause Summary

The vulnerability class is an invariant-breaking transfer hook inside a fee-on-transfer token. In a normal AMM design, pair reserves should move only when the pair contract observes actual token balances after a transfer or swap. CFC violates that rule by mutating the pair's own _tOwned balance while processing a sell. The sell branch in CFC._transfer computes sellAmount = amount * 95 / 100, calls the private sync() helper, and only afterwards completes the seller-to-pair transfer. The private helper subtracts sellAmount directly from _tOwned[uniswapV2Pair], sends half to mineAdd, half to 0xdead, resets sellAmount, and forces IUniswapV2Pair(uniswapV2Pair).sync(). Because the pair is synced to the manipulated lower balance before the user's taxed transfer lands, the later transfer recreates a large positive gap between live pair balance and stored reserve. PancakePair skim() then lets the attacker withdraw that excess CFC without any privilege check. Repeating that sequence collapses the CFC reserve toward dust, which in turn lets a comparatively small CFC sale pull almost the entire SAFE reserve out of the pool.

The exact breakpoint is visible in the verified CFC token code:

// Verified CFC token code
if (to == uniswapV2Pair && !_isAddLiquidity()) {
    sellAmount = amount.mul(95).div(100);
    sync();
}
...
function sync() private {
    if (_tOwned[uniswapV2Pair] > sellAmount && _tOwned[address(0xdead)] < _tTotal - minSwap) {
        _tOwned[uniswapV2Pair] -= sellAmount;
        _tOwned[mineAdd] += sellAmount.div(2);
        _tOwned[address(0xdead)] += sellAmount.div(2);
        sellAmount = 0;
        IUniswapV2Pair(uniswapV2Pair).sync();
    }
}

4. Detailed Root Cause Analysis

The pre-state used by the attacker is public and reconstructible from chain state immediately before tx 0xa3c130ed8348919f73cbefce0f22d46fa381c8def93654e391ddc95553240c1e. The validator rechecked the seed metadata, the seed trace, the verified CFC source, the verified PancakePair source, and the balance diff for the seed transaction. No privileged state, off-chain secret, or attacker-only artifact is required.

The attack works because the vulnerable sell path creates a reserve/balance mismatch on demand:

  1. The attacker first acquires enough CFC to interact with the SAFE/CFC pool.
  2. The attacker transfers CFC directly to 0x595488f902c4d9ec7236031a1d96cf63b0405cf0.
  3. Inside CFC, sellAmount is computed as 95% of the transfer amount and CFC.sync() subtracts that amount from the pair's internal CFC balance before the pair actually receives the user's post-tax transfer.
  4. CFC immediately forces PancakePair sync(), so the pair stores the manipulated lower CFC reserve.
  5. The seller's remaining CFC then lands in the pair, creating positive excess balance over the stored reserve.
  6. Because PancakePair skim() transfers balance - reserve to any caller, the attacker can reclaim that excess CFC.
  7. Repeating the cycle ratchets the stored CFC reserve downward until the SAFE/CFC price is catastrophically distorted.

The seed trace shows the reserve-collapse loop and the terminal drain. Early in the exploit, the attacker transfers the full acquired CFC position into the pair and immediately starts a skim() recovery cycle. Later in the same trace, the pool reserve has been crushed to single-digit CFC units and the final SAFE drain becomes trivial:

... CFC::transfer(PancakePair, 39826415935369441925607)
... PancakePair::sync()
... PancakePair::skim(0x8213e87bb381919b292ace364d97d3a1ee38caa4)
... repeated transfer/sync/skim iterations ...
... PancakePair::getReserves() -> 3766006263764885626736 SAFE, 9 CFC
... PancakePair::swap(3766006263764885626693 SAFE, 0, attacker, 0x)
... SAFEToken::transfer(attacker, 3766006263764885626693)

The balance diff matches this mechanism. The SAFE/CFC pair loses 3466655815335789640548 SAFE, retains only 1808 SAFE after the exploit, and its CFC balance drops by 68261198482416212594495. The exploit contract finishes with 6124398799521459371489 additional USDT, 59939559466430581995288233 LP tokens, and 1000000000000000002 residual CFC.

The gas conversion used in the validated success predicate is also deterministic. The sender EOA 0x106016cdc3878c7ff2b386303000db0001d838eb spends 34405215000000000 wei according to the balance diff. Validator RPC checks confirmed that the Pancake WBNB/USDT pair at block 29116479 held 19843906858088525182890933 USDT and 84225634585443609706267 WBNB. Converting the gas spend at that block price yields 8106010542430563665 raw USDT units, or 8.106010542430563665 USDT. Subtracting that from the exploit contract's USDT gain gives the net adversary-cluster profit of 6116292788979028807824 raw units, or 6116.292788979028807824 USDT.

5. Adversary Flow Analysis

The attacker flow is a single adversary-crafted transaction sent by EOA 0x106016cdc3878c7ff2b386303000db0001d838eb to exploit contract 0x8213e87bb381919b292ace364d97d3a1ee38caa4. The transaction is self-contained and permissionless.

Stage 1 is flash funding. The exploit contract borrows public USDT liquidity from five DODO pools inside the same call tree. The trace records nested flashLoan calls before any stateful exploit logic runs.

Stage 2 is asset acquisition. The exploit swaps 13000000000000000000000 USDT into SAFE through SAFE/USDT pair 0x400db103af7a0403c9ab014b2b73702b89f6b4b7, then swaps 2515327136323557421986 SAFE into CFC, receiving 39826415935369441925607 CFC after CFC buy-side taxes.

Stage 3 is reserve collapse. The attacker repeatedly calls CFC::transfer(victimPair, reserveCfc) and then PancakePair::skim(attacker). Each repetition lowers the stored CFC reserve and returns the synthetic excess CFC to the attacker. The trace shows this decay explicitly, with transfer amounts stepping down from 20922968599968977506950 CFC to 12758187876418358813, then 637909393820917944, then 3118, then 158, until the reserve is effectively dust.

Stage 4 is value extraction. Once the SAFE/CFC pool reaches 3766006263764885626736 SAFE and 9 CFC in stored reserves, the attacker sells only 841743938029412618031 CFC through the router and receives 3766006263764885626693 SAFE. The attacker then routes the SAFE back through the SAFE/USDT pool and receives 19124398799521459371489 USDT on that final swap leg.

Stage 5 is cleanup and residual state. The contract repays the flash-loan stack, keeps 6124398799521459371489 net-added USDT before gas adjustment, retains 59939559466430581995288233 LP tokens, and ends with 1000000000000000002 CFC still on hand.

6. Impact & Losses

The direct victimized liquidity is the SAFE/CFC pool 0x595488f902c4d9ec7236031a1d96cf63b0405cf0, with the root cause residing in CFC token 0xdd9b223aec6ea56567a62f21ff89585ff125632c. The measured on-chain losses are:

  • SAFE: 3466655815335789640548 raw units (3466.655815335789640548 SAFE) removed from the SAFE/CFC pair.
  • USDT: 6124398799521459371489 raw units (6124.398799521459371489 USDT) accumulated by the exploit contract before attributing gas.

After accounting for 8.106010542430563665 USDT-equivalent gas paid by the adversary EOA, the validated adversary-cluster net profit is 6116.292788979028807824 USDT. The SAFE/CFC pool is left with only 1808 SAFE and heavily distorted CFC reserves, which means the pool is economically unusable after the exploit until liquidity is restored. No separate non-monetary success predicate is needed because the realized monetary extraction fully captures exploit success.

7. References

  • Seed exploit transaction: 0xa3c130ed8348919f73cbefce0f22d46fa381c8def93654e391ddc95553240c1e on BNB Chain block 29116479.
  • Exploit sender EOA: 0x106016cdc3878c7ff2b386303000db0001d838eb.
  • Exploit contract: 0x8213e87bb381919b292ace364d97d3a1ee38caa4.
  • Vulnerable token: CFC 0xdd9b223aec6ea56567a62f21ff89585ff125632c.
  • Drained pair: SAFE/CFC PancakePair 0x595488f902c4d9ec7236031a1d96cf63b0405cf0.
  • Exit pair: SAFE/USDT PancakePair 0x400db103af7a0403c9ab014b2b73702b89f6b4b7.
  • Validator-confirmed WBNB/USDT pricing pair for gas conversion: 0x16b9a82891338f9ba80e2d6970fdda79d1eb0dae.
  • Primary evidence set: seed transaction metadata, verbose execution trace, balance diff, verified CFC source, and verified PancakePair source collected in the session artifacts.