-
Notifications
You must be signed in to change notification settings - Fork 93
OETH Oracle #1815
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
OETH Oracle #1815
Conversation
Added more OETH Oracle fork tests
…elated Added governable behaviour tests to OETH Oracle unit tests
|
Second pass review looks good. |
Review part 1: RequirementsWe want to provide an oracle for OETH for use on lending platforms, both classic lending platforms and CDP style stablecoins, to allow them to determine the value of OETH as collateral. Oracles for lending platforms and stablecoin CDP collateral have some core considerations: There is no safe direction for the oracle to fail to. If an oracle reports a price that is too low, then the borrower' collateral will report as insuffienct, and will be liquidated. This make angrey borrowers. If the reported price is extra low, then these liquidations could operate agaist the protocol as well, making the protocol itself insolvant. If prices are too high, then attackers will deposit OETH, and borrow out more than its value in collateral, making the protocol insolvant. Oracles only have to be near the right answer. Because lending platforms / CDP's usually have collateral ratios on ETH of 110%+, 5-8% off in either direction will not result in protocol level insolvancy. This is a pretty big amount of allowed error, and is a good thing. However, even small errors in the direction of low will result in people who were on the edge of liquidation getting liquidated. They would be mad, but at least it's not everybody. Transient errors are very bad. A single extreme bad value can cause a lot of reckage. Inherint to large lending platforms are a lot of liquidation bots in a constant race with each other. If bad values happen, the consequences can be as fast as the same block that the oracle update happened in. Proposed architectureWe will have a chainlink compatibile oracle contract, feed by an on chain updater contract using on chain data, callable by anyone. A chainlink keeper will backstop the calls. This seems like a good architecure. Great compatibility, cheap reads. No hard requirement that oracle be updated by the origin team, and a good backstop as long as someone keeps feeding the keeper money. Proposed pricing strategyOur current plan for getting new prices to use in updates is:
How does this work in practice:
Assuming our vault, strategies, and backing coins are healthy, this pricing strategy should work great for the intended use case. The cap prevents curve pool manipulation up, and any manipulation down is quickly limited by the vault's floor price. The 0.5% margin for error here is well within what lending platforms would tolerate with no problems. However, the pricing strategy could be broken if something were wrong.
|
Review Part 2: Easy ChecksAuthentication
Ethereum
Cryptographic codeNo crypto Gas problems
Black magic
Overflow
Proxy
Events
This behavior is okay. We emit events on the updater contract for updates, but not on the oracle contract itself. However, this matches chainlinks behavior, and all updates can be tracked down by getting round info. |
| marketPrice = _getMarketPrice(); | ||
|
|
||
| // If market price is above the vault price with the withdraw fee | ||
| if (marketPrice > MAX_VAULT_PRICE) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
>= would be more efficient here, assuming MAX_VAULT_PRICE is truly the max the vault can report. If the vault actually did report MAX_VAULT_PRICE and the marketPrice was also MAX_VAULT_PRICE, then we would get the same answer checking the vault as not checking the vault.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good point. I'll update
|
|
||
| import { IOracleReceiver } from "./IOracleReceiver.sol"; | ||
| import { IVault } from "../interfaces/IVault.sol"; | ||
| import { AggregatorV3Interface } from "../interfaces/chainlink/AggregatorV3Interface.sol"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think AggregatorV3Interface is used?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AggregatorV3Interface is not used. I'll remove it.
Apparently Slither can detect unused imports. I'll see why it's not detecting ours.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I was wondering how this got through the automated tools. I should have chased this down more.
Review Part 3: Medium ChecksRounding
Dependencies
External calls
Deploy
|
|
@naddison36, got any thoughts on how we should handle bad vault situations? Only thing I've thought of is that we could factor in the difference between the vault total supply and the OUSD supply, if it's less than one. Would add a ton of gas to the oracle update, but in theory we only need to actually call the update if there's been a big price move, and we only the pay the cost if we are off peg. |
|
There is some good discussion here. Regarding the bad Vault State:
|
| ****************************************/ | ||
|
|
||
| /// @notice The number of decimals in the Oracle price. | ||
| function decimals() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could also be expressed as:
uint8 public constant override decimals = 18;
| expect(await oethOracleUpdater.vault()).to.eq(oethVault.address); | ||
|
|
||
| expect(await oethOracle.decimals()).to.eq(18); | ||
| expect(await oethOracle.version()).to.eq(1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would be cool to have a test that maps deployed implementation addresses to versions. And when we attempt to update the Oracle and forget to also bump the version that test would fail.
sparrowDom
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@naddison36 do you mind pinging when you implement the changes we have discussed today and I'll finish my full review?
| /// Can not be run twice in the same block. | ||
| /// @param _price is the Oracle price with 18 decimals | ||
| function addPrice(uint128 _price) external override { | ||
| if (msg.sender != oracleUpdater) revert OnlyOracleUpdater(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 on using custom errors
| // price is to 18 decimals, so we do not need to scale them up to 18 decimals | ||
| price += | ||
| redeemAssets[i] * | ||
| IOracle(priceProvider).price(allAssets[i]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Leaving a note here so it is not forgotten: we query oracle price per asset in this line above and also in _toUnitPrice function withing the _calculateRedeemOutputs
sparrowDom
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Eyeballed the PR again and left some extra comments
| /// @notice Contract or account that can call addRoundData() to update the Oracle prices | ||
| address public oracleUpdater; | ||
|
|
||
| /// @notice Last round ID where isBadData is false and price is within maximum deviation |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't find any reference to isBadData does this comment need updating?
| addresses.mainnet.Timelock | ||
| ); | ||
| expect(await oethOracleUpdater.MAX_VAULT_PRICE()).to.eq( | ||
| parseUnits("0.995") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
to add extra security we should add a fork test that subtracts the VaultCore's redeemFeeBps from 1 and see if it matches "0.995". This way the fork test will fail in case we update the redeemFeeBps and forget to re-deploy the OETH oracle updater with its MAX_VAULT_PRICE constant
| answer = marketPrice; | ||
| // Avoid getting the vault price as this is gas intensive. | ||
| // its not going to be higher than 0.995 with a 0.5% withdraw fee | ||
| vaultPrice = MAX_VAULT_PRICE; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mental note: I think we've discussed this to change to:
vaultPrice = vault.floorPrice();
answer = Math.min(vaultPrice + vault.redeemFeeBps().scaleBy(18, 4), marketPrice);
|
|
||
| if (marketPrice > vaultPrice) { | ||
| // Return the market price with the Vault price as the floor price | ||
| answer = marketPrice; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mental note: and this one similarly:
answer = Math.min(vaultPrice + vault.redeemFeeBps().scaleBy(18, 4), marketPrice);
probably the if condition can be reworked
| answer = marketPrice; | ||
| } else { | ||
| // Return the vault price | ||
| answer = vaultPrice; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And I think we've said that if market price is lower than the vaultPrice, then we might go with the market price.
Thinking more about this, I am not so sure this is a good idea, since the market price can be manipulated downwards even though there is a smaller financial incentive to do it. The way it is in the codebase right now seems safer to me.
probably something we should discuss more about.
|
After some analysis, we decided to not use this oracle approach. We did end up discovering a bug in Curve's oracle implementation though. |
Contract Changes
floorPriceto the Vault to get the value (USD or ETH) of the collateral assets received from redeeming 1 Origin Token (OUSD or OETH) from the Vault.OETHOraclewhich is updated byOETHOracleUpdaterthat aggregates on-chain OETH/ETH prices.price_oracleto Curve interface which gets the EMA Oracle priceTests
Unit tests
Fork tests
Review
If you made a contract change, make sure to complete the checklist below before merging it in master.
Refer to our documentation for more details about contract security best practices.
Contract change checklist: