[VPD-893]: Add DeviationBoundedOracle Contracts#306
[VPD-893]: Add DeviationBoundedOracle Contracts#306
Conversation
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…ve events/errors to interface
| */ | ||
| constructor(ResilientOracleInterface _resilientOracle, address nativeMarketAddress, address vaiAddress) { | ||
| ensureNonzeroAddress(address(_resilientOracle)); | ||
| ensureNonzeroAddress(nativeMarketAddress); |
There was a problem hiding this comment.
| ensureNonzeroAddress(nativeMarketAddress); | |
| ensureNonzeroAddress(nativeMarketAddress); | |
| ensureNonzeroAddress(vaiAddress); |
contracts/DeviationBoundedOracle.sol
Outdated
| } | ||
| _expandPriceWindow(state, spot, asset); | ||
| _checkAndTriggerProtection(state, spot, asset); | ||
| (minPrice, maxPrice) = _resolveBoundedPrices( |
There was a problem hiding this comment.
state.minPrice, state.maxPrice, and state.isProtectedPriceActive are all re-read from storage here after _expandPriceWindow and _checkAndTriggerProtection have already loaded (and potentially written) them. That's up to 3 avoidable warm SLOADs on every call. Consider having _expandPriceWindow return (uint128 windowMin, uint128 windowMax) and _checkAndTriggerProtection return bool nowActive, then pass those locals in directly.
| address asset = _getUnderlyingAsset(vToken); | ||
| MarketProtectionState storage state = assetProtectionConfig[asset]; | ||
| uint256 spot = _fetchSpotPrice(asset); | ||
| if (!state.isBoundedPricingEnabled) { |
There was a problem hiding this comment.
If setTokenConfig was never called for the asset, state is zero-initialized so isBoundedPricingEnabled == false and this silently returns (spot, spot) with no protection — every other helper here uses _ensureInitialized and reverts. We should add _ensureInitialized(asset) at the top here as well.
There was a problem hiding this comment.
_ensureEnabled is only called by helpers that operate on assets enabled for bounded pricing. Returning the spot price here is intentional, it prevents non-enabled assets from falling back to the resilient oracle, avoiding additional branching in the Comptroller.
| /// @notice Timestamp of the last protection trigger — reset on every trigger | ||
| uint64 lastProtectionTriggeredAt; |
There was a problem hiding this comment.
_checkAndTriggerProtection early-returns when isProtectedPriceActive == true, so this timestamp is only ever set on the initial false→true transition — it's never refreshed for subsequent anomalies during the same protection cycle.
There was a problem hiding this comment.
Yes, as per the design this should be updated on each call, but I was thinking the threshold breach itself might be sufficient.
I think it should be fine to rely on a fixed cooldown from when it was triggered, since additionally we also have a threshold based check. This avoids repeated updates on every call, keeping it open here.
contracts/DeviationBoundedOracle.sol
Outdated
| * @custom:error PriceRangeNotConverged if window range is still above exit threshold | ||
| * @custom:event ProtectedPriceDisabled | ||
| */ | ||
| function disableActiveProtection(address asset) external { |
There was a problem hiding this comment.
For address(0) or any uninitialized asset, state is the zero default with isProtectedPriceActive == false, so this reverts with ProtectedPriceInactive — a misleading error suggesting the asset is configured but inactive. Add ensureNonzeroAddress(asset) and use _ensureInitialized(asset) like the other governance functions, so callers get MarketNotInitialized instead.
| revert ProtectedPriceActive(asset); | ||
| } | ||
|
|
||
| state.isBoundedPricingEnabled = enabled; |
There was a problem hiding this comment.
Calling with enabled equal to the current value still pays an SSTORE and emits a misleading BoundedPricingWhitelistUpdated event. Add if (state.isBoundedPricingEnabled == enabled) return; before the write.
| revert ProtectedPriceActive(asset); | ||
| } | ||
|
|
||
| state.isBoundedPricingEnabled = enabled; |
There was a problem hiding this comment.
Re-enabling a previously disabled asset leaves the window state stale. If asset was disabled at spot=$15 with window ($13, $17) and re-enabled later at spot=$80, the next public call expands maxPrice to $80, fires the trigger ($80 > $13 * 1.1667), and returns collateral=$13 for an asset worth $80 — mass liquidations follow.
when flipping enabled false → true, we should re-seed minPrice = maxPrice = _fetchSpotPrice(asset).
|
@coderabbitai review |
…eviationBoundedOracle
contracts/DeviationBoundedOracle.sol
Outdated
| minPrice: spotU128, | ||
| maxPrice: spotU128, | ||
| isProtectedPriceActive: false, | ||
| isBoundedPricingEnabled: true, |
There was a problem hiding this comment.
how about we have isBoundedPricingEnabled pass in as a param
There was a problem hiding this comment.
Yes would be more flexible adde here: 58ce852
contracts/DeviationBoundedOracle.sol
Outdated
| function disableActiveProtection(address asset) external { | ||
| _checkAccessAllowed("disableActiveProtection(address)"); | ||
| ensureNonzeroAddress(asset); | ||
| _ensureInitialized(asset); |
There was a problem hiding this comment.
MarketProtectionState storage state = _ensureInitialized(asset); ?
contracts/DeviationBoundedOracle.sol
Outdated
|
|
||
| MarketProtectionState storage state = _ensureInitialized(asset); | ||
|
|
||
| if (!enabled && state.isProtectedPriceActive) { |
There was a problem hiding this comment.
how about rename isProtectedPriceActive to currentlyUsingProtectedPrice etc..
| * @custom:event ProtectionInitialized | ||
| * @custom:event BoundedPricingWhitelistUpdated | ||
| */ | ||
| function setTokenConfig( |
There was a problem hiding this comment.
ideally we should have batch version for the following setters as we might want to enable this for mostly all the assets
| if (rangeRatio >= state.resetThreshold) { | ||
| revert PriceRangeNotConverged(asset, rangeRatio, state.resetThreshold); | ||
| } | ||
|
|
There was a problem hiding this comment.
lastProtectionTriggeredAt = 0 ?
contracts/DeviationBoundedOracle.sol
Outdated
| uint256 spot, | ||
| address asset | ||
| ) internal returns (bool triggered) { | ||
| if (state.isProtectedPriceActive) return true; |
There was a problem hiding this comment.
Let's try not to early return here, as we need to update the lastProtectionTriggerAt, imagine that protectedPriceActive has already been set to true, and then the market is volatile and constantly exceeds the deviation threshold. In this case, we need to keep lastProtectionTriggerAt up to date as well. In the current implementation, this is always going to be the stale value.
| constructor(ResilientOracleInterface _resilientOracle, address nativeMarketAddress, address vaiAddress) { | ||
| ensureNonzeroAddress(address(_resilientOracle)); | ||
| ensureNonzeroAddress(nativeMarketAddress); | ||
| ensureNonzeroAddress(vaiAddress); |
There was a problem hiding this comment.
do we have vai on on bsc chain ?
There was a problem hiding this comment.
Yes, we do have it, there isn’t any market for it, just the token address. Still, I think it’s good to keep the bounded oracle consistent across all assets for overall oracle behavior.
…lds, and fix NatSpec
Description
Adds DeviationBoundedOracle contract that wraps the Resilient Oracle and provides manipulation-resistant pricing for lending operations
Maintains a per-market rolling min/max price window and automatically activates protection mode when the spot price deviates significantly from the window bounds
In protection mode, collateral is valued at min(spot, windowMin) and debt at max(spot, windowMax), guarding against short-duration price manipulation on low-liquidity tokens
Supports configurable entry/exit thresholds with hysteresis and per-market cooldown periods
Adds IDeviationBoundedOracle interface