All incidents

OKC Flash-LP Reward Drain

Share
Nov 13, 2023 18:51 UTCAttackLoss: 81,304.04 OKCPending manual check1 exploit txWindow: Atomic
Estimated Impact
81,304.04 OKC
Label
Attack
Exploit Tx
1
Addresses
2
Attack Window
Atomic
Nov 13, 2023 18:51 UTC → Nov 13, 2023 18:51 UTC

Exploit Transactions

TX 1BSC
0xd85c603f71bb84437bc69b21d785f982f7630355573566fa365dbee4cd236f08
Nov 13, 2023 18:51 UTCExplorer

Victim Addresses

0x36016c4f0e0177861e6377f73c380c70138e13eeBSC
0xabba891c633fb27f8aa656ea6244dedb15153fe0BSC

Loss Breakdown

81,304.04OKC

Similar Incidents

Root Cause Analysis

OKC Flash-LP Reward Drain

1. Incident Overview TL;DR

At BSC block 33464599, transaction 0xd85c603f71bb84437bc69b21d785f982f7630355573566fa365dbee4cd236f08 let an unprivileged attacker drain OKC rewards from MinerPool in a single transaction. The attacker used public DODO and Pancake flash liquidity to buy OKC, mint a dominant temporary OKC/USDT LP position, insert a constructor-time helper into the reward-holder set, trigger MinerPool::processLPReward(), unwind the position, repay lenders, and finish with 6264547501012742466858 raw USDT units of net profit after fee valuation.

The root cause is a composition failure inside OKC’s own reward logic. MinerPool::processLPReward() pays against instantaneous LP balances rather than durable stake, while LPRewardProcessor::addHolder() uses extcodesize as an EOA check and therefore accepts a contract during construction. That combination makes the reward pool flash-loanable and turns a one-transaction temporary LP majority into an ACT reward-pool drain.

2. Key Background

OKC maintains a separate MinerPool at 0x36016c4f0e0177861e6377f73c380c70138e13ee that holds OKC inventory and periodically distributes rewards to addresses tracked by LPRewardProcessor. The tracked pair is the OKC/USDT Pancake pair at 0x9cc7283d8f8b92654e6097aca2acb9655fd5ed96.

The reward system does not measure stake duration, deposit age, or snapshots. It only keeps a holder list and, when processing runs, reads each holder’s current LP balance. That makes eligibility and payout size depend on momentary state rather than durable participation.

OKC also exposes a public referrer-binding path. A plain transfer of exactly bindAmount (1e16 raw OKC) records a referrer, and later add-liquidity transfers call processInviterReward(sender, amount), which can pull an extra payout from MinerPool::withdrawTo(). The attacker used this public path to extract a small additional OKC payout through a separate helper.

3. Vulnerability Analysis & Root Cause Summary

The vulnerability class is an application-layer attack against reward accounting, not a low-level EVM bug. OKC::_transfer() treats add-liquidity transfers specially: it inserts the sender into the LP reward holder set and triggers inviter rewards. LPRewardProcessor::addHolder() attempts to exclude contracts by checking extcodesize, but contracts under construction still report zero code size, so the check is bypassable. MinerPool::processLPReward() then loops over the entire holder set and transfers 1% of the pool’s proportional OKC amount based on each holder’s current LP balance, with no time weighting, epoch boundary, or lockup.

That means an attacker can borrow capital, acquire most LP supply for a single transaction, register a fresh helper during construction, and immediately claim the pool’s long-accumulated inventory. The attack remains permissionless because all liquidity sources, reward triggers, and helper deployments are public. The incident trace and balance deltas confirm that this exact path executed on-chain and ended with both reward-pool depletion and attacker profit.

Verified OKC source, simplified to the vulnerable branches:

if (recipient == uniswapV2Pair && _isAddLiquidity(amount)) {
    lpRewardProcessor.addHolder(sender);
    processInviterReward(sender, amount);
    super._transfer(sender, recipient, amount);
    return;
}

function processLPReward() public {
    uint256 pairTotalSupply = ISwapPair(pair).totalSupply();
    uint256 pairTokenBalance = IERC20(ISwapPair(pair).token1()).balanceOf(address(this));

    for (uint256 i = 0; i < lpHolderCount; i++) {
        address _addr = lpRewardProcessor.holders(i);
        uint256 _lpBal = IERC20(pair).balanceOf(_addr);
        uint256 amount = pairTokenBalance * _lpBal / pairTotalSupply;
        token.transfer(_addr, amount * 1 / 100);
    }
}

function addHolder(address adr) external onlyAdmin {
    uint256 size;
    assembly { size := extcodesize(adr) }
    if (size > 0) return;
    holders.push(adr);
}

4. Detailed Root Cause Analysis

The attacker first assembled public flash liquidity. The seed trace shows five nested DODO flash loans followed by a Pancake V3 USDT flash of 2500000000000000000000000 raw units. That provided enough USDT to buy OKC and temporarily dominate the OKC/USDT LP supply.

