October 20, 2026
|
Exploit Postmortem
Cover image for "ORBCore's $32.6K Loss and How Olympix Would Have Prevented It"

ORBCore's $32.6K Loss and How Olympix Would Have Prevented It

Date: September 11, 2026

Chain: BNB Chain

Loss: $32,610.72, about 45 BNB

Class: reentrancy into an unguarded sell function through a token's receive() callback

The PancakeSwap pool behind ORB held about 45.3 WBNB when the attacker arrived. To take it, the attacker borrowed roughly half a million BNB worth of working capital, bought almost every ORB token out of the pool, and drove a single call that re-entered ORBCore 58 levels deep before it unwound. When the stack finally collapsed and every loan was repaid, the attacker kept about 45.3 BNB, the pool's original liquidity, worth $32,610.72 by SlowMist's count.

Two contracts made that loop possible. ORBToken automatically called into ORBCore whenever it received BNB, and ORBCore's addPoolAndSell() function had no guard to stop it from being entered again while it was still running.

Two Contracts Built to Work Together

ORBToken was a deflationary token, so an ordinary sell through its PancakeSwap pair burned 5% of the amount sold. ORBCore was the protocol's core contract, managing pool interactions and selling tokens on the system's behalf, and it was whitelisted on ORBToken so its sales skipped that 5% tax.

ORBToken also had a receive() function, the code that runs when a contract is sent BNB. According to SlowMist's alert as relayed by Rekt Radar, that receive() granted a maximum allowance and called into ORBCore's addPoolAndSell(). Together, the whitelist and the callback meant that anyone who could send BNB to the token could trigger a tax-free sell through the core.

Calling Back Before the Work Is Done

Reentrancy happens when a function hands control to outside code before it has finished its own bookkeeping, and that outside code calls the same function again. A bank teller who hands over cash and then turns away to update the ledger, while the same customer steps back to the window and asks again, is facing the same problem, because each request is judged against a ledger that hasn't caught up.

In ORBCore, addPoolAndSell() refunded BNB to the calling account before its internal _sellToken() step completed, per the DeFiHackLabs reproduction. The attacker's own receive() function caught that refund and sent it straight back to ORBToken, which called addPoolAndSell() again. Nothing in addPoolAndSell() checked whether it was already running, so every refund opened a new level of the same call.

Every level also did damage to the pool. Each _sellToken() sold ORB tax-free and then called burnLP(), which destroyed ORB directly from the pair's own balance, up to 20% of the pair's ORB per call, and then called sync() to write the smaller balance into the pair's recorded reserves. A 20% cap sounds conservative, but compounded across dozens of nested calls it empties the ORB side of a pool.

Half a Million BNB for a 45 BNB Pool

The pair held about 45.3 WBNB and 5.826 million ORB, and the attacker owned only 3,483 ORB. With burnLP() capped at 20% of the pair's ORB per call and each sell limited to what the attacker held, that stack could never grind down a reserve of 5.8 million ORB. The attacker needed to buy almost the whole ORB side of the pool before the burns ran, and that took borrowed money.

The outer layer was a Moolah flash loan of 241,591 WBNB, Moolah's entire liquidity, with a nested Moolah flash loan of 3,145 BTCB deposited into Venus as collateral to borrow another 260,000 BNB. That added up to roughly 501,000 BNB of borrowed capital, all of which had to be repaid by the end of the transaction. The attacker used it to buy about 5.827 million ORB out of the pool, routed through ORBCore because ORBToken reverted any buy that didn't go to the core. That buy left the pair holding more than 440,000 WBNB and only about 590 ORB, so the reentrant loop now only had to burn through about 590 ORB instead of 5.8 million.

Fifty-Eight Levels Down

