OToken Vault Oracles - #2953
Conversation
There was a problem hiding this comment.
I see a couple of issues with this approach.
1. Curve AMO self reference
Curve AMO strategy's checkBalance reads the virtualPrice on the Curve pool. And in that pool there are real backing assets as well as printed OToken. The AMO strategy, for that reason, has a virtual increased weight on the oracle price when some other strategy becomes under-backed.
e.g. AMO Strategy can be 40% of Vault Value, but if you withdrawAll from it it can represent only 10% of Vault Value. Meaning while funds are in the AMO they have artificial too strong weight.
The oracle would still show depeg it would just soften it because of the AMO.
2. Donation attack & Rebase accounting
Already left comments inline regarding the donation attack. A similar issue is with rebase accounting. Not all OToken supply is rebasing. So if we were to correctly report the "pending yield" or in other words by what amount OTokens are priced above 1e18 because the yield hasn't been rebased yet... this becomes a bigger accounting issue / gas usage issue. We should really only get the previewYield(super gas expensive) and apply it to rebasingSupply - considering we chose to ignore the value nonRebasingSupply is foregoing by choosing to opt out. That would give us a ceiling for the premium OToken price we can report. The proposed fix to hardcode the ceiling to 1e18 addresses both of these issues.
3. Correct oracle naming / framing.
This isn't a market price oracle and is rather a pure solvency/backing oracle. And I think we should be really verbose about that. I would rename the descriptions to:
- "OETH / WETH Vault Backing Ratio"
- "OUSD / USDC Vault Backing Ratio"
- "superOETHb / WETH Vault Backing Ratio"
We can also change the Natspec to be explicit that this isn't some sort of "Exchange rate" oracle.
We might also want to rename contracts:
OTokenVaultOracle→OTokenVaultBackingRatio
I propose we update the Natspec to - and we should be explicit... otherwise we will be hammered with Immunefy reports:
/**
* @title OToken Vault Backing Ratio
* @notice Reports the Vault's backing per OToken, in the Vault's underlying asset, capped at par.
* @dev THIS IS NOT A MARKET PRICE AND NEVER READS ONE. It reports book value:
* min(1e18, vault.totalValue() * 1e18 / oToken.totalSupply())
* It reads 1e18 whenever the Vault is fully backed and below 1e18 only when it is not.
* Treat it as a solvency signal, not a price.
*
* Known limitations, in the order that matters:
*
* 1. LOSSES ARE UNDER-REPORTED. Part of Vault value is the Vault's own OToken held in AMO
* positions and marked at par, so a real backing loss surfaces smaller than it is. The
* ratio still crosses 1e18 at exactly the right moment; only the magnitude is compressed.
* 2. INPUTS ARE NOT FRESH. `updatedAt` is the read timestamp, not a freshness attestation.
* E.g. the validator strategy updates its balances once daily on a cadence controlled
* off-chain, so the on-chain data this feed reads can be days old.
* 3. NO OPERATIONAL ENVELOPE. No heartbeat, no deviation threshold, no multi-node
* aggregation, no pause, no owner. `roundId` is synthetic and constant.
* 4. IT CAN REVERT. Any contributing strategy reverting reverts this feed. Chainlink feeds
* effectively never do; decide whether a revert means freeze or fall back.
* 5. THE READ SURFACE IS MUTABLE. The set of strategies this traverses changes behind a 48h
* governance timelock, and strategy weights change with no timelock at all.
*
* Pricing the OToken as collateral: take min(thisFeed, independentMarketFeed).
* As debt: take max(...).
*/
| uint256 supply = oToken.totalSupply(); | ||
| require(supply > 0, "No data present"); | ||
|
|
||
| return (vault.totalValue() * 1e18) / supply; |
There was a problem hiding this comment.
The Vault's totalValue might contain funds that are unreachable - redeemable by the token holders. This is enforced by different yield/rebase caps which prevent the token from capturing all of the VaultValue in a single rebase.
Also this has me slightly worried that offering such an on-chain oracle that reads immediate data opens us up for a read-only oracle attack.
There was a problem hiding this comment.
Oh regarding nominator of vault value what I had in mind was:
uint256 vaultValue = vault.totalValue();
uint256 accumulatedYield = vault.previewYield();
uint256 valueWithYield = accumulatedYield + supply;
// only show Vault value according to the value amount that is allowed to rebase
vaultValue = valueWithYield > vaultValue ? vaultValue : valueWithYield;
return vaultValue * 1e18 / supply;
This fixes the donation lever problem, where one could donate to the Vault and inflate the price value instantly.
The downside is that this operation becomes super costly gas wise:
- OETH: totalValue -> 130k gas, previewYield -> 150k gas
- OUSD: totalValue -> 690k gas, previewYield -> 705k gas
There was a problem hiding this comment.
I guess we could achieve a similar outcome by just upper bounding it. And thus prevent any donation lever:
uint256 price = (vault.totalValue() * 1e18) / supply;
return price > 1e18 ? 1e18 : price;
Summary
Adds immutable Vault-backed price oracles for OETH, OUSD, and superOETHb.
Each oracle reports the value of one OToken in its Vault’s underlying asset:
Prices use 18 decimals and are exposed through:
price()latestAnswer()latestRoundData()getRoundData(1)The OToken address is resolved directly from the configured Vault to prevent mismatched Vault/token deployments.
Deployments
Ethereum Mainnet
OETH_VAULT_ORACLEOETH_VAULT_PROXY(0x39254033945AA2E4809Cc2977E7087BEE48bd7Ab)OETH / WETHOUSD_VAULT_ORACLEOUSD_VAULT_PROXY(0xE75D77B1865Ae93c7eaa3040B038D7aA7BC02F70)OUSD / USDCBase
OETHBASE_VAULT_ORACLEOETHBASE_VAULT_PROXY(0x98a0CbeF61bD2D21435f433bE4CD42B56B38CC93)superOETHb / WETHThe Foundry deployment scripts verify the Vault, OToken, description, decimals, version, and that the returned price is positive. No governance actions are required.
Behavior
1e18when Vault value and OToken supply are equal.1e18as unreleased yield accumulates between rebases.1e18if the Vault becomes insolvent and its assets are worth less than the outstanding OToken supply.Vault.totalValue()is normalized to 18 decimals.No data presentwhen OToken supply is zero.1; the price is calculated live rather than historically stored.Testing
Added concrete and fuzz coverage for:
Operational impact
The deployed oracle names and callable interface are not currently referenced by Talos actions. No curated action ABI, pinned address, or existing Hardhat deployment artifact requires updating in this PR.
Code Change Checklist