All incidents

SellToken Arbitrary-Pair LP Drain

Share
May 10, 2023 16:16 UTCAttackLoss: 58,060,083.58 QiQi, 303.33 WBNBPending manual check3 exploit txWindow: 15s
Estimated Impact
58,060,083.58 QiQi, 303.33 WBNB
Label
Attack
Exploit Tx
3
Addresses
2
Attack Window
15s
May 10, 2023 16:16 UTC → May 10, 2023 16:16 UTC

Exploit Transactions

TX 1BSC
0x59ed06fd0d44aec351bed54f57eccec65874da5a25a0aa71e348611710ec05f3
May 10, 2023 16:16 UTCExplorer
TX 2BSC
0x904e48ccc1a1eada85f2e3a6444debc428c55f8652ebbebe26e77d02be2902bf
May 10, 2023 16:16 UTCExplorer
TX 3BSC
0x247e61bd0f41f9ec56a99558e9bbb8210d6375c2ed6efa4663ee6a960349b46d
May 10, 2023 16:16 UTCExplorer

Victim Addresses

0x274b3e185c9c8f4ddef79cb9a8dc0d94f73a7675BSC
0x4cd4bf5079fc09d6989b4b5b42b113377ad8d565BSC

Loss Breakdown

58,060,083.58QiQi
303.33WBNB

Similar Incidents

Root Cause Analysis

SellToken Arbitrary-Pair LP Drain

1. Incident Overview TL;DR

An unprivileged attacker drained SellToken’s LP inventory by first listing an attacker-controlled token through the public StakingRewards.addLiquidity() entrypoint and then calling StakingRewards.sell() against a different asset already held by the protocol: the QiQi/SELLC LP token at 0x4cd4bf5079fc09d6989b4b5b42b113377ad8d565. Because sell() trusts the caller-supplied token1 and uses an attacker-controlled AMM quote to determine payout, the staking contract transferred genuine LP inventory out to the attacker helper contract.

The core exploit completed in three on-chain phases. In tx 0x59ed06fd0d44aec351bed54f57eccec65874da5a25a0aa71e348611710ec05f3, the attacker bootstrapped a listed market for its own token. In tx 0x904e48ccc1a1eada85f2e3a6444debc428c55f8652ebbebe26e77d02be2902bf, the attacker repeatedly called sell(f635, 4cd4, amount) and pulled protocol-held LP into an attacker-controlled quote pair. In tx 0x247e61bd0f41f9ec56a99558e9bbb8210d6375c2ed6efa4663ee6a960349b46d, the attacker burned the stolen LP into QiQi and SELLC, then swapped the SELLC leg into 303.326431258573769033 WBNB.

The root cause is an access-control and asset-selection failure inside SellToken’s staking contract. addLiquidity() permissionlessly marks arbitrary attacker tokens as listed, and sell() pays out arbitrary caller-selected token1 assets without checking that the asset is protocol-approved or that the staking contract’s inventory was intended to back the listed token.

2. Key Background

SellToken uses a custom Pancake-style router and factory on BNB Chain. The staking contract at 0x274b3e185c9c8f4ddef79cb9a8dc0d94f73a7675 holds protocol inventory directly, including LP tokens. The QiQi/SELLC LP token at 0x4cd4bf5079fc09d6989b4b5b42b113377ad8d565 is a transferable ERC20 claim on the underlying QiQi and SELLC reserves.

Three background facts from the collected evidence are critical. First, sell() obtains its payout quote from the custom router with the path [token, token1], so the caller controls the asset pair used for valuation. Second, the staking contract stores LP inventory in its own balance rather than in a segregated vault. Third, the protocol’s quote path does not verify that token1 is one of the protocol-approved payout assets.

Two pieces of verified code matter for understanding the exploit. First, the staking contract constructor preconfigures only a narrow set of trusted assets, but later exposes a public listing path. Second, the LP token follows the standard pair pattern where transferred LP can be burned into underlying reserves. The relevant LP burn logic is visible in the verified pair source:

function burn(address to) external lock returns (uint amount0, uint amount1) {
    uint liquidity = balanceOf[address(this)];
    amount0 = liquidity.mul(balance0) / _totalSupply;
    amount1 = liquidity.mul(balance1) / _totalSupply;
    _burn(address(this), liquidity);
    _safeTransfer(_token0, to, amount0);
    _safeTransfer(_token1, to, amount1);
}

That means once the attacker obtains real 0x4cd4... LP tokens from the staking contract, it can destroy them and receive the QiQi and SELLC that back the pair.

3. Vulnerability Analysis & Root Cause Summary

The vulnerability is an attack-path bug in SellToken’s staking contract, not a generic AMM issue. The contract exposes a public addLiquidity(address _token, address token1, uint amount1) function that sets listToken[_token] = true for any caller-supplied token. It also exposes sell(address token, address token1, uint amount) with only require(listToken[token]), meaning the function never validates whether token1 is an approved payout asset. Instead, it computes a quote through getTokenPriceSellc(token, token1, coin) and transfers _sellc units of the arbitrary token1 out of the staking contract’s own balance.

The verified victim code shows the exact unsafe behavior:

function addLiquidity(address _token,address token1, uint amount1) public {
    bool isok = IERC20(_token).transferFrom(msg.sender, address(this), IERC20(_token).totalSupply());
    isok = IERC20(token1).transferFrom(msg.sender, address(this), amount1);
    require(isok);
    IRouters.addLiquidity(_token, token1, lp, amount1, 0, 0, address(this), block.timestamp + 100);
    if (myReward[_token] == address(0)) {
        myReward[_token] = token1;
    }
    listToken[_token] = true;
}

function sell(address token,address token1,uint amount) public {
    require(listToken[token]);
    bool isok = IERC20(token).transferFrom(msg.sender, address(this), amount);
    require(isok);
    uint coin = amount * 50 / 100;
    uint _sellc = getTokenPriceSellc(token, token1, coin);
    if (IERC20(token1).balanceOf(address(this)) < _sellc) {
        IRouters.removeLiquidity(token, token1, lp, 0, 0, address(this), block.timestamp + 100);
    }
    IERC20(token1).transfer(msg.sender, _sellc);
    IERC20(token).transfer(msg.sender, coin);
}

The broken invariant is straightforward: a listed token should only be redeemable against protocol-approved quote assets that intentionally back that token. SellToken breaks that invariant twice. It lets arbitrary users self-list attacker tokens, and it lets those listed tokens redeem against unrelated protocol inventory chosen by the caller. Once those two conditions combine, the staking contract becomes a treasury payout primitive keyed off attacker-controlled AMM pricing.

The violated security principles are equally concrete:

  • no asset whitelist on a treasury payout path;
  • trusting arbitrary ERC20 and LP assets as if they were approved collateral;
  • using attacker-controlled AMM state as a price oracle for treasury transfers;
  • failing to segregate unrelated LP inventory from user-callable redemption logic.

4. Detailed Root Cause Analysis

The exploit begins from a publicly reconstructible BNB Chain pre-state before block 28090570. At that point, the SellToken staking contract, router, factory, QiQi token, SELLC token, and QiQi/SELLC LP already exist on-chain, and the staking contract already holds a large balance of the 0x4cd4... LP token. Nothing privileged is missing from the adversary model: the attacker only needs an EOA, deployable contracts, public liquidity, and public contract entrypoints.

The ACT exploit conditions are all satisfied by this pre-state:

  • the staking contract already holds the attacker-selected token1 asset, namely the valuable 0x4cd4... LP token;
  • the attacker can list a fresh token through public addLiquidity() by pairing it with an attacker-controlled helper token whose transferFrom semantics allow the deposit to succeed;
  • the attacker can create and control the AMM pair used by getTokenPriceSellc(token, token1, coin), which lets the payout quote be manipulated upward after each drain round.

The attacker then deploys helper contracts and prepares a malicious market. The trace for tx 0x59ed... shows the attacker-created helper token at 0x47aa... exposing a writable balance primitive and being used to satisfy the staking contract’s public listing flow:

0x47aa6C247C19ffDad6272998b8719594e310445E::set_balance(
    0xf635FeA87f0A8a444edE1dBB698D875dBb417829,
    100000000000000000000
)
StakingRewards::addLiquidity(
    0xf635FeA87f0A8a444edE1dBB698D875dBb417829,
    0x47aa6C247C19ffDad6272998b8719594e310445E,
    1000000000000000000
)

