All incidents

Cellframe Migration Drain

Share
Jun 01, 2023 02:07 UTCAttackLoss: 963,002.99 CELLPending manual check2 exploit txWindow: 0s
Estimated Impact
963,002.99 CELL
Label
Attack
Exploit Tx
2
Addresses
2
Attack Window
0s
Jun 01, 2023 02:07 UTC → Jun 01, 2023 02:07 UTC

Exploit Transactions

TX 1BSC
0xe2d496ccc3c5fd65a55048391662b8d40ddb5952dc26c715c702ba3929158cb9
Jun 01, 2023 02:07 UTCExplorer
TX 2BSC
0x943c2a5f89bc0c17f3fe1520ec6215ed8c6b897ce7f22f1b207fea3f79ae09a6
Jun 01, 2023 02:07 UTCExplorer

Victim Addresses

0xb4e47c13db187d54839cd1e08422af57e5348fc1BSC
0xd98438889ae7364c7e2a3540547fad042fb24642BSC

Loss Breakdown

963,002.99CELL

Similar Incidents

Root Cause Analysis

Cellframe Migration Drain

1. Incident Overview TL;DR

Cellframe's LpMigration contract on BNB Chain let any caller migrate LP_OLD into LP_NEW using attacker-manipulable spot prices and then mint the new LP directly to msg.sender. In block 28708274, the attacker first minted a small LP_OLD position in tx 0xe2d496ccc3c5fd65a55048391662b8d40ddb5952dc26c715c702ba3929158cb9, then in tx 0x943c2a5f89bc0c17f3fe1520ec6215ed8c6b897ce7f22f1b207fea3f79ae09a6 used a 1000 WBNB DODO flash loan and a 500000 CELL Pancake V3 flash loan to skew both the old and new pools. Each successful migrate() call converted a small chunk of attacker-held LP_OLD into a much larger LP_NEW position backed by protocol-owned CELL from 0xb4e47c13db187d54839cd1e08422af57e5348fc1. The attacker then unwound the new LP, repaid both flash sources, and finished the two-transaction sequence with a net profit of 245.387267214078247245 BNB after gas. The direct measurable protocol loss was 963002.994732800177571756 CELL removed from the migrator.

2. Key Background

Cellframe had two relevant token pairs on PancakeSwap V2:

  • OLD_CELL at 0xf3E1449DDB6b218dA2C9463D4594CEccC8934346 with old LP pair LP_OLD at 0x06155034f71811fe0D6568eA8bdF6EC12d04Bed2.
  • CELL at 0xd98438889Ae7364c7E2A3540547Fad042FB24642 with new LP pair LP_NEW at 0x1c15f4E3fd885a34660829aE692918b4b9C1803d.

LpMigration at 0xb4e47c13db187d54839cd1e08422af57e5348fc1 held a large treasury-owned CELL inventory intended to seed migrated LP_NEW positions. Before the exploit tx, the migrator balance diff shows 998946697703500845435675 raw CELL units, so the contract was heavily funded and publicly callable.

The attack needed only public infrastructure:

  • anyone could buy OLD_CELL and mint LP_OLD;
  • DODO pool 0xfeafe253802b77456b4627f8c2306a9cebb5d681 exposed public WBNB flash liquidity;
  • Pancake V3 pool 0xa2c1e0237bf4b58bc9808a579715df57522f41b2 exposed public CELL flash liquidity;
  • LpMigration.migrate(uint256) was permissionless and had no oracle, slippage bound, or protocol-owned recipient check.

The adversary cluster in the observed incident was the EOA 0x2525c811ecf22fc5fcde03c67112d34e97da6079 plus helper contract 0x1e2a251b29e84e1d6d762c78a9db5113f5ce7c48. Contract-creation metadata confirms that the helper was created by the same EOA in the attack block.

3. Vulnerability Analysis & Root Cause Summary

