All incidents

OceanLife Reflection Drain on PancakeSwap

Share
Apr 19, 2023 01:04 UTCAttackLoss: 32.29 WBNBPending manual check1 exploit txWindow: Atomic
Estimated Impact
32.29 WBNB
Label
Attack
Exploit Tx
1
Addresses
2
Attack Window
Atomic
Apr 19, 2023 01:04 UTC → Apr 19, 2023 01:04 UTC

Exploit Transactions

TX 1BSC
0xa21692ffb561767a74a4cbd1b78ad48151d710efab723b1efa5f1e0147caab0a
Apr 19, 2023 01:04 UTCExplorer

Victim Addresses

0xb5a0ce3acd6ec557d39afdcbc93b07a1e1a9e3faBSC
0x915c2dfc34e773dc3415fe7045bb1540f8bdae84BSC

Loss Breakdown

32.29WBNB

Similar Incidents

Root Cause Analysis

OceanLife Reflection Drain on PancakeSwap

1. Incident Overview TL;DR

On BSC block 27470679, transaction 0xa21692ffb561767a74a4cbd1b78ad48151d710efab723b1efa5f1e0147caab0a used a public DODO flash loan of 969 WBNB to drain the OLIFE/WBNB Pancake pair at 0x915c2dfc34e773dc3415fe7045bb1540f8bdae84. The attacker EOA 0xfb8ef8de849079559801bff8848178640cdd41b7 called helper contract 0xa9de288d61a7ed99cdd1109b051ef402d85a6b91, bought almost all circulating OceanLife, repeatedly self-transferred OLIFE, called deliver(66859267695870000), and then swapped against a phantom OLIFE input to extract 1001286315327663894139 WBNB from the pair.

The root cause is a reflection-accounting bug in OceanLife, not a flash-loan bug in DODO or a bug in PancakeSwap. OceanLife keeps the AMM pair as a non-excluded reflected holder, so balanceOf(pair) depends on the global reflection rate. Fee-bearing self-transfers and deliver() reduce _rTotal, and OceanLife also double-counts the charity fee by both crediting the excluded charity wallet and subtracting the same reflected amount from _rTotal. That lets the pair’s visible OLIFE balance rise without any real transfer into the pair, and PancakeSwap then treats that phantom balance increase as fresh swap input.

2. Key Background

OceanLife (0xb5a0ce3acd6ec557d39afdcbc93b07a1e1a9e3fa) is an RFI-style reflection token. Non-excluded accounts store reflected balances in _rOwned, and balanceOf(account) converts those reflected units back into token units through the current reflection rate. Excluded accounts instead read _tOwned directly.

At exploit pre-state, the OLIFE/WBNB Pancake pair was still reflection-eligible, while the charity wallet 0xdc57ccd9e2a1d89e1e6a9dd8fdc8d54636b286e6 was excluded and configured as the charity recipient. The owner was already address(0), so the unsafe configuration could no longer be repaired on-chain before the exploit. The validator RPC checks captured the critical pre-state values:

  • Pair excluded from reflections: false
  • OceanLife owner: 0x0000000000000000000000000000000000000000
  • Pair pre-state OLIFE balance: 161703370635833872
  • Pair pre-state WBNB balance: 32286315327689621042
  • OceanLife pre-state total supply: 245568139475261284

These conditions matter because a reflected AMM pair is incompatible with reserve accounting. PancakeSwap V2 expects the pair’s token balances to move only when tokens are actually transferred in or out. OceanLife violates that expectation by letting global reflection-rate changes mutate balanceOf(pair) with no transfer event.

3. Vulnerability Analysis & Root Cause Summary

This incident is an ATTACK-class ACT exploit. The vulnerable design is OceanLife’s decision to leave the Pancake pair as a non-excluded reflected holder while also exposing deliver() and fee-bearing transfers that directly change the global reflection rate. For non-excluded accounts, balanceOf() is computed from _rOwned[account] / currentRate, so reducing currentRate makes the same reflected balance appear as more OLIFE. The attacker first bought enough OLIFE to become the dominant reflected holder, then used repeated self-transfers to trigger tax, burn, and charity fee logic many times. Those transfers were unusually powerful because _sendToCharity() credited the excluded charity wallet with rCharity, and _reflectFee() then subtracted that same rCharity from _rTotal again. After the attacker finally called deliver(), the pair’s visible OLIFE balance jumped far above total supply even though the attacker never sent OLIFE to the pair for the drain leg. PancakePair then released almost all of its WBNB because it interpreted the phantom OLIFE balance increase as valid swap input.

