TL;DR
This is just my live research — should not be taken as a source of truth until the official Post-mortem.
An attacker created and activated several option markets, waited until expiry, and then manipulated oracle expiry prices to force massively inflated settlements.
By abusing broken oracle access control and unsafe proxy ownership flows, they were able to set arbitrary expiry prices and drain all collateral from the affected vaults.
While the overall exploit path is clear, one critical question remains open for me: how the attacker satisfied the oracle proxy authorization checks in practice.
I will be back to it later so if any readers notice something I might have missed, or have a different perspective on this aspect of the attack, I'd be very happy to hear your thoughts in the comments.
Let's start and run the transaction through Tenderly

Impressive…
But that's not the start of the hack. Before it we have this tx:
It contains the deployment of two contracts

https://etherscan.io/address/0x15a03631e1fa9fcfd935419ba68353c6dbb6e0cf
This is a minimalist owner-controlled proxy / admin wallet / execution. It performs three functions:
- Proxy (delegatecall-router) — proxies any calls to the current implementation
- Admin wallet — allows the owner to: make calls and delegatecalls and call transfer / transferFrom / approve
- Upgrade — the owner can replace the implementation at any time
owner() → 0xcf5df51a10c097140fb3a367281a4f5313725b1fhttps://etherscan.io/address/0x482e51a68cdb369f9c821c90d2c4db01742edfe1
- Deployment and initialization of another key contract (Executor)
- registration of supported markets/assets/oracle/router
- controller settings
- initial configuration populating (assets, collateral, owners)
- subsequent logic upgrade via delegatecall
Key function: function 0xb7b1dd95() was called later in this transaction https://etherscan.io/tx/0x9b686c9d9532f224b84825d5b6c8a8c27811a33de4b0f20204aafd288304ab54
What's going on inside?
delegatecall in stor_9_0_19 → factory / implementation
- Gets a new executor address
stor_9_0_19.delegatecall(...)
→ returns address executor
stor_a_0_19 = executorCalls 0x1578() — initialization of arrays
-----------------------------------
| Underlying| Strike | Collateral |
-----------------------------------
| stETH | USDC | WETH |
| AAVE | USDC | wstETH |
| LINK | USDC | USDC |
| PAXG | USDC | WBTC |
-----------------------------------Added:
- collateral configs
- caps
- risk params
- reward params
Values:
- types 1…4
- limits
- decimals (e.g. 10**8 for USDC)
Adding owners: a multi-owner model is formed on the Executor side.
array_f.push(msg.sender);
array_f.push(0x5c49555418ed5bf828296e13b9d65aeff9ed6b8f);
array_f.push(0x657cdefc7ef8b459b519defc8bed2a67d3cc1aab);Calls the Executor:
- setup(whitelist, factory, oracle, controller)
- setRouter(router)
Loads all markets, collateral, and configs
executor.addMarket(…)Adds owners
Registering collateral/caps/configs: Functions with selectors are called sequentially:
- 0x5ac038ed
- 0x4ad172ee
- 0x938059a5
- 0x8263f60a
- 0x75619427
- 0x71f67bc6
The final step is passing the collateral to the Executor.
token.transferFrom(msg.sender, executor, amount)As result:

oToken is validated => Controller/Vault can now work with it

The Otoken contract was verified:
- the product is whitelisted
- expiry is correct
- the option did not exist previously
Deployed the oToken proxy EIP-1167 Initialized it via init(…)

The following product was allowed in the Whitelist contract:
Ex: Call option (isPut = false) on stETH, with a strike price of USDC, with a collateral of WETH

This series repeats for each market, followed by approval and collateral transfer events. Then, for each received oToken:
User opened a vault:
- vaultId = 1
- type = regular (not a whitelisted vault)

A collateral was added to the vault:
- exactly the one permitted by the product (WETH)
- sufficient to cover the risk

Technically:
- a short-oToken was minted
- oToken was issued to the user
- the vault is now obligated under this option

To summarize the contents
A call options market for stETH/Aave, etc., settled in USDC and collateralized with WETH, was deployed and activated.
To achieve this, the following steps were performed: product whitelisting, oToken deployment and whitelisting, opening vaults, depositing collateral and minting short positions.
After the mint, the market became active, and oToken became a tradable instrument representing the right to buy stETH/Aave, etc., at a fixed price until the expiration date.
There's nothing interesting at the current address, so let's move on to the next address from the owner list. The first important transaction from this address:
https://etherscan.io/tx/0xb73e45948f4aabd77ca888710d3685dd01f1c81d24361d4ea0e4b4899d490e1e
Interaction is carried out with the contract:
https://etherscan.io/address/0x4bfd5c65082171df83fd0fbbe54aa74909529b2c
Thanks dedaub I know the that this contract is designed to manage ownership and implement upgradeable proxy contracts. It is used to perform privileged operations (specifically, Oracle expiry settlement) that require owner access.
Incidentally, this attack occurred just after a change in Ribbon's Oracle infrastructure… There was a series of events like this.




When calling 0xb48dc7a7 on this contract:
- The contract takes ownership and implementation from several proxy contracts
- Fixes the price in the oracle at expiry for each underlying
- Returns ownership and implementation
- Does this for each market (oToken / underlying)
This transaction contains manipulations on the oracle side. And I'm still having trouble understanding why the contracts so freely transferred ownerShip to another address…
In any case, this is done through https://etherscan.io/address/0x9D7b3586f361e3621Bf4F099cBC9d155e8ae6B76 — the administrative Proxy-Owner / Executor contract.
Its sole purpose is to centrally manage other contracts, primarily:
- Upgradeable proxy (owner / implementation),
- oracle / pricer / registry,
- tokens on the balance of these contracts,
Inside the function
function transferOwnership(address conduit, address newPotentialOwner) public nonPayable {
require(msg.data.length - 4 >= 64);
require(_transferOwnership[tx.origin], Error('ProxyOwner: address not authorized'));
require(bool(newPotentialOwner.code.size));
v0 = newPotentialOwner.transferOwnership(conduit).gas(msg.gas);
require(bool(v0), 0, RETURNDATASIZE()); // checks call status, propagates error data on error
}So somehow the address was already marked in _transferOwnership[tx.origin]. I'm stuck at this point.
cast keccak \
$(cast abi-encode "f(address,uint256)" \
0x657cdefc7ef8b459b519defc8bed2a67d3cc1aab 2)
0xcd289dea5498740e065d241251d02bbaee26b73402c44a441f7b69f710894d49
cast storage \
0x9D7b3586f361e3621Bf4F099cBC9d155e8ae6B76 \
0xcd289dea5498740e065d241251d02bbaee26b73402c44a441f7b69f710894d49 --rpc-url https://mainnet.infura.io/v3/<API_KEY>
result -> 0x0000000000000000000000000000000000000000000000000000000000000001So, the flag for the attacking address was indeed set to True by this point (I can't figure out why???)
This is the critical moment of the attack: without this authorization being in place, none of the oracle manipulation would have been possible.
In any case, after this comes setImplementation, setExpiryPrice, and the return of ownerShip. This happens for each of the created products.
There's really no point in analyzing what follows, as long as the clear call to the oracle is visible.
TOTAL
General procedure:
- An otoken is created
- Waited for expiry
- Called setExpiryPrice with an artificially high price
- Settlement removed the entire collateral.
The puzzles that could lead to this hack I see:
- broken oracle access control.
- the ability to create economically unsound option products.
- the protocol lacked effective safeguards to limit payouts or contain the impact, allowing a single flaw to cascade into a full drain.