Exploit post-mortems
/
OSK CoreVault
BNB Chain
Price manipulation

OSK CoreVault's $176K Loss and How Olympix Would Have Prevented It

Loss
Loss
Exploit date
November 4, 2026

OSK CoreVault kept an InsurancePool for the worst day, a reserve meant to cover redemptions when the vault itself came up short. At 21:50:20 UTC on November 3, 2026, that reserve paid out 260.22 BNB to a redemption that should have been worth a sliver of it. One transaction on BNB Smart Chain, in block 131507810, deployed an attack contract, borrowed 207,358.41 WBNB, crashed the price the vault trusted, cashed out, and paid everything back before the block closed.

ExVul reconstructed the net loss at about 251.23 BNB, roughly $175,800 at the BNB price when the transaction landed. The attacker walked away with about 152.26 BNB after putting in an 8 BNB position of their own, according to ExVul. The bug sat in one pricing function that let a number the attacker had just set inside the transaction decide how much the InsurancePool owed.

A Redemption Priced by a Pool

CoreVault is a yield product on BNB Smart Chain built around the OSK token. Users open positions in the CoreVaultFarming contract, and when they redeem their principal, the contract works out what they're owed in BNB. To do that it asks OracleLib, the protocol's pricing library, what WBNB is worth, and OracleLib reads the answer from the PancakeSwap WBNB/USDT pool.

The InsurancePool stands behind that settlement. When a redemption needs more than the vault can cover on its own, the pool tops it up. That arrangement ties the oracle's answer directly to the InsurancePool's balance. A WBNB price that's too low means more BNB per dollar of principal, and the InsurancePool is the account that makes up the difference.

Three Samples, One of Them Fresh

OracleLib's getAccountingPrices() kept a small window of three price samples. Each call took the current PancakeSwap spot price, pushed it into the window as the newest sample, and returned the plain average of the three. The window was meant to smooth out noise between readings.

The trouble is the timing. The spot price a contract reads is the pool's state at that exact moment inside the running transaction. A flash loan lets anyone move enormous capital through a pool within that same transaction, so a same-transaction spot read returns whatever price the borrower has just created. According to ExVul, getAccountingPrices() pushed that same-transaction spot into the window and returned the raw average straight away. The manipulated reading was written and read in a single call, and with only three samples, one extreme value was enough to drag the average far from the market.

ExVul also points out that OracleLib already contained a selector that could route pricing through spot, TWAP or median logic, and getAccountingPrices() never called it. A time-weighted average is built from prices observed across many blocks, which puts it out of reach of a single transaction. The protection existed in the library. The settlement path just didn't use it.

One Transaction, Start to Finish

The attacker's wallet deployed its attack contract in the exploit transaction itself, and the contract borrowed 207,358.41 WBNB, about $145 million at the time. The event log matches a Morpho-style lending market, and according to Beepro the loan came from Moolah, the lending protocol under Lista DAO.

Next, the contract sold 195,000 WBNB into the PancakeSwap WBNB/USDT pool and received 13,791,474.30 USDT. That works out to an average of about $70.73 per WBNB on a trade made while the market price sat near $700, and it left the pool's spot price for WBNB at a small fraction of its real value.

With the pool in that state, the contract redeemed through CoreVaultFarming's _redeemPrincipalCore(), which settled through getAccountingPrices(). The crashed spot price went into the three-sample window, the average came back far too low, and the settlement treated each BNB as worth far less than it was. According to CertiK, the manipulated oracle inflated the redemption by roughly 33 times. The InsurancePool's event log in the same transaction records a payout of 260.22 BNB into the redemption flow, the same figure OnchainLens and Beepro report.

The attacker then unwound everything. The contract sent the 13,791,474.30 USDT back into the pool and received 194,901.17 WBNB, about 98.83 WBNB less than it had sold, a cost of the round trip that the redemption proceeds more than covered. It repaid the full 207,358.41 WBNB to the lender in the same transaction.

