Premia's $437K Loss and How Olympix Would Have Prevented It
At 03:14:37 UTC on November 1, 2026, an attacker created six brand-new Premia v3 option pools on Arbitrum. Sixty-two seconds later, Premia's own underwriter vaults started selling options into those pools, and by 03:15:40 UTC six vaults had lost 3.644 WBTC, 43.47 WETH, 46,215 ARB and 48,479 USDC.e. Priced at the hour of the attack, that's about $437,279 taken from vault depositors in two transactions one second apart.
The vaults asked one question before locking collateral into a pool: did Premia's factory deploy it. The attacker had made sure the answer was yes. Once the collateral sat inside a pool the attacker controlled end to end, the pool's fee accounting paid it straight back out to the attacker as "fees".
Vaults That Underwrite Whatever the Factory Built
Premia v3 splits options into two layers. Pools are per-series markets, one for each combination of base and quote token, strike, maturity and call or put, and anyone can create one through the pool factory. Underwriter vaults sit on top. Depositors put one asset into a vault, and the vault sells options against it, collecting premiums. Call vaults hold the base asset (WBTC, WETH or ARB); put vaults hold USDC.e. When someone buys from a vault, the vault calls the matching pool's writeFrom function and locks its collateral there, so the buyer can be paid if the option finishes in the money.
That design depends on the vault only writing into pools that behave like real markets. The verified UnderwriterVault implementation on Blockscout shows how the vault finds its pool. Its _getPoolAddress function builds a pool key from the vault's own base, quote and oracle plus the trader's strike and maturity, then asks the factory:
The quote logic stores that result as quote.pool, and the only pool check on the buy path is this line:
The error is called OptionPoolNotListed, but the condition only tests whether the pool exists. The vault's other checks look at the option itself: a nonzero strike, a maturity in the future, days to expiry and delta inside the vault's bounds, and enough free collateral. None of them look at who created the pool, how old it is, or whether anyone besides the trader has liquidity in it. On a buy, the vault even calls addListing for the strike and maturity itself, so the trade is what puts the series on the vault's books.
Six Pools, Sixty-Two Seconds
The setup transaction landed in block 520,166,285 at 03:14:37 UTC. The attacker's account called its own contract, and Blockscout shows six PoolProxy contracts created in that transaction by Premia's pool factory deployer. Each new pool also carried dust-sized liquidity positions owned by three helper addresses, which cost nothing and would matter later.
The first drain transaction followed at 03:15:39 UTC in block 520,166,479. The attack contract took three flash loans from Balancer (0.9 WBTC, 11 WETH and 6,000 USDC.e) and worked through the WBTC call, WETH call and WETH put vaults one pool at a time. The WBTC call pool shows the pattern clearly:
The attacker paid the WBTC call vault a premium of 0.11714618 WBTC to buy 3.75 contracts. The vault called writeFrom on the attacker's pool and sent it 3.76125 WBTC, which was 3.75 WBTC of collateral plus a 0.01125 WBTC protocol fee.
The attacker deposited two liquidity positions of 187.1 contracts each, posting 0.28065 and 0.46775 WBTC, about 0.748 WBTC in total.
It ran ten pairs of tiny trades of 0.0001 contracts against its own liquidity, each paying a taker fee of 0.0000003 WBTC.
The three helper positions claimed zero fees and withdrew, and one of them deposited again. At that tick update the pool recorded a fee rate of 0.024 WBTC per contract for the attacker's range, up from zero.
The pool paid 0.0112524 WBTC in protocol fees, then emitted ClaimFees for the attacker's position: 4.4904 WBTC, which is exactly 187.1 contracts times 0.024.
The attacker put about 0.748 WBTC in and took 4.4904 WBTC out as fees, while its trades had paid a few millionths of a WBTC in real fees. The difference was the vault's collateral. The WETH call pool repeated it with 45 WETH of vault collateral and a 53.88 WETH fee claim, and the WETH put pool with 22,000 USDC.e of collateral and a 26,347.2 USDC.e claim.
The second transaction hit one second later, at 03:15:40 UTC in block 520,166,483, with three more flash loans. The WBTC put vault wrote 20,790 USDC.e of collateral into its pool and the attacker claimed 24,898.104 USDC.e. The ARB call vault wrote 48,000 ARB and the claim was 57,480 ARB. The ARB put vault wrote 7,200 USDC.e and the claim was 8,622.72 USDC.e. The same transaction swapped the WBTC, USDC.e and ARB into WETH through Uniswap v3 and stablecoin pools, and across both transactions the contract unwrapped about 191.8 WETH into ETH.
The on-chain record pins down where the money went and that it came out as fee claims far larger than the fees paid in. It doesn't by itself say which line in the pool's fee and tick accounting let the fee rate jump to 0.024 after the helper positions left and one returned, and as of November 13, 2026 no public analysis had identified that line. What's clear from the events is the shape of it: in a pool where the attacker controlled every position, the fee accounting credited the attacker's liquidity with fees that no trader had paid.
Counting the Loss
Netting every token transfer into and out of the six vault contracts across both drain transactions gives the depositors' loss after the premiums the attacker paid in:
WBTC call vault: 3.64410382 WBTC. WETH call vault: 43.47254568 WETH. ARB call vault: 46,215.43 ARB. WBTC put vault: 20,215.07 USDC.e. WETH put vault: 21,333.61 USDC.e. ARB put vault: 6,930.68 USDC.e.
At CoinGecko's 03:00 UTC prices on November 1 (WBTC about $77,607, WETH about $2,263, ARB about $0.164 and USDC.e at $1.00), that's $437,279, with the WBTC call vault alone accounting for about $282,800. Other figures in circulation measure different things. PeckShield put the loss at $464.6K, Blockaid's figure of about $400,500 came while the attack was still running, and Hokanews reported about $437,000, in line with the reconstruction here.
After the Drain
Within four minutes of the second transaction, the attacker's account began sending ETH out in batches of 10 to 31.8 ETH. According to PeckShield, the funds were bridged from Arbitrum to Ethereum and swapped into BTC through Chainflip and NEAR Intents. No recovery had been reported as of this writing.
On October 19, 2026, Premia Labs announced it was winding down the Kyan exchange, the successor to Premia, and said a smaller team was building 0-DTE spread vaults. As of November 13, 2026, the @KyanExchange account hadn't posted about the incident, and crypto.news reported that Premia Labs hadn't released an explanation, recovery plan or reimbursement plan. It also quotes Blockaid's summary: empty option pools, flash loans, and option buys that locked vault collateral in those pools before it was withdrawn.
It's the same misplaced trust in something an attacker could create that let forged packets from an attacker-run chain cost Secret Network $4.67M in June: the system checked that an input had a legitimate origin and never asked whether the thing at that origin deserved its money.
How Olympix Would Have Caught the Open Pool Path
Olympix reads a contract as states, transitions and entry points, and takes in the surrounding context to infer the invariants a system relies on but never writes down. For Premia's vaults, two invariants fall straight out of the design. Collateral a vault locks into a pool should leave that pool only to pay option holders at exercise or to return to the vault. And the fees a liquidity position claims from a pool can't exceed the fees traders actually paid into it.
From there the engine maps paths. It narrows from every path in the system to the feasible ones, then to the ones that touch an invariant. The path here is short and fully public: create a pool through the factory, call the vault's trade function for that pool's strike and maturity, let writeFrom move vault collateral into it, add liquidity, trade against yourself, and claim fees. Any address can do every step.
Had the team run Olympix on the vault and pool contracts, the engine would have searched that sequence for a way to break the fee invariant and would have returned a counterexample: the minimal set of state changes, the conditions that make it work (a fresh pool where one actor controls every position), and a generated Forge test that replays it. That test would have shown fee claims larger than the fees paid in and vault collateral leaving the pool as fees with no option ever exercised. It would have been the November 1 drain, produced in development, at a merge where fixing it costs a code review.
The fix for each half is plain once the sequence is on the table. Vaults can keep an explicit allowlist of pools they'll underwrite, or require a minimum age and outside liquidity before writing into a new series. Pools can bound every fee claim by the fees actually accrued in the range.
The Lesson for Options Builders
When one contract hands custody to another, the receiving contract becomes part of the first one's security model, so the case to test is an attacker building that receiving contract with the protocol's own factory. Audits are an essential part of that work, and they're stronger when every merge also runs against inferred invariants that can produce a concrete failing sequence. If you want to see how that would look across your own vault and pool contracts, book a technical walkthrough.
On-Chain References
Attacker EOA: 0xbeBa...4dBd
Attack contract: 0x5771...785f
Setup tx, six pools created (block 520,166,285): 0xe3c2...31f5
First drain tx (block 520,166,479): 0x6460...41ac
Second drain tx (block 520,166,483): 0xd7e8...44ff
Pool factory deployer: 0xAB98...1543
Attacker-created WBTC call pool: 0x1B25...D77d
UnderwriterVault implementation (verified source): 0xb539...3d21
WBTC call vault: 0x45dF...a531
WETH call vault: 0xbae4...8e1B
ARB call vault: 0xDC63...2fFf
WBTC put vault: 0x6A1F...8C1F
WETH put vault: 0xD469...A52a
ARB put vault: 0xBe3E...439e
More post-mortems
See every post-mortem →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.




