> For the complete documentation index, see [llms.txt](https://docs.curvance.com/app/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.curvance.com/app/developer-docs/oracles/price-guard.md).

# 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.

```solidity
struct PriceGuard {
    uint40 timestampStart;
    uint40 ips;
    uint88 basePrice;
    uint88 minPrice;
}
```

<table><thead><tr><th width="201">Field</th><th>Meaning</th></tr></thead><tbody><tr><td><code>timestampStart</code></td><td>The timestamp used as the dynamic growth start. Static guards require this to be <code>0</code>.</td></tr><tr><td><code>ips</code></td><td>Increase per second, WAD-scaled. <code>0</code> means the guard is static.</td></tr><tr><td><code>basePrice</code></td><td>The starting maximum price. For static guards, this is the fixed maximum. <code>0</code> means the guard is disabled.</td></tr><tr><td><code>minPrice</code></td><td>The starting minimum price. For static guards, this is the fixed minimum.</td></tr></tbody></table>

### 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:

```solidity
guarded = price * ((timePassed * ips) + WAD) / WAD;
```

`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:

```solidity
function setGuardedPriceConfig(
    address asset,
    bool inUSD,
    uint256 timestampStart,
    uint256 ips,
    uint256 basePrice,
    uint256 minPrice
) external;
```

Adaptor guards are disabled per `(asset, inUSD)` pair:

```solidity
function disableGuardedPriceConfig(address asset, bool inUSD) external;
```

Combined aggregator guards are configured once for the secondary feed:

```solidity
function setGuardedPriceConfig(
    uint256 timestampStart,
    uint256 ips,
    uint256 basePrice,
    uint256 minPrice
) external;
```

Combined aggregator guards are disabled by deleting `pg`:

```solidity
function disableGuardedPriceConfig() external;
```

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:**

```solidity
function getPrice(
    address asset,
    bool inUSD,
    bool getLower
) external view returns (PricingResult memory);
```

**Inputs:**

<table><thead><tr><th width="108">Type</th><th width="99">Name</th><th>Description</th></tr></thead><tbody><tr><td><code>address</code></td><td><code>asset</code></td><td>The asset to price.</td></tr><tr><td><code>bool</code></td><td><code>inUSD</code></td><td><code>true</code> for a USD-denominated price, <code>false</code> for a native-token-denominated price.</td></tr><tr><td><code>bool</code></td><td><code>getLower</code></td><td>Selector used by adaptor implementations that support multiple price paths.</td></tr></tbody></table>

**Return data:**

<table><thead><tr><th width="170">Type</th><th width="91">Name</th><th>Description</th></tr></thead><tbody><tr><td><code>PricingResult</code></td><td>None</td><td>Struct containing <code>price</code>, <code>inUSD</code>, and <code>hadError</code>.</td></tr></tbody></table>

**Events:** None.

{% hint style="info" %}
`OracleAdaptor.getPrice()` checks support for `asset`, then delegates to the adaptor implementation's internal `_getPrice()` path.
{% endhint %}

### Read Adaptor Guard Config

#### **getPriceGuard()**

**Description:** Returns the configured `PriceGuard` for an adaptor asset and denomination.

**Contract:** `IOracleAdaptor` / `BaseOracleAdaptor`

**Function signature:**

```solidity
function getPriceGuard(
    address asset,
    bool inUSD
) external view returns (PriceGuard memory);
```

**Inputs:**

<table><thead><tr><th width="109">Type</th><th width="97">Name</th><th>Description</th></tr></thead><tbody><tr><td><code>address</code></td><td><code>asset</code></td><td>The asset whose guard should be read.</td></tr><tr><td><code>bool</code></td><td><code>inUSD</code></td><td><code>true</code> for the USD-denominated guard, <code>false</code> for the native-token-denominated guard.</td></tr></tbody></table>

**Return data:**

<table><thead><tr><th width="132">Type</th><th width="87">Name</th><th>Description</th></tr></thead><tbody><tr><td><code>PriceGuard</code></td><td>None</td><td>The stored guard. <code>basePrice == 0</code> means disabled.</td></tr></tbody></table>

**Events:** None.

#### **priceGuards()**

**Description:** Compiler-generated getter for the `BaseOracleAdaptor.priceGuards` public mapping.

{% hint style="info" %}
This is the interface-safe way to read adaptor guard data.
{% endhint %}

**Contract:** `BaseOracleAdaptor`

**Function signature:**

```solidity
function priceGuards(
    address asset,
    bool inUSD
)
    external
    view
    returns (
        uint40 timestampStart,
        uint40 ips,
        uint88 basePrice,
        uint88 minPrice
    );
```

**Inputs:**

<table><thead><tr><th width="117">Type</th><th width="102">Name</th><th>Description</th></tr></thead><tbody><tr><td><code>address</code></td><td><code>asset</code></td><td>The asset whose guard should be read.</td></tr><tr><td><code>bool</code></td><td><code>inUSD</code></td><td><code>true</code> for the USD-denominated guard, <code>false</code> for the native-token-denominated guard.</td></tr></tbody></table>

**Return data:**

<table><thead><tr><th width="107">Type</th><th width="170">Name</th><th>Description</th></tr></thead><tbody><tr><td><code>uint40</code></td><td><code>timestampStart</code></td><td>Dynamic growth start timestamp, or <code>0</code> for static guards.</td></tr><tr><td><code>uint40</code></td><td><code>ips</code></td><td>WAD-scaled per-second growth rate.</td></tr><tr><td><code>uint88</code></td><td><code>basePrice</code></td><td>Starting maximum guard price. <code>0</code> means disabled.</td></tr><tr><td><code>uint88</code></td><td><code>minPrice</code></td><td>Starting minimum guard price.</td></tr></tbody></table>

**Events**: None.

{% hint style="info" %}
Prefer `getPriceGuard()` when integrating through `IOracleAdaptor`.
{% endhint %}

### Read Combined Aggregator Guard

### **pg()**

**Description:** Compiler-generated getter for the `CombinedAggregator.pg` public struct.

**Contract:** `CombinedAggregator`

**Function signature:**

```solidity
function pg()
    external
    view
    returns (
        uint40 timestampStart,
        uint40 ips,
        uint88 basePrice,
        uint88 minPrice
    );
```

**Inputs:** None.

**Return data:**

<table><thead><tr><th width="117">Type</th><th width="174">Name</th><th>Description</th></tr></thead><tbody><tr><td><code>uint40</code></td><td><code>timestampStart</code></td><td>Dynamic growth start timestamp, or <code>0</code> for static guards.</td></tr><tr><td><code>uint40</code></td><td><code>ips</code></td><td>WAD-scaled per-second growth rate.</td></tr><tr><td><code>uint88</code></td><td><code>basePrice</code></td><td>Starting maximum guard price. <code>0</code> means disabled.</td></tr><tr><td><code>uint88</code></td><td><code>minPrice</code></td><td>Starting minimum guard price.</td></tr></tbody></table>

**Events:** None.

{% hint style="info" %}
This getter returns struct fields as separate return values.
{% endhint %}

### 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:**

```solidity
function latestRoundData()
    external
    view
    returns (
        uint80 roundId,
        int256 answer,
        uint256 startedAt,
        uint256 updatedAt,
        uint80 answeredInRound
    );
```

**Inputs:** None.

**Return data:**

<table><thead><tr><th width="114">Type</th><th width="170">Name</th><th>Description</th></tr></thead><tbody><tr><td><code>uint80</code></td><td><code>roundId</code></td><td>Round ID from the primary aggregator.</td></tr><tr><td><code>int256</code></td><td><code>answer</code></td><td>Combined answer after applying the secondary guard.</td></tr><tr><td><code>uint256</code></td><td><code>startedAt</code></td><td>Timestamp from the primary aggregator.</td></tr><tr><td><code>uint256</code></td><td><code>updatedAt</code></td><td>Timestamp from the primary aggregator, or <code>0</code> when the secondary feed is stale.</td></tr><tr><td><code>uint80</code></td><td><code>answeredInRound</code></td><td>Round value from the primary aggregator.</td></tr></tbody></table>

**Events:** None.

{% hint style="info" %}
Round metadata comes from the primary feed and should not be used as proof that the secondary feed is fresh.
{% endhint %}

#### **`getAdjustedAnswer()`**

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

**Contract:** `CombinedAggregator`

**Function signature:**

```solidity
function getAdjustedAnswer(
    int256 answer
) public view returns (int256 result);
```

**Inputs:**

<table><thead><tr><th width="115">Type</th><th width="97">Name</th><th>Description</th></tr></thead><tbody><tr><td><code>int256</code></td><td><code>answer</code></td><td>Primary answer to combine with the guarded secondary answer.</td></tr></tbody></table>

**Return data:**

<table><thead><tr><th width="95">Type</th><th width="95">Name</th><th>Description</th></tr></thead><tbody><tr><td><code>int256</code></td><td><code>result</code></td><td>Combined adjusted answer, or <code>0</code> when the secondary feed is stale or the guard returns <code>0</code>.</td></tr></tbody></table>

**Events:** None.

{% hint style="info" %}
This is a read-only helper used to apply the same secondary guard behavior outside `latestRoundData()`.
{% endhint %}