Next, the attacker bought OKC, bound a fresh helper as its referrer via the public bindAmount transfer rule, and added large OKC/USDT liquidity. Because OKC’s add-liquidity branch calls both lpRewardProcessor.addHolder(sender) and processInviterReward(sender, amount), the same path both prepared the reward drain and extracted an extra referral payout from MinerPool.

The critical breakpoint came when the attacker pre-funded a future helper address with dust USDT and dust OKC, then deployed it. During the constructor, the helper transferred 1 raw OKC into the pair. At that moment extcodesize(helper) was still zero, so LPRewardProcessor::addHolder() accepted the helper as if it were an EOA and inserted it into the holder set. The trace then shows MinerPool::processLPReward() transferring 77890958849117701118009 raw OKC units to that helper while it temporarily held 225705840317082411194413 LP tokens.

Incident trace excerpt showing constructor-time holder registration, referral payout, and reward drain:

0x95b82e466655454bcddF2DA15384736799a08D97::addHolder(0x28e7c8337373C81bAF0A4FE88ee6E33d3C23E974)
0x36016C4F0E0177861E6377f73C380c70138E13EE::withdrawTo(0x617432Fc98c1fFaAB62B8cB94Cef6D75ABD95598, 27264968780804476044587)
OKC::transfer(0x28e7c8337373C81bAF0A4FE88ee6E33d3C23E974, 77890958849117701118009)
emit Transfer(from: 0x36016C4F0E0177861E6377f73C380c70138E13EE, to: 0x28e7c8337373C81bAF0A4FE88ee6E33d3C23E974, value: 77890958849117701118009)

After the reward transfer, the attacker swept both helpers, burned the temporary LP, sold the accumulated OKC back into USDT, repaid all flash lenders, and forwarded the remaining USDT to EOA 0xbbcc139933d1580e7c40442e09263e90e6f1d66d. The collected balance diff gives the final profit accounting deterministically: the attacker EOA moved from 25447501696775159872569 raw USDT units to 31715603565614503579854, a gross delta of 6268101868839343707285; fee valuation subtracts 3554367826601240427 raw USDT units, leaving 6264547501012742466858 net.

5. Adversary Flow Analysis

The adversary flow is fully contained in one BSC transaction:

  1. Flash funding: the attacker execution contract 0xd5d8c2fd8a743a89bc497b2f180c52d719a007b9 borrows USDT from five DODO pools and one Pancake V3 pool.
  2. Temporary LP construction: the contract buys OKC, binds helper 0x617432fc98c1ffaab62b8cb94cef6d75abd95598 as referrer, and adds a large OKC/USDT LP position.
  3. Constructor-time holder registration: LP is moved to future helper 0x28e7c8337373c81baf0a4fe88ee6e33d3c23e974, that address is dust-funded, and its constructor-time OKC transfer causes LPRewardProcessor to register it as a holder.
  4. Reward trigger: MinerPool::processLPReward() is called publicly and transfers most of the pool’s distributable OKC to the helper that now owns the dominant LP position.
  5. Unwind and realization: both helpers sweep assets back, the LP is burned, OKC is sold for USDT, all flash lenders are repaid, and the profit is transferred to the attacker EOA.

Closing trace excerpt showing profit transfer:

BEP20USDT::balanceOf(0xD5d8c2fd8A743A89BC497B2F180C52d719a007B9) -> 6268101868839343707285
BEP20USDT::transfer(0xbbcc139933D1580e7c40442E09263e90E6F1D66D, 6268101868839343707285)
emit Transfer(from: 0xD5d8c2fd8A743A89BC497B2F180C52d719a007B9, to: 0xbbcc139933D1580e7c40442E09263e90E6F1D66D, value: 6268101868839343707285)

6. Impact & Losses

The direct victim-side loss was borne by MinerPool, which dropped from 8365188345705299960198987 raw OKC units to 8283884305543082741380459, for a net loss of 81304040162217218818528 raw OKC units. Most of that depletion was captured by the attacker-controlled constructor helper; the rest was incidentally distributed to pre-existing holders because the payout loop iterates across the full holder set.

From the attacker’s perspective, the exploit ended in net positive USDT after repaying every lender inside the same transaction. The net profit, after fee valuation, was 6264547501012742466858 raw USDT units.

7. References

  • Incident transaction: 0xd85c603f71bb84437bc69b21d785f982f7630355573566fa365dbee4cd236f08
  • Verified OKC source: token 0xabba891c633fb27f8aa656ea6244dedb15153fe0
  • MinerPool victim: 0x36016c4f0e0177861e6377f73c380c70138e13ee
  • OKC/USDT Pancake pair: 0x9cc7283d8f8b92654e6097aca2acb9655fd5ed96
  • Seed artifacts used for validation: tx metadata, opcode trace, ERC20 balance diff, and fee valuation under the collected session artifacts