All incidents

QiQi Reward Quote Override Drain

Share
May 13, 2023 23:12 UTCAttackLoss: 1,614.38 QiQiPending manual check5 exploit txWindow: 30m 16s
Estimated Impact
1,614.38 QiQi
Label
Attack
Exploit Tx
5
Addresses
2
Attack Window
30m 16s
May 13, 2023 23:12 UTC → May 13, 2023 23:43 UTC

Exploit Transactions

TX 1BSC
0x4b9d6687a0188ca3898bd6af55578fc037983a9a80f31e744f494547fe5697a8
May 13, 2023 23:12 UTCExplorer
TX 2BSC
0xd1c5990b3a9930178dbb36c264bd294fc6c9f8f0027a366947ca0d853b571617
May 13, 2023 23:16 UTCExplorer
TX 3BSC
0xfe80df5d689137810df01e83b4bb51409f13c865e37b23059ecc6b3d32347136
May 13, 2023 23:17 UTCExplorer
TX 4BSC
0xda093864178f4ed5813f5b19bd06fa5712ee62aad739dc9bbebc473b8bd73a29
May 13, 2023 23:42 UTCExplorer
TX 5BSC
0x8a453c61f0024e8e11860729083088507a02a38100da8b0c3b2d558788662fa0
May 13, 2023 23:43 UTCExplorer

Victim Addresses

0xeaf83465025b4bf9020fdf9ea5fb6e71dc8a0779BSC
0x0b464d2c36d52bbbf3071b2b0fca82032dcf656dBSC

Loss Breakdown

1,614.38QiQi

Similar Incidents

Root Cause Analysis

QiQi Reward Quote Override Drain

1. Incident Overview TL;DR

On BSC, the attacker turned StakingRewards at 0xeaF83465025b4Bf9020fdF9ea5fB6e71dC8a0779 into a QiQi payout oracle that trusted attacker-chosen market data. After deploying a fake quote token and helper contracts, the attacker seeded valid staking positions, created a fresh QiQi/fake-token pair, and then called StakingRewards.claim(token, token1) with the fake token as token1. In drain transaction 0x8a453c61f0024e8e11860729083088507a02a38100da8b0c3b2d558788662fa0, StakingRewards transferred 1614383680555555545202 QiQi out of its own balance.

The root cause is a logic flaw in StakingRewards.claim: the function prices rewards with caller-supplied token1 instead of the canonical reward quote asset previously stored in myReward[token]. Because the custom router/factory can permissionlessly create new pairs, any unprivileged attacker can manufacture a fake QiQi quote market, feed that market into claim, and drain real QiQi from the staking contract.

2. Key Background

StakingRewards tracks a canonical quote asset per listed token in myReward[token]. For QiQi at 0x0B464d2C36d52bbbf3071B2b0FcA82032DCf656d, the pre-state at block 28187296 already bound the quote asset to SELLC at 0xa645995e9801F2ca6e2361eDF4c2A138362BADe4.

The custom router at 0xBDDFA43dbBfb5120738C922fa0212ef1E4a0850B is also relevant because it is already trusted by StakingRewards as coeAdmin, and its addLiquidity path will create a pair if one does not exist. That means a public attacker can create a new QiQi/attacker-token pair without protocol approval and then force claim to read price data from that attacker-created pair.

The payout path also requires only ordinary staking preconditions. The claimant must have a live staking position, users[token][msg.sender].mnu > 0, and enough elapsed time for block.timestamp > stakedOfTime[token][msg.sender]. The setup transaction 0xfe80df5d689137810df01e83b4bb51409f13c865e37b23059ecc6b3d32347136 satisfied those requirements by funding ten staking positions with standard ERC-20 transfers and approvals.

3. Vulnerability Analysis & Root Cause Summary

This is an ATTACK-class ACT incident, not a privileged compromise. The vulnerable invariant is straightforward: QiQi rewards for a listed staking token must always be priced against the canonical quote asset fixed for that market, not against an arbitrary asset chosen by the claimer. StakingRewards establishes that canonical asset through myReward[token], but claim(address token, address token1) ignores it and instead reads reserves from getTokenPrice(token1, token, banOf).

That breaks unit consistency. banOf is derived from the user’s stored staking amounts, which were accumulated under the canonical market, yet claim re-values those units against whatever asset the caller supplies. Because the router can permissionlessly create a new pair, the attacker can choose both the replacement quote token and its reserve ratio. Once that manipulated pair exists, claim transfers real QiQi from StakingRewards according to attacker-controlled reserves while leaving myReward[QiQi] unchanged.

The exact code-level breakpoint is the reward-pricing line inside StakingRewards.claim:

function claim(address token,address token1) public {
    require(listToken[token]);
    require(users[token][msg.sender].mnu > 0);
    require(block.timestamp > stakedOfTime[token][msg.sender]);
    uint minit=block.timestamp-stakedOfTime[token][msg.sender];
    uint coin;
    for(uint i=0;i< users[token][msg.sender].mnu;i++){
        if(stakedOfTimeSum[token][msg.sender][i+1] > minit && stakedOf[token][msg.sender][i+1] >0){
            uint banOf=stakedOf[token][msg.sender][i+1] / 100;
            uint send=getTokenPrice(token1,token,banOf) / RATE_DAY;
            coin+=minit*send;
        }
    }
    IERC20(token).transfer(msg.sender,coin*50/100);
}

4. Detailed Root Cause Analysis

At the validated ACT pre-state, block 28187296, the important conditions were already live: StakingRewards.listToken(QiQi) == true, StakingRewards.getpair(QiQi) == SELLC, router.coeAdmin() == StakingRewards, and StakingRewards held 34359509029752649869739669 QiQi. Nothing attacker-specific was required to reach that state.

The attacker first deployed three contracts in public transactions: fake token 0xb5f9683efe43183614fc87d89751bb23c1c8bf72 in 0x4b9d6687a0188ca3898bd6af55578fc037983a9a80f31e744f494547fe5697a8, setup helper 0x9a366027e6be5ae8441c9f54455e1d6c41f12e3c in 0xd1c5990b3a9930178dbb36c264bd294fc6c9f8f0027a366947ca0d853b571617, and flash helper 0xc2f54422c995f6c2935bc52b0f55a03c2f3e429c in 0xda093864178f4ed5813f5b19bd06fa5712ee62aad739dc9bbebc473b8bd73a29.

Next, transaction 0xfe80df5d689137810df01e83b4bb51409f13c865e37b23059ecc6b3d32347136 seeded the reward-bearing positions. The setup helper deployed ten child claimers, funded the flow with 1000 USDT total, and each child called StakingRewards.stake(address,address,address,address,uint256) on QiQi. The balance diff for that transaction shows the attacker EOA losing 1000000000000000000000 USDT, and the trace shows repeated calls into 0xeaF834... with the five-argument stake ABI.

The decisive exploit transaction was 0x8a453c61f0024e8e11860729083088507a02a38100da8b0c3b2d558788662fa0. The flash helper borrowed 10000000000000000000000 QiQi from public Pancake V3 pool 0x4B1aC1E4B828EBC81FcaC587BEf64e4aDd1dBCEc, used the custom router to create pair 0x01B8492c9Ce064714911Fc167f609e0c6CCA1f1c for QiQi and the attacker token, seeded that pair with 10000 QiQi against only 100 raw fake-token units, and then invoked claim through the previously seeded child claimers.

The drain trace shows the exploit sequence directly:

0x4B1aC1E4...::flash(..., 10000000000000000000000, ...)
PancakeRouter::addLiquidity(QiQi, fakeToken, 10000000000000000000000, 100, ...)
0x2c37655f...::createPair(QiQi, fakeToken)
0xA74Bc1f0...::claim(fakeToken)
StakingRewards::claim(QiQi, fakeToken)
emit Transfer(from: StakingRewards, to: 0xA74Bc1f0..., value: 88946759259259258690)

The router side is permissionless, which is why this remains ACT. Its liquidity path creates the pair when none exists:

function _addLiquidity(...) internal virtual returns (uint amountA, uint amountB) {
    if (IPancakeFactory(factory).getPair(tokenA, tokenB) == address(0)) {
        IPancakeFactory(factory).createPair(tokenA, tokenB);
    }
    ...
}

The drain balance diff proves the resulting state change. StakingRewards lost 1614383680555555545202 QiQi, the flash pool gained 100000000000000000000 QiQi as fee, and the attacker-controlled cluster gained 1452120939074074064123 QiQi net. The representative attacker address 0xa3aa817587556c023e78b2285d381c68cee17069 had 1175229543727660022 QiQi before the exploit window and the counted attacker cluster had 1453296168617801724145 QiQi after the drain sequence.

The exploit conditions are therefore fully deterministic:

  • QiQi had to be listed and backed by claimable QiQi inventory in StakingRewards.
  • The attacker had to create at least one valid staking position and wait until block.timestamp > stakedOfTime.
  • The attacker had to create a custom-router pair between QiQi and an attacker-controlled token.
  • The pair reserves had to be selected so getTokenPrice(attackerToken, QiQi, banOf) overvalued the stored stake units.
  • Flash liquidity was optional for exploitability but made the incident capital-efficient; the public Pancake V3 pool supplied it in the seed transaction.

