All incidents

StarlinkCoin Pair Drain

Share
Feb 16, 2023 22:48 UTCAttackLoss: 38.36 WBNBPending manual check1 exploit txWindow: Atomic
Estimated Impact
38.36 WBNB
Label
Attack
Exploit Tx
1
Addresses
2
Attack Window
Atomic
Feb 16, 2023 22:48 UTC → Feb 16, 2023 22:48 UTC

Exploit Transactions

TX 1BSC
0x146586f05a4513136deab3557ad15df8f77ffbcdbd0dd0724bc66dbeab98a962
Feb 16, 2023 22:48 UTCExplorer

Victim Addresses

0x518281f34dbf5b76e6cdd3908a6972e8ec49e345BSC
0x425444da1410940cfdfb6a980bd16aa7a5376d6dBSC

Loss Breakdown

38.36WBNB

Similar Incidents

Root Cause Analysis

StarlinkCoin Pair Drain

1. Incident Overview TL;DR

On BSC block 25729305, transaction 0x146586f05a4513136deab3557ad15df8f77ffbcdbd0dd0724bc66dbeab98a962 used three public DODO WBNB flash loans and a single helper contract to drain the Starlink/WBNB Pancake pair at 0x425444dA1410940CFdfB6A980Bd16aA7a5376d6D. The helper first bought Starlink from the pair, then executed 20 public skim() / sync() rounds that repeatedly rewrote the pair's Starlink reserve downward, and finally sold taxed Starlink back through PancakeRouter to extract almost all WBNB from the pool.

The root cause is a deterministic accounting bug in StarlinkCoin at 0x518281F34dbf5B76e6cdd3908a6972E8EC49e345. The owner had already set marketBuyFees=100 and marketSellFees=100, but the cached aggregate denominators buyFeeRate=8 and sellFeeRate=10 were left stale. Because _transfer still allocates fee sub-transfers using those stale denominators, pair-originated taxed transfers debit the sender for more than the requested amount. Public PancakePair maintenance functions then turn that sender-overdebit into a reserve-ratcheting drain.

2. Key Background

StarlinkCoin is a fee-on-transfer token paired with WBNB on PancakeSwap V2. Pancake pairs expose two public maintenance functions that matter here:

  • skim(address) transfers any token balance in the pair that exceeds its stored reserves.
  • sync() overwrites the stored reserves with the pair's current token balances.

For fee-on-transfer tokens, sender conservation is critical: for any taxed transfer, the sender's total debits must equal the requested transfer amount, and the fee sub-transfers must sum to the fee that was deducted from that amount. If the AMM pair is itself the sender, any over-debit directly distorts pair balances and can be crystallized with sync().

The publicly reconstructible pre-state at block 25729304 already exposed the misconfiguration:

{
  "pair_reserves": {
    "reserve0": "57120777503837642765",
    "reserve1": "38359839689566853695"
  },
  "fee_config": {
    "marketBuyFees": "100",
    "marketSellFees": "100",
    "buyFeeRate": "8",
    "sellFeeRate": "10"
  }
}

The same pre-state also showed enough public WBNB liquidity in three DODO pools to fund the exploit in one transaction: 877375841828279330315, 517364914399096708048, and 783090851445088813886 wei WBNB.

3. Vulnerability Analysis & Root Cause Summary

The vulnerability is an attack-class accounting bug in StarlinkCoin, not a benign MEV opportunity. SetBuyValues and SetSellValues mutate the component fee numerators but never recompute the cached denominators buyFeeRate and sellFeeRate. As a result, _transfer computes _fee from the stale denominator and then redistributes that fee using updated numerators divided by the same stale denominator. When marketBuyFees is 100 while buyFeeRate stays 8, a buy-side transfer sends the full requested amount to marketing and still sends amount - _fee to the recipient. When marketSellFees is 100 while sellFeeRate stays 10, the same over-debit happens on sell-side transfers. The sender therefore loses more than amount, violating transfer conservation. Because PancakePair is taxed when it is the sender, every pair-originated transfer reduces the pair's real Starlink balance below its stored reserve, and public skim() / sync() calls let any user ratchet that reserve downward until a small Starlink position can be sold for nearly all WBNB in the pool.

4. Detailed Root Cause Analysis

The code-level breakpoint is visible directly in the verified StarlinkCoin source:

function SetBuyValues(uint lp,uint market, uint burn) external onlyOwner {
    LPBuyFees = lp;
    marketBuyFees = market;
    burnBuyFees = burn;
}

function SetSellValues(uint lp,uint market, uint burn) external onlyOwner {
    LPSellFees = lp;
    marketSellFees = market;
    burnSellFees = burn;
}