The attacker's contract sent BNB to ORBToken, and the cycle began. ORBToken's receive() called addPoolAndSell(), addPoolAndSell() refunded BNB before finishing, the attack contract's receive() bounced it back, and the whole sequence repeated. The DeFiHackLabs author confirmed the recursion ran 58 levels deep in the real trace. The ordering is what made it pay: each level re-entered at its refund, before its own burnLP() had run, so the sells executed on the way down and the burns executed on the way back up. In the DeFiHackLabs reconstruction the big buy fires at the deepest level, which means every burn on the way up hit a pool whose ORB side had already been bought out, each one taking up to 20% of whatever ORB was left and re-syncing the reserves.

As the calls unwound, the attacker made a final sell of its remaining ORB into a pool whose ORB side had been all but erased, and the pool paid out nearly its entire WBNB reserve. The attacker repaid Venus and both Moolah loans in full and was left with about 45.302 BNB in the DeFiHackLabs reproduction, matching the 44.9 to 45 BNB reported on-chain. All of it ran in transaction 0x5e6b...5b34 from the attack contract 0x4f33...6831.

No statement from an ORB official account appeared in the reporting window, and no return of funds has been identified.

How Olympix Catches a Callback That Comes Back In

Olympix reads a contract system the way an attacker does, as states, transitions and entry points, and it reads ORBToken and ORBCore together because the bug only exists where they meet. The states that matter here are the pair's reserves and the core's in-progress sell, and every path that can reach addPoolAndSell(), including the token's receive() callback, is worth mapping. The engine takes in the design context, a whitelisted core and an auto-calling token, to infer the invariants the system depends on but never writes down.

Olympix's static analysis surfaces the signal: a state-changing function that sends value to an outside address before its own work is complete, with no reentrancy guard in scope, reachable from a callback in a second contract. Olympix has flagged this class before in other codebases, a callback re-entering an unguarded state-changing path, and the signal narrows the search from every path across both contracts to the ones that can re-enter addPoolAndSell() while it's still running.

BugPoCer looks at the two contracts as one system and asks what has to hold for ORBCore's sells to be safe. Running Counterexample-Guided Invariant Refutation, it works outward from that system and infers the rule in plain words: each call to addPoolAndSell() finishes its sell before any other can start, so the pool's reserves only move through completed sells. It then executes paths through receive(), addPoolAndSell() and _sellToken() until one breaks that rule.

The counterexample BugPoCer returns is the September 11 loop: send BNB to ORBToken, let receive() call addPoolAndSell(), catch the early refund, send it back, and repeat while burnLP() and sync() drain the ORB side of the pool. It comes with the minimal conditions that trigger it and a generated Forge test that replays it, so the 58-level loop shows up in development instead of on mainnet. Because the engine runs automatically during development, at every merge and before release, a convenience callback added to the token gets tested against the core it calls into.

Where Two Reasonable Contracts Meet

ORBToken's callback and ORBCore's whitelist each made sense on their own, and the loss came from where they met, an unguarded function that two contracts could reach in a loop. Any time one contract calls into another mid-execution, the function on the receiving end has to assume it can be entered again before it finishes. To see how Olympix maps the paths between your contracts and hands back a runnable counterexample for each one that re-enters where it shouldn't, book a technical walkthrough.

On-Chain References

Protocol: ORBCore and ORBToken

Chain: BNB Chain

Date: September 11, 2026

Loss: $32,610.72 (about 45.3 BNB)

Attacker EOA: 0xd8b4...dad3

Attack contract: 0x4f33...6831

ORBCore (vulnerable contract): 0x24b6...3c18

ORBToken: 0xc4d2...e7d7

ORB/WBNB PancakeSwap pair: 0x64fa...788c

Exploit tx: 0x5e6b...5b34

Block: 121296110

Flash loan source: Moolah 0x8f73...5D8C

Sources

SlowMist: TI Alert on the ORB exploit

DeFiHackLabs PR #1258: ORB exploit PoC

DeFiHackLabs: ORB_exp.sol

Rekt Radar: Web3 Daily Exploits, September 12, 2026

More from Olympix:

No items found.

Ready to Shift Security Assurance In-House? Talk to Our Security Experts Today.