For the complete documentation index, see llms.txt. This page is also available as Markdown.

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 minPrice and basePrice values.

  • Dynamic guards increase both bounds over time from timestampStart using ips, 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.

Field
Meaning

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:

  1. The caller must pass the market-permission check.

  2. If ips == 0, timestampStart must be 0.

  3. If ips > 0, timestampStart must be nonzero, not in the future, and at least seven days before the current block timestamp.

  4. ips must fit in uint40.

  5. basePrice must be nonzero and fit in uint88.

  6. minPrice must be less than or equal to basePrice.

  7. The asset must price successfully for the requested denomination through both getPrice(asset, inUSD, false) and getPrice(asset, inUSD, true).

  8. The returned price denomination must match the requested inUSD value.

  9. The guarded current minimum cannot be above the current price used in the final validation read.

  10. If an existing dynamic guard has a later timestampStart, a new nonzero timestampStart cannot 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:

  1. The caller must pass the market-permission check.

  2. secondaryAggregator.latestRoundData() must return a positive answer.

  3. The secondary feed update must be inside secondaryHeartbeat.

  4. The guarded current minimum cannot be above the current secondary answer.

  5. If an existing dynamic guard has a later timestampStart, a new nonzero timestampStart cannot 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:

  1. If price < minPrice, the guard returns 0.

  2. If price > basePrice, the guard returns basePrice.

  3. 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:

  1. The dynamic minimum is calculated from minPrice.

  2. If the observed price is below the dynamic minimum, the guard returns 0.

  3. The dynamic maximum is calculated from basePrice.

  4. If the observed price is above the dynamic maximum, the guard returns the dynamic maximum.

  5. 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:

Type
Name
Description

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:

Type
Name
Description

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:

Type
Name
Description

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:

Type
Name
Description

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:

Type
Name
Description

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:

Type
Name
Description

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:

Type
Name
Description

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:

Type
Name
Description

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()

Description: Applies the secondary feed and its guard to an input primary answer.

Contract: CombinedAggregator

Function signature:

Inputs:

Type
Name
Description

int256

answer

Primary answer to combine with the guarded secondary answer.

Return data:

Type
Name
Description

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?