4. Detailed Root Cause Analysis

4.1 Code-Level Breakpoint

The exploitable behavior is visible directly in OceanLife’s source:

function balanceOf(address account) public view override returns (uint256) {
    if (_isExcluded[account]) return _tOwned[account];
    return tokenFromReflection(_rOwned[account]);
}

function deliver(uint256 tAmount) public {
    address sender = _msgSender();
    require(!_isExcluded[sender], "Excluded addresses cannot call this function");
    (uint256 rAmount,,,,,,) = _getValues(tAmount);
    _rOwned[sender] = _rOwned[sender].sub(rAmount);
    _rTotal = _rTotal.sub(rAmount);
    _tFeeTotal = _tFeeTotal.add(tAmount);
}

function _reflectFee(uint256 rFee, uint256 rBurn, uint256 rCharity, uint256 tFee, uint256 tBurn, uint256 tCharity) private {
    _rTotal = _rTotal.sub(rFee).sub(rBurn).sub(rCharity);
    _tFeeTotal = _tFeeTotal.add(tFee);
    _tBurnTotal = _tBurnTotal.add(tBurn);
    _tCharityTotal = _tCharityTotal.add(tCharity);
    _tTotal = _tTotal.sub(tBurn);
}

function _sendToCharity(uint256 tCharity, address sender) private {
    uint256 currentRate = _getRate();
    uint256 rCharity = tCharity.mul(currentRate);
    address currentCharity = _charity[0];
    _rOwned[currentCharity] = _rOwned[currentCharity].add(rCharity);
    _tOwned[currentCharity] = _tOwned[currentCharity].add(tCharity);
    emit Transfer(sender, currentCharity, tCharity);
}

The invariant that should hold is simple: the AMM pair’s token balance should change only when a real transfer changes the pair’s holdings. OceanLife breaks that invariant because balanceOf(pair) is derived from reflection state, and both fee logic and deliver() mutate the global rate without requiring a transfer into the pair.

4.2 Exploit Pre-State

The public, reproducible pre-state is BSC mainnet at the start of the exploit transaction. The collector’s state checks show that:

  • the pair was still reflection-eligible;
  • the charity wallet was excluded;
  • ownership had already been renounced;
  • the pair still held meaningful OLIFE and WBNB liquidity.

That made the opportunity permissionless. Any unprivileged actor could deploy a helper contract, borrow WBNB from the public DODO pool 0xFeAFe253802b77456B4627F8c2306a9CeBb5d681, and compose the same public token and AMM calls.

4.3 Execution Path

The trace shows the full exploit path:

0xFeAFe253802b77456B4627F8c2306a9CeBb5d681::flashLoan(969000000000000000000, ...)
PancakeRouter::swapExactTokensForTokensSupportingFeeOnTransferTokens(969000000000000000000, ...)
OceanLife::transfer(0xA9DE288d61a7ED99CDD1109B051EF402d85a6b91, 148760274602488242)
...
OceanLife::deliver(66859267695870000)
OceanLife::balanceOf(PancakePair) -> 217839506118721725361721643770
PancakePair::swap(0, 1001286315327663894139, 0xA9DE288d61a7ED99CDD1109B051EF402d85a6b91, 0x)
WBNB::transfer(0xFeAFe253802b77456B4627F8c2306a9CeBb5d681, 969000000000000000000)
WBNB::transfer(0xfB8EF8dE849079559801BFF8848178640CDd41B7, 32286315327663894139)

The crucial intermediate values are also explicit in the trace:

  • After the buy leg, PancakePair::getReserves() and OceanLife::balanceOf(pair) showed the pair held only 5583143203784247 OLIFE against 1001286315327689621042 WBNB.
  • The attacker then performed repeated self-transfers. Each one emitted a charity transfer to 0xdc57... and a self-transfer back to the helper, proving the exploit used OceanLife’s public fee machinery rather than privileged hooks.
  • After deliver(66859267695870000), OceanLife::balanceOf(pair) returned 217839506118721725361721643770.
  • Immediately after, PancakePair emitted Swap(... amount0In: 217839506118716142218517859523, amount1Out: 1001286315327663894139 ...), confirming that the pair interpreted the phantom OLIFE balance increase as real input.