This was an ATTACK-class invariant failure in the migration contract, not an MEV arbitrage. LpMigration treated the live LP_NEW reserve ratio as a pricing oracle and also trusted the live LP_OLD redemption output inside the same attacker-controlled transaction. That let the attacker choose both sides of the migration equation: first manipulate LP_OLD so burning a small LP chunk returns a large amount of WBNB, then manipulate LP_NEW so the computed CELL / WBNB ratio becomes extreme. The contract then approved that inflated CELL amount from its own treasury balance and called Pancake's addLiquidity with msg.sender as the recipient. No fair-value check, oracle anchor, slippage bound, or protocol-side accounting stopped the transfer of treasury CELL into an attacker-owned LP position. The broken invariant is straightforward: migrating LP_OLD into LP_NEW must preserve fair value and must not let a temporary reserve skew turn a small LP position into a disproportionate claim on protocol-owned CELL.

Verified LpMigration source shows the precise breakpoint:

function migrate(uint amountLP) external {
    (uint token0, uint token1) = migrateLP(amountLP);
    (uint eth, uint cell, ) = IUniswapV2Router01(LP_NEW).getReserves();

    uint resoult = cell / eth;
    token1 = resoult * token0;

    IERC20(CELL).approve(ROUTER_V2, token1);
    IERC20(WETH).approve(ROUTER_V2, token0);

    IUniswapV2Router01(ROUTER_V2).addLiquidity(
        WETH,
        CELL,
        token0,
        token1,
        0,
        0,
        msg.sender,
        block.timestamp + 5000
    );
}

The fatal design choice is that both token0 and the cell / eth ratio are attacker-shaped values from the same transaction, while the CELL funding source is the protocol's own balance.

4. Detailed Root Cause Analysis

The public pre-state immediately before the attacker sequence in block 28708274 already contained all required ingredients: the migrator held a large CELL inventory, LP_OLD and LP_NEW were live, and public flash liquidity existed in both WBNB and CELL. No privileged keys, private orderflow, or non-public dependencies were needed.

The setup tx 0xe2d496ccc3c5fd65a55048391662b8d40ddb5952dc26c715c702ba3929158cb9 created the LP_OLD ammunition. Its trace shows the attacker helper receiving 0.1 BNB, swapping through the old pair, and minting 1285052547256630513 raw LP_OLD units. The setup balance diff records the helper finishing with 1893061496724412611 raw OLD_CELL units and the old pair increasing its OLD_CELL balance, which matches the LP mint path.

The exploit tx 0x943c2a5f89bc0c17f3fe1520ec6215ed8c6b897ce7f22f1b207fea3f79ae09a6 then stacked two public flash loans:

0xFeAFe253802b77456B4627F8c2306a9CeBb5d681::flashLoan(1000000000000000000000, 0, helper, ...)
  helper::DPPFlashLoanCall(...)
    0xA2C1e0237bF4B58bC9808A579715dF57522F41b2::flash(helper, 0, 500000000000000000000000, ...)
      helper::pancakeV3FlashCallback(...)

Inside that callback, the attacker dumped borrowed CELL into LP_NEW and pushed WBNB into LP_OLD, making LP_NEW appear CELL-rich and making each LP_OLD burn return far more WBNB than before. One traced migration iteration shows the mechanism exactly:

LpMigration::migrate(128505254725663051)
  PancakeRouter::removeLiquidity(WBNB, OLD_CELL, 128505254725663051, ...)
    Burn amount0: 1597830731238074674 WBNB
    Burn amount1: 12959211882113411 OLD_CELL
  PancakePair::getReserves()
    -> 22632569499991989356 WBNB, 1515612805132058579051174 CELL
  CellFrame::approve(PancakeRouter, 107000332748088908619084)
  PancakeRouter::addLiquidity(
      WBNB,
      CELL,
      1597830731238074674,
      107000332748088908619084,
      ...,
      helper
  )

That single iteration burned only 0.128505254725663051 LP_OLD but tried to pair roughly 1.597830731238074674 WBNB with 107000.332748088908619084 CELL from the migrator's treasury. The exploit tx balance diff confirms that the migrator's CELL balance fell from 998946697703500845435675 to 35943702970700667863919, a raw delta of -963002994732800177571756.

