Price Guard
Overview
Price guards are oracle-side bounds that Curvance can apply before a price is returned to protocol consumers. They are designed for pegged or reference-priced assets where a bounded price range is part of the market configuration, such as stablecoins or liquid staking assets priced against a reference asset.
A guard has a minimum price and a maximum price. If the observed price falls below the minimum, the guard returns 0 so the pricing path can treat the read as an error. If the observed price is above the maximum, the guard returns the maximum. If the observed price is inside the range, the guard returns the observed price.
Guards can be static or dynamic:
Static guards use fixed
minPriceandbasePricevalues.Dynamic guards increase both bounds over time from
timestampStartusingips, a WAD-scaled per-second growth rate.
Where Guards Live
Price guard state lives in two source locations.
Adaptor Guards
BaseOracleAdaptor stores guards in priceGuards[asset][inUSD]. This means each supported asset can have separate guard configuration for USD-denominated prices and native-token-denominated prices.
Adaptor guards are applied after _adjustPrice() normalizes the incoming feed value to WAD precision. In this path, minPrice and basePrice should be configured in the same 18-decimal price scale that the adaptor returns.
Combined Aggregator Guards
CombinedAggregator stores one guard in pg. This guard applies only to secondaryAggregator.
The combined answer is computed from the primary feed and the guarded secondary answer. The secondary guard is applied before the multiplication with the primary feed. CombinedAggregator does not provide a separate guard for the primary feed.
For this path, minPrice and basePrice are in the same units as the secondary feed answer, not the final combined WAD-normalized adaptor price.
PriceGuard Struct Fields
IOracleAdaptor.PriceGuard is the shared struct used by both guard locations.
timestampStart
The timestamp used as the dynamic growth start. Static guards require this to be 0.
ips
Increase per second, WAD-scaled. 0 means the guard is static.
basePrice
The starting maximum price. For static guards, this is the fixed maximum. 0 means the guard is disabled.
minPrice
The starting minimum price. For static guards, this is the fixed minimum.
Guard Validation
Adaptor Guard Validation
BaseOracleAdaptor.setGuardedPriceConfig() validates the requested guard before storage is updated:
The caller must pass the market-permission check.
If
ips == 0,timestampStartmust be0.If
ips > 0,timestampStartmust be nonzero, not in the future, and at least seven days before the current block timestamp.ipsmust fit inuint40.basePricemust be nonzero and fit inuint88.minPricemust be less than or equal tobasePrice.The asset must price successfully for the requested denomination through both
getPrice(asset, inUSD, false)andgetPrice(asset, inUSD, true).The returned price denomination must match the requested
inUSDvalue.The guarded current minimum cannot be above the current price used in the final validation read.
If an existing dynamic guard has a later
timestampStart, a new nonzerotimestampStartcannot move it backward.
After validation, the adaptor stores the packed field values and emits PriceGuardUpdated(pg).
Combined Aggregator Guard Validation
CombinedAggregator.setGuardedPriceConfig() applies the same timestamp, type-width, nonzero basePrice, and minPrice <= basePrice checks. It then validates the secondary feed directly:
The caller must pass the market-permission check.
secondaryAggregator.latestRoundData()must return a positive answer.The secondary feed update must be inside
secondaryHeartbeat.The guarded current minimum cannot be above the current secondary answer.
If an existing dynamic guard has a later
timestampStart, a new nonzerotimestampStartcannot move it backward.
After validation, the aggregator stores the packed field values and emits PriceGuardUpdated(pg).
Runtime Guard Behavior
When no guard is active, basePrice == 0 and the price is returned unchanged.
For static guards, where ips == 0:
If
price < minPrice, the guard returns0.If
price > basePrice, the guard returnsbasePrice.Otherwise, the guard returns
price.
For dynamic guards, both bounds grow using the same formula:
timePassed is block.timestamp - timestampStart. The formula is applied separately to minPrice and basePrice.
At runtime:
The dynamic minimum is calculated from
minPrice.If the observed price is below the dynamic minimum, the guard returns
0.The dynamic maximum is calculated from
basePrice.If the observed price is above the dynamic maximum, the guard returns the dynamic maximum.
Otherwise, the observed price is returned.
In CombinedAggregator, this guarded secondary answer is used in latestRoundData() and getAdjustedAnswer(int256 answer) before the final combined answer is returned. If the secondary feed is stale, latestRoundData() sets updatedAt to 0, while getAdjustedAnswer() returns 0.
Configuration and Disable Flows
Adaptor guards are configured per (asset, inUSD) pair:
Adaptor guards are disabled per (asset, inUSD) pair:
Combined aggregator guards are configured once for the secondary feed:
Combined aggregator guards are disabled by deleting pg:
Do not try to disable a guard by calling setGuardedPriceConfig() with basePrice == 0. The setter rejects a zero basePrice. Use the disable function for the relevant guard location.
When an adaptor asset is removed through removeAsset(address asset), BaseOracleAdaptor deletes the supported-asset flag, wipes asset-specific config, disables both the USD and native-token guards, notifies the Oracle Manager, and emits AssetRemoved(asset).
Permission Model
Guard configuration and disable functions use centralRegistry.hasMarketPermissions(msg.sender). Callers without market permissions revert with the local unauthorized error.
BaseOracleAdaptor.removeAsset(address asset) uses centralRegistry.hasElevatedPermissions(msg.sender) because it removes adaptor support for the asset, not only guard configuration.
Read functions do not require a privileged caller.
Integration Considerations
Do not assume every asset has a price guard. A guard with basePrice == 0 is disabled.
Treat a guarded return value of 0 as a pricing failure signal. It means the observed price was below the configured minimum. In CombinedAggregator, getAdjustedAnswer() also returns 0 when the secondary feed is stale, while latestRoundData() signals that stale secondary feed by returning updatedAt = 0.
For adaptor guards, read and configure the USD and native-token guard slots independently. A guard stored for inUSD == true does not protect the inUSD == false path.
For combined aggregators, the guard is specific to the secondary feed. It does not isolate changes in the primary feed.
When preparing dynamic guard updates, validate the timestamp before submitting the transaction. The contract requires a nonzero timestampStart that is not in the future and is at least seven days before the current block timestamp.
Use real asset and aggregator addresses from the deployment registry for the chain you are integrating with. Documentation examples should use placeholders such as 0x....
User Interaction Functions
Read Guarded Adaptor Price
getPrice()
Description: Returns adaptor pricing data for a supported asset. Where the adaptor implementation uses the BaseOracleAdaptor price adjustment helper, the returned price reflects any active guard for the requested asset and denomination.
Contract: IOracleAdaptor / BaseOracleAdaptor
Function signature:
Inputs:
address
asset
The asset to price.
bool
inUSD
true for a USD-denominated price, false for a native-token-denominated price.
bool
getLower
Selector used by adaptor implementations that support multiple price paths.
Return data:
PricingResult
None
Struct containing price, inUSD, and hadError.
Events: None.
OracleAdaptor.getPrice() checks support for asset, then delegates to the adaptor implementation's internal _getPrice() path.
Read Adaptor Guard Config
getPriceGuard()
Description: Returns the configured PriceGuard for an adaptor asset and denomination.
Contract: IOracleAdaptor / BaseOracleAdaptor
Function signature:
Inputs:
address
asset
The asset whose guard should be read.
bool
inUSD
true for the USD-denominated guard, false for the native-token-denominated guard.
Return data:
PriceGuard
None
The stored guard. basePrice == 0 means disabled.
Events: None.
priceGuards()
Description: Compiler-generated getter for the BaseOracleAdaptor.priceGuards public mapping.
This is the interface-safe way to read adaptor guard data.
Contract: BaseOracleAdaptor
Function signature:
Inputs:
address
asset
The asset whose guard should be read.
bool
inUSD
true for the USD-denominated guard, false for the native-token-denominated guard.
Return data:
uint40
timestampStart
Dynamic growth start timestamp, or 0 for static guards.
uint40
ips
WAD-scaled per-second growth rate.
uint88
basePrice
Starting maximum guard price. 0 means disabled.
uint88
minPrice
Starting minimum guard price.
Events: None.
Prefer getPriceGuard() when integrating through IOracleAdaptor.
Read Combined Aggregator Guard
pg()
Description: Compiler-generated getter for the CombinedAggregator.pg public struct.
Contract: CombinedAggregator
Function signature:
Inputs: None.
Return data:
uint40
timestampStart
Dynamic growth start timestamp, or 0 for static guards.
uint40
ips
WAD-scaled per-second growth rate.
uint88
basePrice
Starting maximum guard price. 0 means disabled.
uint88
minPrice
Starting minimum guard price.
Events: None.
This getter returns struct fields as separate return values.
Guarded Combined Price Reads
latestRoundData()
Description: Returns the latest combined aggregator round data with the secondary guard applied to the secondary answer.
Contract: CombinedAggregator
Function signature:
Inputs: None.
Return data:
uint80
roundId
Round ID from the primary aggregator.
int256
answer
Combined answer after applying the secondary guard.
uint256
startedAt
Timestamp from the primary aggregator.
uint256
updatedAt
Timestamp from the primary aggregator, or 0 when the secondary feed is stale.
uint80
answeredInRound
Round value from the primary aggregator.
Events: None.
Round metadata comes from the primary feed and should not be used as proof that the secondary feed is fresh.
getAdjustedAnswer()
getAdjustedAnswer()Description: Applies the secondary feed and its guard to an input primary answer.
Contract: CombinedAggregator
Function signature:
Inputs:
int256
answer
Primary answer to combine with the guarded secondary answer.
Return data:
int256
result
Combined adjusted answer, or 0 when the secondary feed is stale or the guard returns 0.
Events: None.
This is a read-only helper used to apply the same secondary guard behavior outside latestRoundData().
Last updated
Was this helpful?