4.4 Why the Drain Works

The attacker never needed to transfer OLIFE to the pair during the drain leg. Once the pair’s reflected balance had been inflated, PancakeSwap’s standard V2 accounting did the rest: it compared observed token balances against stored reserves and inferred that OLIFE had been sent in. Because the observed balance exceeded even OceanLife’s post-transaction totalSupply() of 203999715656702127, the pair effectively priced against impossible inventory and released almost all of its WBNB.

The non-monetary exploit oracle is therefore an AMM reserve-accounting violation: balanceOf(pair) exceeded totalSupply() without a real transfer into the pair. The monetary oracle is the attacker’s realized WBNB profit after repaying the flash loan.

5. Adversary Flow Analysis

The adversary flow is deterministic and fits in a single transaction:

  1. EOA 0xfb8ef8de849079559801bff8848178640cdd41b7 called helper contract 0xa9de288d61a7ed99cdd1109b051ef402d85a6b91.
  2. The helper borrowed 969 WBNB from the public DODO pool and approved PancakeRouter and OceanLife.
  3. The helper swapped the borrowed WBNB for OLIFE on the OLIFE/WBNB pair, becoming the dominant reflected holder.
  4. The helper repeatedly called OceanLife::transfer(helper, currentBalance) to itself. Those calls were fee-bearing because neither sender nor recipient was excluded, so every transfer reduced _rTotal, burned supply, and credited the charity wallet.
  5. The helper called OceanLife::deliver(66859267695870000), pushing the reflection rate down further.
  6. The helper queried pair state, saw the inflated OLIFE balance, and called PancakePair::swap(0, 1001286315327663894139, helper, 0x) to extract WBNB.
  7. The helper repaid the flash-loan principal and transferred the remaining 32286315327663894139 WBNB to the attacker EOA.

No privileged key, allowlist, or private orderflow was required. The exploitable condition was fully public and reproducible from archive state, the verified OceanLife source, and the transaction trace.

6. Impact & Losses

The OLIFE/WBNB Pancake pair was effectively drained. Its WBNB balance fell from 32286315327689621042 wei before the exploit to 25726903 wei after it. The attacker EOA finished with 32286315327663894139 wei of WBNB, and the helper contract ended with zero WBNB after forwarding profit.

Measured in the reference asset:

  • Gross attacker WBNB after the transaction: 32286315327663894139
  • Native BNB gas paid by the attacker EOA: 33801045000000000
  • Net BNB-equivalent gain: 32252514282663894139

The pair also ended in an impossible accounting state, with visible OLIFE balance 217839506118721725361721643770 and token totalSupply() only 203999715656702127. That state confirms the exploit was not a normal swap sequence but a reserve-accounting failure caused by OceanLife’s reflection logic.

7. References

  • Exploit transaction: 0xa21692ffb561767a74a4cbd1b78ad48151d710efab723b1efa5f1e0147caab0a
  • Attacker EOA: 0xfb8ef8de849079559801bff8848178640cdd41b7
  • Attacker helper: 0xa9de288d61a7ed99cdd1109b051ef402d85a6b91
  • Victim token: OceanLife 0xb5a0ce3acd6ec557d39afdcbc93b07a1e1a9e3fa
  • Victim pair: PancakePair OLIFE/WBNB 0x915c2dfc34e773dc3415fe7045bb1540f8bdae84
  • Flash-loan pool: DODO 0xFeAFe253802b77456B4627F8c2306a9CeBb5d681
  • Verified OceanLife source used for code review: artifacts/collector/seed/56/0xb5a0ce3acd6ec557d39afdcbc93b07a1e1a9e3fa/src/Contract.sol
  • Exploit trace used for call-level validation: artifacts/collector/seed/56/0xa21692ffb561767a74a4cbd1b78ad48151d710efab723b1efa5f1e0147caab0a/trace.cast.log
  • Pre/post state checks used for deterministic validation: artifacts/auditor/iter_0/rpc_state_checks.json
  • Balance-delta evidence used for profit confirmation: artifacts/collector/seed/56/0xa21692ffb561767a74a4cbd1b78ad48151d710efab723b1efa5f1e0147caab0a/balance_diff.json