That same trace shows the consequence of calling the public victim entrypoint: the staking contract accepts the attacker-supplied assets, creates the pair, and marks the attacker token as listed. This is the first code-level breakpoint. From here forward, sell() will accept the attacker token as valid input.

The second breakpoint appears when the attacker points sell() at the protocol’s unrelated LP inventory. In tx 0x904e..., the trace shows the staking contract pricing f635 against the attacker-controlled f635/4cd4 pair and transferring real 0x4cd4... LP out of its own balance:

StakingRewards::sell(
    0xf635FeA87f0A8a444edE1dBB698D875dBb417829,
    PancakePair: [0x4cd4Bf5079Fc09d6989B4b5B42b113377AD8d565],
    18045114
)
...
PancakePair::transfer(
    0xf635FeA87f0A8a444edE1dBB698D875dBb417829,
    990439119291144328257
)

The attacker compounds the drain by recycling each stolen LP payout back into the attacker-controlled f635/4cd4 quote pair. Each recycle deepens the quoted payout, so the next sell() call extracts even more of the staking contract’s 0x4cd4... inventory. The tx 0x904e... balance diff records the first large depletion event directly: staking loses 673615941890716545425569 units of 0x4cd4... in that transaction alone.

The final realization happens in tx 0x247e..., where the attacker burns the stolen LP and exits to WBNB. The trace shows the attacker transferring 20039077964870417428911990 LP units back into the real QiQi/SELLC pair and then calling burn:

PancakePair::transfer(
    PancakePair: [0x4cd4Bf5079Fc09d6989B4b5B42b113377AD8d565],
    20039077964870417428911990
)
PancakePair::burn(0xf635FeA87f0A8a444edE1dBB698D875dBb417829)
...
emit Burn(
    sender: 0xf635FeA87f0A8a444edE1dBB698D875dBb417829,
    amount0: 58060083580333963477961713,
    amount1: 6939399227733031072530260,
    to: 0xf635FeA87f0A8a444edE1dBB698D875dBb417829
)

Immediately afterward, the attacker dumps the extracted SELLC into the public SELLC/WBNB pair. The same trace shows the Pancake pair paying out exactly 303326431258573769033 wei of WBNB:

0x358EfC593134f99833C66894cCeCD41F550051b6::swap(
    0,
    303326431258573769033,
    0xf635FeA87f0A8a444edE1dBB698D875dBb417829,
    0x
)

The balance diffs confirm the net asset movement described by the trace. In tx 0x247e..., the staking contract loses another 20039077964870417428911990 units of 0x4cd4..., the real QiQi/SELLC pair loses 58060083580333963477961713 QiQi and 6939399227733031072530260 SELLC, and the attacker contract gains the corresponding underlying before exiting. That is enough to deterministically support the root-cause conclusion without speculative assumptions.

5. Adversary Flow Analysis

The attacker cluster on BNB Chain consists of EOA 0xc67af66b8a72d33dedd8179e1360631cf5169160, attacker-deployed helper contract 0xf635fea87f0a8a444ede1dbb698d875dbb417829, and the attacker-controlled helper token 0x47aa6c247c19ffdad6272998b8719594e310445e. The EOA acquires seed assets, deploys the helper contract, funds it, and receives the realized value.

The transaction sequence in the validated ACT path is:

  1. Txs 0xd925..., 0xe869..., 0x9943..., 0xe6d5..., and 0xea6a...: public swaps that accumulate seed assets.
  2. Tx 0x3ad68d4f85b63ac8e4e456ee32a39b1907a9be1ab34d43068e545b95f7af6693: attacker helper deployment.
  3. Txs 0x9c727f..., 0x1f8a34..., and 0x169813...: QiQi and WBNB funding transfers into the helper.
  4. Tx 0x59ed06fd0d44aec351bed54f57eccec65874da5a25a0aa71e348611710ec05f3: public addLiquidity listing of the attacker token and creation of attacker-controlled quote infrastructure.
  5. Tx 0x904e48ccc1a1eada85f2e3a6444debc428c55f8652ebbebe26e77d02be2902bf: repeated sell(f635, 4cd4, amount) calls that drain protocol LP inventory and recycle payouts back into the quote pair.
  6. Tx 0x247e61bd0f41f9ec56a99558e9bbb8210d6375c2ed6efa4663ee6a960349b46d: additional sell rounds, LP burn, and final SELLC-to-WBNB exit.

