OceanLife Reflection Drain on PancakeSwap
Exploit Transactions
0xa21692ffb561767a74a4cbd1b78ad48151d710efab723b1efa5f1e0147caab0aVictim Addresses
0xb5a0ce3acd6ec557d39afdcbc93b07a1e1a9e3faBSC0x915c2dfc34e773dc3415fe7045bb1540f8bdae84BSCLoss Breakdown
Similar Incidents
FIREDRAKE Reflection Drain on PancakeSwap
56%BUNN Reflection Drain via PancakePair
46%BEVO reflection accounting lets anyone manufacture PancakeSwap input and drain WBNB
45%BIGFI Burn Bug Drains PancakeSwap
42%Sheep Burn Reserve Drain
37%StarlinkCoin Pair Drain
37%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()andOceanLife::balanceOf(pair)showed the pair held only5583143203784247OLIFE against1001286315327689621042WBNB. - 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)returned217839506118721725361721643770. - 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:
- EOA
0xfb8ef8de849079559801bff8848178640cdd41b7called helper contract0xa9de288d61a7ed99cdd1109b051ef402d85a6b91. - The helper borrowed
969WBNB from the public DODO pool and approved PancakeRouter and OceanLife. - The helper swapped the borrowed WBNB for OLIFE on the OLIFE/WBNB pair, becoming the dominant reflected holder.
- 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. - The helper called
OceanLife::deliver(66859267695870000), pushing the reflection rate down further. - The helper queried pair state, saw the inflated OLIFE balance, and called
PancakePair::swap(0, 1001286315327663894139, helper, 0x)to extract WBNB. - The helper repaid the flash-loan principal and transferred the remaining
32286315327663894139WBNB 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