All incidents

FIREDRAKE Reflection Drain on PancakeSwap

Share
Feb 06, 2023 11:11 UTCAttackLoss: 16.18 WBNBPending manual check1 exploit txWindow: Atomic
Estimated Impact
16.18 WBNB
Label
Attack
Exploit Tx
1
Addresses
2
Attack Window
Atomic
Feb 06, 2023 11:11 UTC → Feb 06, 2023 11:11 UTC

Exploit Transactions

TX 1BSC
0x09925028ce5d6a54801d04ff8f39e79af6c24289e84b301ddcdb6adfa51e901b
Feb 06, 2023 11:11 UTCExplorer

Victim Addresses

0x1954b6bd198c29c3ecf2d6f6bc70a4d41ea1cc07BSC
0x6db8209c3583e7cecb01d3025c472d1eddbe49f3BSC

Loss Breakdown

16.18WBNB

Similar Incidents

Root Cause Analysis

FIREDRAKE Reflection Drain on PancakeSwap

1. Incident Overview TL;DR

On BNB Smart Chain block 25430419, transaction 0x09925028ce5d6a54801d04ff8f39e79af6c24289e84b301ddcdb6adfa51e901b drained the FIREDRAKE/WBNB PancakeSwap pair at 0x6db8209c3583e7cecb01d3025c472d1eddbe49f3. The adversary EOA 0xc726bd0e973722e17eb088b8fcfedaa931fa0293 called exploit contract 0xe02970bd38b283c3079720c1e71001abe001bc83, which borrowed WBNB from DODO pool 0xfeafe253802b77456b4627f8c2306a9cebb5d681, bought FIREDRAKE, called FIREDRAKE.deliver(uint256), and then used the reflection-created phantom FIREDRAKE balance to pull out nearly all WBNB from the pair.

The root cause is a reflection-accounting flaw in FIREDRAKE 0x1954b6bd198c29c3ecf2d6f6bc70a4d41ea1cc07, not the flash loan itself. deliver() is publicly callable by any non-excluded holder, directly reduces _rTotal, and therefore changes the global reflection rate. Because the PancakeSwap pair was not excluded from reflections, balanceOf(pair) increased without any transfer into the pair. PancakePair then treated that synthetic balance increase as real token input and released WBNB. The attacker cluster retained 16179364280405492225 wei of WBNB before gas and 16161900440405492225 wei net after the sender EOA's gas cost.

2. Key Background

FIREDRAKE is a reflection token. For non-excluded holders, balances are not stored directly in token units. Instead, the token stores reflected balances in _rOwned, derives a global rate from _rTotal, and computes user-visible balances as rOwned / currentRate.

The verified FIREDRAKE source shows the relevant mechanism:

function balanceOf(address account) public view override returns (uint256) {
    if (_isExcluded[account]) return _tOwned[account];
    return tokenFromReflection(_rOwned[account]);
}

function deliver(uint256 tAmount) public {
    address sender = _msgSender();
    require(!_isExcluded[sender], "Excluded addresses cannot call this function");
    (uint256 rAmount,,,,,) = _getValues(tAmount);
    _rOwned[sender] = _rOwned[sender].sub(rAmount);
    _rTotal = _rTotal.sub(rAmount);
    _tFeeTotal = _tFeeTotal.add(tAmount);
}

function tokenFromReflection(uint256 rAmount) public view returns(uint256) {
    uint256 currentRate = _getRate();
    return rAmount.div(currentRate);
}

At block 25430418, the pair was still reflection-participating and had live reserves. Independent chain queries show FIREDRAKE.isExcluded(pair) == false, FIREDRAKE.totalFees() == 0, and getReserves() for the pair returned 99995897288241500535820 FDP and 16326701609462839506 wei WBNB before the exploit tx. This matters because UniswapV2-style pairs infer how much token input arrived by comparing stored reserves with token.balanceOf(pair). That assumption fails if a token balance can change without a transfer.

3. Vulnerability Analysis & Root Cause Summary

The vulnerability category is ATTACK. Any FIREDRAKE holder could call deliver() and mutate the global reflection state. That operation reduced _rTotal for the whole reflected-holder set, which lowered currentRate and increased every untouched non-excluded holder's apparent token balance. The FDP/WBNB Pancake pair remained in that reflected-holder set, so its observable FIREDRAKE balance became larger even though no FIREDRAKE transfer entered the pair after the attacker bought tokens. The pair's reserve accounting invariant was therefore broken: the token balance used inside swap() no longer matched the economically received token input since the last reserve update. Once that invariant broke, PancakePair released WBNB against phantom input. The flash loan only supplied temporary capital and was not the vulnerability.

4. Detailed Root Cause Analysis

The invariant that failed is: for the FIREDRAKE/WBNB pair, the token balance observed during swap() must equal the amount of FIREDRAKE that actually arrived in the pair since the last reserve update. FIREDRAKE violated that assumption because deliver() changes _rTotal globally while the pair remained non-excluded.

The code-level breakpoint is FIREDRAKE deliver() together with reflected-balance conversion in balanceOf() and tokenFromReflection(). _getCurrentSupply() derives the reflection rate from _rTotal, so decreasing _rTotal changes every non-excluded holder's displayed balance:

function _getCurrentSupply() private view returns(uint256, uint256) {
    uint256 rSupply = _rTotal;
    uint256 tSupply = _tTotal;
    for (uint256 i = 0; i < _excluded.length; i++) {
        if (_rOwned[_excluded[i]] > rSupply || _tOwned[_excluded[i]] > tSupply) return (_rTotal, _tTotal);
        rSupply = rSupply.sub(_rOwned[_excluded[i]]);
        tSupply = tSupply.sub(_tOwned[_excluded[i]]);
    }
    if (rSupply < _rTotal.div(_tTotal)) return (_rTotal, _tTotal);
    return (rSupply, tSupply);
}

