At 21:50:20 UTC on 3 November 2026, a single transaction on BNB Smart Chain drained 259.09 BNB from the reserve behind CoreVault, a BNB vault system tied to the OSK token. The attacker kept 160.26 BNB, which CertiK valued at about $112,200. There was no stolen key and no reentrancy. The attacker borrowed about 207,000 WBNB, dumped most of it into PancakeSwap’s WBNB/USDT pool, and then asked CoreVault to redeem a position while the pool showed BNB at roughly one percent of its real price. CertiK reported that the redemption came out about 33 times larger than it should have.
The interesting part is not that a spot price can be moved with a flash loan. Everyone knows that. The interesting part is that the protocol reportedly did have a multi-source oracle, and the attack went through the path the oracle used when its safer inputs were not ready yet. This post walks through what the chain shows, where the reported root cause sits, and what to test in any vault that pays out of a reserve.
0x072a10a579e2a8afa26a681205a2aaaa38f74bcb9d1064b12bd9e9ebe2c598b3, block 131,507,810.0xd3565b191b28b49cc517189b984dacdb1ff05304.We pulled the receipt and the surrounding state from public BSC nodes. The transaction is a contract creation: the attacker deployed a helper at 0xc28cfeee...f387e2 and ran the whole attack from its constructor, so there was no separate setup transaction to front-run or flag.
The token transfers in the receipt follow a clean sequence:
0x8f73b65b...e5d8c sends 207,358.410468 WBNB to the attack contract 0x92f521a7...f542. Onchain Lens identified this lender as Lista DAO’s Moolah, and its estimate of about $143.6M for the loan matches the amount at roughly $700 per BNB.0x16b9a828...1eb0dae and receives 13,791,474.30 USDT.0xfc1b8abb...159ef5, 0x302f5084...5d674 and several other addresses. These are the vault’s own bookkeeping as the redemption is processed.The native BNB side does not show up as token transfers, so we compared balances one block before and after using an archive node:
| Address | Before | After | Change |
|---|---|---|---|
0xf40e0d52...53df1 (reserve paying out) | 259.1618 BNB | 0.0691 BNB | -259.0928 BNB |
0xd3565b19...05304 (attacker EOA) | 1.9114 BNB | 162.1692 BNB | +160.2578 BNB |
The numbers reconcile. The round trip through PancakeSwap cost the attacker 195,000 minus 194,901.165, about 98.83 WBNB in fees and slippage. Take that from the 259.09 BNB that left the reserve and you get 160.26 BNB, which is what landed in the attacker’s wallet. Onchain Lens reported a gross withdrawal of 260.21 BNB through CoreVault and a net depletion matching the roughly 259 BNB we see, and Telem’s write-up reports the same pair of figures.
Right before the attack block, the PancakeSwap pair held about 15,345,479 USDT and 21,917 WBNB, a spot price of about $700.15 per BNB. After the 195,000 WBNB dump and the USDT that came out, the reserves were about 1.55M USDT and 216,917 WBNB, which puts the spot price near $7.16. That is roughly a 98x drop, held for the length of one transaction.
CertiK reported the redemption was inflated about 33x, not 98x. We do not have the verified CoreVault source to explain the exact ratio, so treat this as our reading rather than a confirmed fact: a figure between 1x and 98x is what you would expect if the manipulated spot price was blended with other price inputs instead of used alone. Blackpaper’s summary says the redemption logic averaged a same-transaction spot price into a short sampling window. Either way, a partial dilution of a 98x move still left a 33x error, which was more than enough to empty the reserve.
The most detailed public description comes from Telem, and since CoreVault has not published a post-mortem, the function names below are as reported there and not something we have verified against source code.
According to that report, CoreVault’s redeemPrincipal pays BNB for a USD-denominated principal. To convert, it uses a value called wbnbPerUsd, how many WBNB one dollar buys. That value comes from an OracleLib helper that takes a median of three sources. When the TWAP or average-price windows are not fully populated, the median falls back to the PancakeSwap spot price. The payout then flows through Treasury.payoutBnb into InsurancePool.withdrawReserve.
Dumping WBNB makes WBNB cheap, so wbnbPerUsd goes up, so a fixed dollar principal is converted into far more BNB. The attacker did not need to fool the oracle forever. It needed one transaction in which the only source that counted was the one it could move.
A simplified sketch of that failure pattern, written for illustration only and not CoreVault’s actual code:
// Simplified illustration of the pattern. Not CoreVault's source.
function wbnbPerUsd() public view returns (uint256) {
uint256 spot = pancakeSpotWbnbPerUsd(); // movable inside one tx
(uint256 twap, bool twapReady) = twapWbnbPerUsd();
(uint256 avg, bool avgReady) = avgWbnbPerUsd();
// The dangerous branch: when safer feeds are not ready,
// the "median" quietly becomes the spot price.
if (!twapReady || !avgReady) {
return spot;
}
return median3(spot, twap, avg);
}
function redeemPrincipal(uint256 usdPrincipal) external {
uint256 bnbAmount = usdPrincipal * wbnbPerUsd() / 1e18;
treasury.payoutBnb(msg.sender, bnbAmount); // pulls from the insurance reserve
}
A safer shape refuses to price at all when the robust inputs are missing:
// Simplified illustration of a fail-closed version.
function wbnbPerUsd() public view returns (uint256 price) {
(uint256 twap, bool twapReady) = twapWbnbPerUsd();
require(twapReady, "oracle: twap not ready");
uint256 spot = pancakeSpotWbnbPerUsd();
// Spot may be used as a sanity check, never as the price.
require(_withinBps(spot, twap, MAX_DEVIATION_BPS), "oracle: spot deviates");
return twap;
}
A median of three sources is a reasonable design. It only works if all three sources are live and independent. The reported fallback breaks both conditions at once: when the windows are empty there is effectively one source, and it is the one an attacker can move with borrowed money.
Fallback paths are easy to miss in review because they look defensive. Someone wrote “if the TWAP is not ready, do not revert, use what we have” so the product would keep working right after deployment or after a quiet period. That is a liveness decision. In a function that sends value out, it turns into a security decision, and the safe default for a payout is to stop, not to guess.
The second design problem is where the money came from. An insurance reserve is supposed to absorb losses. Here it was wired directly into the redemption path, so any pricing error in redemptions was paid from the backstop. That turned a pricing bug into a reserve drain, and there was no per-transaction or per-period limit in the way.
Make every oracle fallback fail closed in value-moving paths. If a TWAP, median or external feed is not ready, revert redemptions, withdrawals and liquidations. A paused vault is an annoyance; a vault that prices from spot is a donation.
Test the cold start. Many TWAP implementations need a warm-up period after deployment, after a pool migration, or after a long gap in observations. Write tests that call every payout function in that state and assert that they revert.
Use spot only as a sanity check. Comparing spot to a TWAP and rejecting large deviations is useful. Letting spot set the price, even inside a median, is not.
Bound reserve outflows. Rate limits, per-transaction caps and caps relative to the reserve’s size would not fix a bad price, but they turn a full drain into a partial one and buy time for monitoring to react.
Model the flash loan budget. Here roughly $143.6M of WBNB was borrowed against a pool holding about $30.7M of total liquidity. Your threat model should assume an attacker can borrow several times the depth of any pool your contracts read from, for free, within one transaction. Our earlier guide to flash loan attacks covers the general pattern.
Treat “derived from balances” and “derived from spot” as the same smell. The Moonwell MAMO exploit inflated a value through direct token transfers; CoreVault inflated one through a swap. Both read a number an attacker could set. Our smart contract security checklist is a good place to start when reviewing a vault.
| Item | Value |
|---|---|
| Chain | BNB Smart Chain |
| Exploit transaction | 0x072a10a579e2a8afa26a681205a2aaaa38f74bcb9d1064b12bd9e9ebe2c598b3 |
| Block / time | 131,507,810 / 3 Nov 2026, 21:50:20 UTC |
| Attacker EOA | 0xd3565b191b28b49cc517189b984dacdb1ff05304 |
| Attack contract (created in tx) | 0xc28cfeee046e1421e9dbfb2cf689074bf9f387e2 |
| Flash loan | 207,358.410468 WBNB |
| WBNB dumped / recovered | 195,000 / 194,901.165115 WBNB |
| PancakeSwap spot before / during | about $700.15 / about $7.16 per BNB |
| Reserve balance change | -259.0928 BNB |
| Attacker profit | +160.2578 BNB (about $112,200 per CertiK) |
CoreVault did not lose its reserve because it trusted a single DEX price on purpose. It lost it because a fallback written for convenience became the only price that mattered for one transaction. If your protocol pays out from a reserve or converts between assets with an on-chain price, talk to our team or check our audit pricing to have the cold-start and fallback paths reviewed before an attacker finds them.
How 53M MAMO sent straight to Moonwell's market on Base bypassed a 20M supply cap, inflated collateral 3.68x and unlocked $11.03M of loans.
How 99 withdrawal requests for 0 CORE emptied 91 b14g staking orders on Core, why zero meant 'everything', and what the hard-fork recovery changes.
How an attacker used freshly created option pools and Balancer flash loans to pull about $437K of collateral out of six Premia v3 vaults on Arbitrum.
How a replaced rebalancing contract that kept its permissions let an attacker drain about 8.57 WBTC from the Troves xWBTC/WBTC vault on Starknet.