Calculated from recorded token losses using historical USD prices at the incident time.
0xa21692ffb561767a74a4cbd1b78ad48151d710efab723b1efa5f1e0147caab0a0xb5a0ce3acd6ec557d39afdcbc93b07a1e1a9e3faBSC0x915c2dfc34e773dc3415fe7045bb1540f8bdae84BSCOn 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.
OceanLife (0xb5a0ce3acd6ec557d39afdcbc93b07a1e1a9e3fa) is an RFI-style reflection token. Non-excluded accounts store reflected balances in , and converts those reflected units back into token units through the current reflection rate. Excluded accounts instead read directly.
_rOwnedbalanceOf(account)_tOwnedAt 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:
false0x000000000000000000000000000000000000000016170337063583387232286315327689621042245568139475261284These 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.
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.
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.
The public, reproducible pre-state is BSC mainnet at the start of the exploit transaction. The collector’s state checks show that:
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.
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:
PancakePair::getReserves() and OceanLife::balanceOf(pair) showed the pair held only 5583143203784247 OLIFE against 1001286315327689621042 WBNB.0xdc57... and a self-transfer back to the helper, proving the exploit used OceanLife’s public fee machinery rather than privileged hooks.deliver(66859267695870000), OceanLife::balanceOf(pair) returned 217839506118721725361721643770.Swap(... amount0In: 217839506118716142218517859523, amount1Out: 1001286315327663894139 ...), confirming that the pair interpreted the phantom OLIFE balance increase as real input.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.
The adversary flow is deterministic and fits in a single transaction:
0xfb8ef8de849079559801bff8848178640cdd41b7 called helper contract 0xa9de288d61a7ed99cdd1109b051ef402d85a6b91.969 WBNB from the public DODO pool and approved PancakeRouter and OceanLife.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.OceanLife::deliver(66859267695870000), pushing the reflection rate down further.PancakePair::swap(0, 1001286315327663894139, helper, 0x) to extract WBNB.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.
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:
322863153276638941393380104500000000032252514282663894139The 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.
0xa21692ffb561767a74a4cbd1b78ad48151d710efab723b1efa5f1e0147caab0a0xfb8ef8de849079559801bff8848178640cdd41b70xa9de288d61a7ed99cdd1109b051ef402d85a6b910xb5a0ce3acd6ec557d39afdcbc93b07a1e1a9e3fa0x915c2dfc34e773dc3415fe7045bb1540f8bdae840xFeAFe253802b77456B4627F8c2306a9CeBb5d681artifacts/collector/seed/56/0xb5a0ce3acd6ec557d39afdcbc93b07a1e1a9e3fa/src/Contract.solartifacts/collector/seed/56/0xa21692ffb561767a74a4cbd1b78ad48151d710efab723b1efa5f1e0147caab0a/trace.cast.logartifacts/auditor/iter_0/rpc_state_checks.jsonartifacts/collector/seed/56/0xa21692ffb561767a74a4cbd1b78ad48151d710efab723b1efa5f1e0147caab0a/balance_diff.json