5. Adversary Flow Analysis

The adversary strategy was a five-transaction sequence executed entirely on BSC by unprivileged accounts:

  • 0x4b9d6687a0188ca3898bd6af55578fc037983a9a80f31e744f494547fe5697a8: deployed the fake quote token.
  • 0xd1c5990b3a9930178dbb36c264bd294fc6c9f8f0027a366947ca0d853b571617: deployed the setup helper.
  • 0xfe80df5d689137810df01e83b4bb51409f13c865e37b23059ecc6b3d32347136: created child claimers and seeded QiQi staking positions.
  • 0xda093864178f4ed5813f5b19bd06fa5712ee62aad739dc9bbebc473b8bd73a29: deployed the flash-loan orchestration helper.
  • 0x8a453c61f0024e8e11860729083088507a02a38100da8b0c3b2d558788662fa0: borrowed QiQi, created the fake pair, triggered claims, removed liquidity, and repaid the flash loan.

Key adversary-controlled accounts identified in the evidence are:

  • EOA 0xa3aa817587556c023e78b2285d381c68cee17069, the origin sender and deployer of all attacker-side contracts.
  • Setup helper 0x9a366027e6be5ae8441c9f54455e1d6c41f12e3c.
  • Flash helper 0xc2f54422c995f6c2935bc52b0f55a03c2f3e429c.
  • Fake quote token 0xb5f9683efe43183614fc87d89751bb23c1c8bf72.
  • Child claimers such as 0xa74bc1f0d567beba3c92298f511667b8f1a08fe7 and 0x6db55a9b1c9795ca67a1e924081885fb448489a3.

The trace also shows why attacker-side helper access control does not make the case non-ACT: the helpers are ordinary attacker deployments, not privileged protocol components. Any other unprivileged actor can deploy equivalent helpers, seed positions, create a substitute fake quote token, and replay the same public call sequence against the same vulnerable logic.

6. Impact & Losses

The direct victim was StakingRewards at 0xeaF83465025b4Bf9020fdF9ea5fB6e71dC8a0779, which lost:

  • QiQi: 1614383680555555545202 raw units (1614.383680555555545202 QiQi, decimal = 18)

The attacker cluster retained 1452120939074074064123 QiQi after the drain transaction, while the flash pool recovered its principal plus a 100000000000000000000 QiQi premium. The remaining difference reflects the protocol’s built-in referral distribution, which leaked part of the drain to protocol-side addresses such as 0x2F98Fa813Ced7Aa9Fd6788aB624b2F3F292B9239.

From a security perspective, the impact is broader than the observed loss amount. Any listed staking token whose reward claim path can be repriced with an attacker-selected quote asset is exposed to the same asset-binding failure. The violated principles are: caller-controlled parameters must not override oracle inputs, reward accounting must preserve unit consistency, and spot AMM reserves from attacker-created pairs cannot be treated as trustworthy price sources.

7. References

  • Collected victim contract source: /workspace/session/.tmp/src/StakingRewards.sol
  • Collected custom router source: /workspace/session/.tmp/src/PancakeRouter.sol
  • Supporting state queries and derived ACT values: /workspace/session/artifacts/auditor/iter_0/supporting_queries.json
  • Updated auditor validation fix summary: /workspace/session/artifacts/auditor/iter_1/current_analysis_result.json
  • Setup trace: /workspace/session/artifacts/collector/seed/56/0xfe80df5d689137810df01e83b4bb51409f13c865e37b23059ecc6b3d32347136/trace.cast.log
  • Setup balance diff: /workspace/session/artifacts/collector/seed/56/0xfe80df5d689137810df01e83b4bb51409f13c865e37b23059ecc6b3d32347136/balance_diff.json
  • Drain trace: /workspace/session/artifacts/collector/seed/56/0x8a453c61f0024e8e11860729083088507a02a38100da8b0c3b2d558788662fa0/trace.cast.log
  • Drain balance diff: /workspace/session/artifacts/collector/seed/56/0x8a453c61f0024e8e11860729083088507a02a38100da8b0c3b2d558788662fa0/balance_diff.json
  • Relevant adversary-crafted transactions: 0x4b9d6687a0188ca3898bd6af55578fc037983a9a80f31e744f494547fe5697a8, 0xd1c5990b3a9930178dbb36c264bd294fc6c9f8f0027a366947ca0d853b571617, 0xda093864178f4ed5813f5b19bd06fa5712ee62aad739dc9bbebc473b8bd73a29
  • Relevant exploit transactions: 0xfe80df5d689137810df01e83b4bb51409f13c865e37b23059ecc6b3d32347136, 0x8a453c61f0024e8e11860729083088507a02a38100da8b0c3b2d558788662fa0