The seed trace shows the exact exploit sequence. First, the adversary borrowed 1363426920555815103015 wei of WBNB from the DODO pool, but only routed 16326701609462839506 wei of WBNB through PancakeRouter to buy 49935372988746381368951 FDP. After that buy, the pair reserve snapshot was 50060579699495119166869 FDP and 32653403218925679012 wei WBNB. The attacker then called deliver(28463162603585437380302), which raised totalFees and changed the pair's observed FDP balance to 11122277578830245110611430 without any intervening FDP transfer into the pair:

0x1954...CC07::deliver(28463162603585437380302)
...
0x6db8...49F3::getReserves() -> 50060579699495119166869 FDP, 32653403218925679012 WBNB
0x1954...CC07::balanceOf(0x6db8...49F3) -> 11122277578830245110611430
0x10ED...024E::getAmountsOut(11072216999130749991444561, [FDP, WBNB]) -> 32506065889868331731

The difference between the pair's observed balance and stored reserve was 11072216999130749991444561 FDP. PancakeRouter priced that phantom balance as enough input to withdraw 32506065889868331731 wei WBNB. The final swap() emitted a Sync event showing the pair left with only 147337329057347281 wei WBNB, confirming that the pair treated the reflection-created balance jump as real input.

The exploit conditions were fully permissionless:

  • The attacker only needed to acquire some FIREDRAKE, which the public pair supplied.
  • The pair had to remain non-excluded from reflections.
  • The pair needed WBNB liquidity to drain.
  • Public router, pair, DODO flash loan, and token entrypoints were sufficient; no privileged role or private order flow was required.

5. Adversary Flow Analysis

The adversary cluster consisted of the sender EOA 0xc726bd0e973722e17eb088b8fcfedaa931fa0293, main exploit contract 0xe02970bd38b283c3079720c1e71001abe001bc83, and helper contract 0x01b2c773b37483d280366ddf48097d79d68cdb37.

The on-chain flow was:

  1. 0xe029...bc83 borrowed WBNB from DODO pool 0xfeafe253802b77456b4627f8c2306a9cebb5d681.
  2. The exploit contract approved PancakeRouter 0x10ed43c718714eb63d5aa57b78b54704e256024e and swapped 16326701609462839506 wei WBNB for 49935372988746381368951 FDP.
  3. The exploit contract called FIREDRAKE.deliver(28463162603585437380302), reducing _rTotal and inflating balanceOf(pair).
  4. Using the helper contract, the adversary called pair.swap(0, 32506065889868331731, helper, "") and received 32506065889868331731 wei WBNB from the pair.
  5. The helper returned 16326701609462839506 wei WBNB so the main exploit contract could complete repayment, and retained 16179364280405492225 wei WBNB as profit before gas.

The decisive trace segment is:

WBNB::transferFrom(exploit, pair, 16326701609462839506)
pair::swap(49935372988746381368951, 0, exploit, 0x)
FIREDRAKE::deliver(28463162603585437380302)
FIREDRAKE::balanceOf(pair) -> 11122277578830245110611430
pair::swap(0, 32506065889868331731, helper, 0x)
emit Sync(11122277578830245110611430, 147337329057347281)

This flow matches the ACT framing exactly: every step was adversary-crafted, every dependency was public, and the success predicate was direct WBNB profit.

6. Impact & Losses

The measurable loss was borne by the FDP/WBNB PancakeSwap pair. The pair lost 16179364280405492225 wei of WBNB reserve value, equivalent to 16.179364280405492225 WBNB, and was left with only 147337329057347281 wei WBNB. The sender EOA also paid 17463840000000000 wei of native gas, so the net cluster profit after gas was 16161900440405492225 wei WBNB-equivalent.

The exploit also corrupted the market's internal price integrity. After the drain, the pair held an enormous reflected FIREDRAKE balance while essentially no WBNB remained, so the market could no longer represent a sane FDP/WBNB price.

7. References

  • Seed transaction: 0x09925028ce5d6a54801d04ff8f39e79af6c24289e84b301ddcdb6adfa51e901b
  • Victim token: 0x1954b6bd198c29c3ecf2d6f6bc70a4d41ea1cc07
  • Victim pair: 0x6db8209c3583e7cecb01d3025c472d1eddbe49f3
  • PancakeRouter: 0x10ed43c718714eb63d5aa57b78b54704e256024e
  • DODO flash-loan pool: 0xfeafe253802b77456b4627f8c2306a9cebb5d681
  • Seed metadata: /workspace/session/artifacts/collector/seed/56/0x09925028ce5d6a54801d04ff8f39e79af6c24289e84b301ddcdb6adfa51e901b/metadata.json
  • Seed trace: /workspace/session/artifacts/collector/seed/56/0x09925028ce5d6a54801d04ff8f39e79af6c24289e84b301ddcdb6adfa51e901b/trace.cast.log
  • Seed balance diff: /workspace/session/artifacts/collector/seed/56/0x09925028ce5d6a54801d04ff8f39e79af6c24289e84b301ddcdb6adfa51e901b/balance_diff.json
  • Verified FIREDRAKE source: /workspace/session/artifacts/collector/seed/56/0x1954b6bd198c29c3ecf2d6f6bc70a4d41ea1cc07/src/Contract.sol