Every step is permissionless. The attacker does not need privileged keys, protocol ownership, or private orderflow. It only needs to deploy contracts, call public entrypoints, and trade against public liquidity. That is why this incident is correctly classified as ACT.

6. Impact & Losses

The measurable loss is the drainage of protocol-controlled QiQi/SELLC LP inventory held by the staking contract and the conversion of the recovered reserves into attacker-controlled assets.

The profit predicate in the validated root cause is also supported directly by the artifacts. The attacker cluster visibly committed 3.700000000000000000 WBNB-equivalent principal, paid 0.102546707000000000 WBNB in aggregate gas, and finished the final unwind with 303.326431258573769033 WBNB-equivalent value. That yields a conservative lower-bound net gain of 299.523884551573769033 WBNB.

Validated loss figures from the collector artifacts are:

  • QiQi: 58060083580333963477961713 raw units (58,060,083.580333963477961713 QiQi at 18 decimals).
  • WBNB: 303326431258573769033 raw units (303.326431258573769033 WBNB at 18 decimals).

The first large LP depletion is visible in tx 0x904e..., where the staking contract loses 673615941890716545425569 units of the 0x4cd4... LP token. The major terminal drain is visible in tx 0x247e..., where the staking contract loses 20039077964870417428911990 more LP units. Those LP tokens are then burned into underlying QiQi and SELLC, and the SELLC leg is sold for WBNB. Operationally, the staking contract’s treasury-like LP inventory is converted into attacker profit through a series of public calls that the contract should never have authorized.

7. References

  1. Seed index: /workspace/session/artifacts/collector/seed/index.json
  2. Tx metadata: 0x59ed06fd0d44aec351bed54f57eccec65874da5a25a0aa71e348611710ec05f3, 0x904e48ccc1a1eada85f2e3a6444debc428c55f8652ebbebe26e77d02be2902bf, 0x247e61bd0f41f9ec56a99558e9bbb8210d6375c2ed6efa4663ee6a960349b46d
  3. Trace artifacts:
    • /workspace/session/artifacts/collector/seed/56/0x59ed06fd0d44aec351bed54f57eccec65874da5a25a0aa71e348611710ec05f3/trace.cast.log
    • /workspace/session/artifacts/collector/seed/56/0x904e48ccc1a1eada85f2e3a6444debc428c55f8652ebbebe26e77d02be2902bf/trace.cast.log
    • /workspace/session/artifacts/collector/seed/56/0x247e61bd0f41f9ec56a99558e9bbb8210d6375c2ed6efa4663ee6a960349b46d/trace.cast.log
  4. Balance diff artifacts:
    • /workspace/session/artifacts/collector/seed/56/0x59ed06fd0d44aec351bed54f57eccec65874da5a25a0aa71e348611710ec05f3/balance_diff.json
    • /workspace/session/artifacts/collector/seed/56/0x904e48ccc1a1eada85f2e3a6444debc428c55f8652ebbebe26e77d02be2902bf/balance_diff.json
    • /workspace/session/artifacts/collector/seed/56/0x247e61bd0f41f9ec56a99558e9bbb8210d6375c2ed6efa4663ee6a960349b46d/balance_diff.json
  5. Verified victim contract source: https://api.etherscan.io/v2/api?chainid=56&module=contract&action=getsourcecode&address=0x274b3e185c9c8f4ddef79cb9a8dc0d94f73a7675
  6. Verified custom router source: https://api.etherscan.io/v2/api?chainid=56&module=contract&action=getsourcecode&address=0xbddfa43dbbfb5120738c922fa0212ef1e4a0850b
  7. Verified QiQi/SELLC LP source: https://api.etherscan.io/v2/api?chainid=56&module=contract&action=getsourcecode&address=0x4cd4bf5079fc09d6989b4b5b42b113377ad8d565