The helper attempted ten equal-size migration chunks, but only nine succeeded. The exploit balance diff shows helper LP_OLD dropping from 1285052547256630513 to 128505254725663054, which is nine spent chunks and one leftover chunk. The final traced migrate() call reverts when CellFrame::transferFrom from the migrator exceeds the migrator's remaining CELL balance, which is why one chunk remains.

After draining the treasury-backed CELL into LP_NEW, the attacker unwound the newly minted LP, swapped excess CELL and OLD_CELL back into WBNB, repaid the 500000 CELL plus fee to the Pancake V3 pool and the 1000 WBNB loan to DODO, and transferred residual WBNB out as profit. The ACT conditions are therefore concrete and minimal: obtain LP_OLD, manipulate LP_OLD and LP_NEW with public liquidity, and ensure the migrator still holds treasury CELL.

5. Adversary Flow Analysis

  1. Prepare LP_OLD inventory: In tx 0xe2d496ccc3c5fd65a55048391662b8d40ddb5952dc26c715c702ba3929158cb9, the attacker EOA called its helper contract with 0.1 BNB. The helper wrapped BNB, bought OLD_CELL, sold part of it back through the fee-on-transfer path, and minted LP_OLD.

  2. Borrow public temporary capital: In tx 0x943c2a5f89bc0c17f3fe1520ec6215ed8c6b897ce7f22f1b207fea3f79ae09a6, the helper borrowed 1000 WBNB from DODO and 500000 CELL from Pancake V3. Both loans were permissionless and fully repaid inside the same transaction.

  3. Manipulate both price inputs: The helper dumped the borrowed CELL into LP_NEW to inflate the CELL / WBNB spot ratio, then traded WBNB into LP_OLD so the prepared LP_OLD position redeemed disproportionately high WBNB when burned.

  4. Call the public migrator repeatedly: The helper called LpMigration.migrate(128505254725663051) in a loop. Each successful call made the migrator remove the caller's LP_OLD, compute a new CELL requirement from skewed LP_NEW reserves, and contribute treasury CELL while minting the resulting LP_NEW back to the helper.

  5. Unwind and realize profit: The helper removed the acquired LP_NEW, sold excess inventory back into WBNB, repaid both flash sources, and transferred the residual WBNB back to the attack controller. Across the setup and exploit sequence, the attacker EOA moved from 0.722500355872381376 BNB-equivalent value to 246.109767569950628621, a net delta of 245.387267214078247245 BNB after gas.

6. Impact & Losses

The directly measured protocol loss was borne by LpMigration, whose CELL inventory fell by 963002994732800177571756 raw units, or 963002.994732800177571756 CELL at 18 decimals. The attack also converted public migration infrastructure into a permissionless treasury drain: any unprivileged actor able to source temporary capital and LP_OLD could execute the same class of attack while the migrator retained sufficient CELL inventory.

The attacker profit predicate is also directly evidenced. The setup balance diff shows the attacker EOA at 722500355872381376 wei before the sequence, and the exploit balance diff shows the same EOA at 246109767569950628621 wei after the exploit tx. Because both balance diffs are post-gas, the resulting 245387267214078247245 wei delta already nets transaction fees.

7. References

  • Verified LpMigration source for 0xb4e47c13db187d54839cd1e08422af57e5348fc1, which exposes the vulnerable migrate(uint256) and migrateLP(uint256) logic.
  • BNB Chain tx 0xe2d496ccc3c5fd65a55048391662b8d40ddb5952dc26c715c702ba3929158cb9, which minted the LP_OLD inventory later consumed by the exploit.
  • BNB Chain tx 0x943c2a5f89bc0c17f3fe1520ec6215ed8c6b897ce7f22f1b207fea3f79ae09a6, whose trace shows the nested DODO and Pancake V3 flashes, repeated migrate() calls, and unwind.
  • Balance diffs for both seed transactions, which quantify the attacker profit and the -963002994732800177571756 CELL loss on LpMigration.
  • Contract-creation metadata showing helper 0x1e2a251b29e84e1d6d762c78a9db5113f5ce7c48 was created by attacker EOA 0x2525c811ecf22fc5fcde03c67112d34e97da6079.