DeviationBoundedOracle
The DeviationBoundedOracle (DBO) is the contract that sits between the ResilientOracle and the Core Pool Comptroller on the borrow-power path. It maintains a per-asset rolling price window, detects when spot deviates beyond a configured threshold, and returns conservative bounded prices while the deviation persists. Its user-visible behaviour is the Protection Mode feature; this article covers the contract itself — where it gets prices, what it stores, and how the Comptroller calls it.
For function-level signatures, structs, events, and errors see the DeviationBoundedOracle reference.
Pricing stack

The ResilientOracle fetches and cross-validates spot from the configured sources, exactly as today. The DBO does not replace this — it wraps it. The borrow-power path on the Comptroller never queries the ResilientOracle itself; the DBO is the only oracle the Comptroller talks to on that path.
The DeviationBoundedOracle (DBO) is the single entry point for every borrow-power read on every market. On each call it fetches spot from the ResilientOracle, then returns one of three outcomes:
Asset not whitelisted (bounded pricing disabled): returns
(spot, spot)immediately. No window read, no trigger logic, no storage write — a thin pass-through.Asset whitelisted, protection inactive: returns
(spot, spot). The price equals spot, but the call still expands the rolling window if needed and runs the trigger check, so this is the path that enters protection mode if a deviation appears.Asset whitelisted, protection active: returns
(min(spot, windowMin), max(spot, windowMax)).
The liquidation path (
USE_LIQUIDATION_THRESHOLDandliquidateCalculateSeizeTokens) calls the ResilientOracle directly and never touches the DBO. Eligibility, seize amount, and incentive calculations stay on real-time spot regardless of whether protection is active on either side of the position.
So the DBO is always in the call chain for the borrow-power path. For non-whitelisted markets it is transparent and the price the Comptroller sees is identical to what the ResilientOracle returned; for whitelisted markets it adds the window/trigger logic on top.
Rolling price window
For each whitelisted asset the DBO stores a (minPrice, maxPrice) pair representing the lowest and highest spot prices observed inside a short rolling window (target ~15 minutes). It is maintained from two distinct sources:
User-triggered, expansion only. Every relevant price read pulls spot from the ResilientOracle. If
spot < minPrice,minPriceis pulled down to spot; ifspot > maxPrice,maxPriceis pushed up. User activity can only widen the window — never contract it.Keeper-triggered, bidirectional. The keeper maintains the true rolling window off-chain and pushes corrected values via
updateMinPrice/updateMaxPrice. On-chain validation enforces both a spot bound (newMin ≤ spot,newMax ≥ spot) and a cross bound (newMin ≤ maxPrice,newMax ≥ minPrice) so the window can never be inverted. The 5%KEEPER_DEADBANDis not enforced inside the write path; the keeper queriescheckAndGetWindowDrift(a view) to skip pushes whose delta is below the deadband, purely as a gas-saving heuristic. This is what brings the window back inwards once an extreme has aged out.
Trigger and exit
Protection activates as a side-effect of the price read whenever spot has moved past the deviation threshold relative to the stored window:
triggerThreshold is per-asset, configured by governance, and bounded between MIN_THRESHOLD = 5% and MAX_THRESHOLD = 50% (the 5% floor keeps routine keeper corrections from accidentally firing it). The cooldown timer (lastProtectionTriggeredAt) is re-stamped only on the first activation or when spot makes a new extreme this update (windowExpanded == true). Recovery within the existing window keeps the cooldown ticking, so exitProtectionMode remains reachable once the price stabilises — sustained reads at the same elevated/depressed level do not, by themselves, defer exit.
Exit cannot turn itself off. Both conditions must hold:
When both are satisfied, the keeper / monitor calls exitProtectionMode(asset) (or includes an ExitProtectionMode item inside syncPriceBoundsAndProtections) to clear the active flag. Governance retains a fallback path.
Bounded-price computation
For each whitelisted asset the DBO returns a (collateralPrice, debtPrice) pair:
Collateral is capped at the recent window low so a pumped asset cannot be over-borrowed against.
Debt is floored at the recent window high so a crashed borrow asset cannot be repaid cheaply.
Both legs collapse back to spot once the trigger clears.
Transient price cache
Every non-view path writes the resolved (collateralPrice, debtPrice) pair into EIP-1153 transient storage so a follow-up getBoundedPricesView in the same transaction can return it without re-fetching spot. The cache is per-asset, transaction-scoped, and clears at the end of the transaction.
Caching is configurable per asset via setCachingEnabled(asset, enabled), independently of the bounded-pricing flag. When disabled, writes are no-ops and reads always miss, so every view call recomputes live from the ResilientOracle — a safety lever for forcing fresh evaluation, not a default knob.
Function surface
The contract exposes three small groups of entry points, one per caller role.
Comptroller — borrow-power path. Computing a bounded price requires fetching fresh spot and updating the rolling window — both are state-mutating, so they cannot live inside a view function. But the borrow-power check runs through ComptrollerLens, which is view. The DBO splits the work into two calls and uses the transient cache (above) as the bridge:
updateProtectionState(vToken)— non-view. Pulls fresh spot, expands the window, evaluates the trigger, and writes the resolved pair to the cache. The Comptroller calls this once per entered asset at the start of every borrow-power check, before the lens runs. This is also the single non-view entry point that advances DBO state day-to-day.getBoundedPricesView(vToken)(and the per-leggetBoundedCollateralPriceView/getBoundedDebtPriceView) — view. Returns the cached pair ifupdateProtectionStateran earlier in the same transaction; otherwise recomputes live from spot without any state writes.getBoundedPrices(vToken)— non-view convenience that does both in one call; for integrators that don't need the pre-warmed-cache split.
Keeper — window correction and exit. All three actions are gated by AccessControlManager.
updateMinPrice(asset, newMin)/updateMaxPrice(asset, newMax)— push corrected bounds. On-chain checks enforcenewMin ≤ spot ∧ newMin ≤ maxPriceandnewMax ≥ spot ∧ newMax ≥ minPrice. The 5%KEEPER_DEADBANDis checked off-chain viacheckAndGetWindowDrift, not in this write path.exitProtectionMode(asset)— clears the active flag once cooldown has elapsed and the window has converged belowresetThreshold. Reverts otherwise.syncPriceBoundsAndProtections(items[])— atomic batch of the above across multiple assets in a single ACM check; any item revert rolls back the whole batch.
Governance — configuration.
setTokenConfig(...)(and the batch variant) — initializes a market'sMarketProtectionState, setstriggerThreshold,resetThreshold,cooldownPeriod, the initialcachingEnabledflag, and the initial bounded-pricing flag. Reverts onMarketAlreadyInitialized,VAINotAllowed(VAI is rejected by design), threshold violations, or if the seeded spot price overflowsuint128. The initial bounded-pricing flag is independent of initialization, so an asset can be configured first and whitelisted later viasetAssetBoundedPricingEnabled.setAssetBoundedPricingEnabled(asset, enabled)— toggles whether the DBO runs the full window/trigger logic for the asset or short-circuits to spot.setCachingEnabled(asset, enabled)— toggles the per-asset transient cache participation independently of bounded pricing.
Liquidation path is unchanged
The borrow-power path uses WeightFunction.USE_COLLATERAL_FACTOR and is the only path that calls _updateProtectionStates. The liquidation path uses WeightFunction.USE_LIQUIDATION_THRESHOLD and reads spot from the ResilientOracle directly. No bounded prices touch eligibility, seize-amount, or incentive calculations regardless of whether protection is active on either side of the position.
Off-chain components
Keeper. Maintains the authoritative 15-minute rolling window off-chain and pushes corrected
minPrice/maxPriceon-chain whenever the stored values have drifted past the 5% deadband. The on-chain writes are constrained bynewMin ≤ spotandnewMax ≥ spot. Once the on-chain exit conditions are satisfied — cooldown elapsed and window converged belowresetThreshold— the keeper submitsexitProtectionMode(asset), or includes anExitProtectionModeitem inside asyncPriceBoundsAndProtectionsbatch.Monitor. Observes protection events and price normalization across independent feeds; gates keeper exit calls on real recovery rather than just the on-chain timer.
Governance. Whitelists assets via
setTokenConfig(single or batch), sets per-assettriggerThreshold,resetThreshold, andcooldownPeriod, can toggle bounded pricing throughsetAssetBoundedPricingEnabled, and retains a fallback path to disable protection for an asset if the keeper path is unavailable.
Further Reading
Last updated