if (recipient == uniswapV2Pair) {
    uint256 _fee = amount.mul(sellFeeRate).div(100);
    super._transfer(sender, addressForMarketing, _fee.mul(marketSellFees).div(sellFeeRate));
    amount = amount.sub(_fee);
} else if (sender == uniswapV2Pair) {
    uint256 _fee = amount.mul(buyFeeRate).div(100);
    super._transfer(sender, addressForMarketing, _fee.mul(marketBuyFees).div(buyFeeRate));
    amount = amount.sub(_fee);
}

super._transfer(sender, recipient, amount);

With the observed pre-state, a buy-side transfer of amount computes _fee = amount * 8 / 100, then transfers _fee * 100 / 8 = amount to the marketing address, and still transfers amount - _fee to the buyer. A sell-side transfer similarly sends amount to marketing and 90% of amount to the recipient. This breaks the invariant that all sender debits must sum to the requested transfer amount.

Once the pair becomes the taxed sender, the exploit path is mechanical:

  1. Borrow WBNB from public DODO pools.
  2. Send WBNB into the pair and buy Starlink, causing the pair to over-debit itself on the outgoing Starlink transfer.
  3. Repeatedly top the pair up with Starlink, call skim() to force another pair-originated transfer, and call sync() to rewrite the now-lower balance into reserves.
  4. After enough rounds, sell a small remaining Starlink position back through PancakeRouter and drain almost all WBNB.

The seed trace shows that exact sequence:

0x0fe261ae...::flashLoan(868602083409996537011, 0, 0xD1B5473F..., ...)
0x6098A563...::flashLoan(512191265255105740967, 0, 0xD1B5473F..., ...)
0xFeAFe253...::flashLoan(775259942930637925747, 0, 0xD1B5473F..., ...)
PancakePair::skim(0xD1B5473FFbADd80ff274F672B295bA8811b32538)
PancakePair::sync()
... repeated 20 rounds ...
PancakePair::swap(0, 2194413131285306950264, 0xD1B5473F..., 0x)
WBNB::transfer(0x4Be823dFcdc911F126599d53C871be325AD8593f, 38359839689566746539)

The collected balance diff confirms the reserve damage: the pair's Starlink balance fell from 57120777503837642765 to 11455820279601178532, while the tx sender paid only 0.019318045 BNB in gas and the profit receiver collected 38359839689566746539 wei WBNB. The exploit-related adversary cluster held 0.184058764163191965 BNB/WBNB-equivalent before the transaction and 38.524580408729938504 afterward, for a net gain of 38.340521644566746539 WBNB-equivalent.

5. Adversary Flow Analysis

The adversary cluster was:

  • EOA 0x187473cf30e2186f8fb0feda1fd21bad9aa177ca, which sent the exploit transaction.
  • Helper contract 0xD1B5473FFbADd80ff274F672B295bA8811b32538, which executed the flash loans, reserve-ratcheting loop, repayments, and profit transfer.
  • Profit receiver 0x4Be823dFcdc911F126599d53C871be325AD8593f, which received the final WBNB.

The on-chain lifecycle was:

  1. Flashloan aggregation. The helper borrowed 2156053291595740203725 wei WBNB across three public DODO pools.
  2. Initial buy. The helper transferred all borrowed WBNB into the pair and swapped for Starlink, immediately benefiting from the pair-overdebit bug on the buy.
  3. Reserve ratcheting. The helper executed 20 transfer -> skim -> sync rounds. Each skim() forced the pair to send Starlink again while still subject to the stale-denominator bug, and each sync() wrote the reduced balance back into reserves.
  4. Final drain. The helper sold Starlink through PancakeRouter, the pair paid out 2194413131285306950264 wei WBNB, all flash loans were repaid, and 38359839689566746539 wei WBNB was transferred to the profit receiver.

Every step was permissionless and used only public contracts, public state, and public liquidity. No privileged key, private orderflow, or attacker-specific artifact was required.

6. Impact & Losses

The victim pair lost 38359839689566746539 wei WBNB (38.359839689566746539 WBNB) and was left with only 107156 wei WBNB. Liquidity providers in the Starlink/WBNB pair absorbed the loss. The pair's Starlink reserve was also rewritten to a much lower value through the public sync() calls, leaving the pool effectively broken after the drain.

The exploit transaction's direct, measurable loss is:

[
  {
    "token_symbol": "WBNB",
    "amount": "38359839689566746539",
    "decimal": 18
  }
]

7. References

  1. Exploit transaction: 0x146586f05a4513136deab3557ad15df8f77ffbcdbd0dd0724bc66dbeab98a962
  2. StarlinkCoin: 0x518281F34dbf5B76e6cdd3908a6972E8EC49e345
  3. PancakePair Starlink/WBNB: 0x425444dA1410940CFdfB6A980Bd16aA7a5376d6D
  4. Seed trace and balance-diff artifacts for the exploit transaction
  5. Pre-state observation artifact for block 25729304
  6. Public BscScan pages for the transaction and the verified StarlinkCoin / PancakePair contracts