Counting the Damage

The headline numbers in early coverage vary because they measure different things. CertiK put the attacker's take at 160.26 BNB, about $112,200, after the reverse trade. OnchainLens put the InsurancePool's net loss at about $179,500. ExVul's reconstruction lands at 251.23 BNB of net loss to the system, with about 152.26 BNB kept by the attacker. The attacker's profit and the victim's loss are different measures, and this post uses the victim's side.

ExVul's alert urged the InsurancePool owner to cut off the vulnerable Treasury connection and move the remaining reserves out. As of November 4, 2026, no post-mortem from the OSK CoreVault team had been reported, and no recovery of funds had been announced.

Why a Short Window Wasn't Enough

A three-sample average looks like a defense when you read the code, and in normal trading it behaves like one. Prices drift a little between calls, the average smooths that drift, and redemptions settle at sensible values. The weakness only appears when one of the three samples is taken in the same transaction as a trade large enough to move the pool, and a test suite built around ordinary market conditions never creates that trade.

The library made the gap harder to see. It had a selector capable of TWAP and median pricing, so anyone skimming OracleLib would find the right tools in place. What mattered was the path the redemption actually took, and that path went through the one function that skipped them. Reviews do their job inside the scope they're given, and this bug lived in the space between what a library offers and what its callers use.

How Olympix Would Have Followed the Price Into the Payout

Olympix reads a contract as states, transitions and entry points, and takes in the surrounding context, including comments and library structure, to infer the invariants a system relies on. In CoreVault, the state that matters is the accounting price, and the transition that spends it is the redemption. Olympix maps the paths through the system and uses signals to narrow them, from every path to the feasible ones and then to the feasible paths that touch an invariant.

Static analysis would have supplied the signal here. It would have surfaced a pricing function that writes a live AMM spot price into its sample window and reads the same window back in the same call, inside a library that a redemption settlement path consumes, while a manipulation-resistant selector sits unused nearby. A settlement value derived from a price that can be moved within the calling transaction is exactly the kind of signal that narrows the search to a few paths. It is the same weakness behind Whalebit's $824K loss, where a staking contract priced deposits from a pool's spot price.

The invariant takes one sentence. The price _redeemPrincipalCore() settles against must not be movable by actions in the same transaction as the redemption. BugPoCer, Olympix's tool for Counterexample-Guided Invariant Refutation, works from the system outward, starting from that rule and searching for a call sequence that breaks it, refining each time a candidate fails.

The sequence that breaks it is the November 3 attack. Borrow WBNB, sell it into the WBNB/USDT pool, redeem a position while the pool is skewed, unwind the swap and repay. BugPoCer would have returned that as a counterexample: the exact sequence of state changes, the minimal conditions that trigger it and a generated Forge test that replays it. Had CoreVault run the analysis at every merge and before release, the path would have shown up when the code that creates it landed, long before the InsurancePool held anyone's money.

The Selector Was Already There

CoreVault's fix was sitting in its own library. Routing getAccountingPrices() through the TWAP or median selector, or refusing to settle against a sample taken in the current block, would have left the attacker's swap with nothing to push against. For any team pricing redemptions, liquidations or mints from an AMM, the code worth checking is the path each payout actually takes, along with every helper that path skips. To see how Olympix traces your pricing paths into the payouts that depend on them and hands back a runnable counterexample for every break it finds, book a technical walkthrough.

On-Chain References

Exploit tx (Nov 3, 21:50:20 UTC): 0x072a...98b3

Attacker wallet: 0xd356...5304

Attack contract: 0xc28c...87e2

Victim, InsurancePool: 0xf40e...3df1

Vulnerable CoreVault: 0x302f...d674

Flash loan source: 0x8f73...5d8c

PancakeSwap WBNB/USDT pool: 0x16b9...0dae

See what Olympix finds on your own code

Send us a repo that's already been audited, and we'll run Olympix at the same commit and show you what comes back.