> 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/adaptors.md).

# Adaptors

### Overview

Oracle adaptors are the contract boundary between Curvance markets and external price sources. The `OracleManager` calls an adaptor through the `IOracleAdaptor` interface, receives a normalized `PricingResult`, and decides how that price should be consumed by market logic.

The contracts in this page cover three related roles:

* **`BaseOracleAdaptor`:** shared support, permission checks, price guards, normalization, and asset removal.
* **Direct feed adaptors:** `ChainlinkAdaptor` and `RedstoneClassicAdaptor`, which read round-based feeds and normalize answers.
* **Derived asset adaptors and wrapped aggregators:** Pendle token adaptors and feed-compatible wrappers that turn an existing feed into a price for a related asset.

Real asset and feed addresses are deployment-specific. Use the deployment registry for the chain you are integrating with instead of hardcoding example addresses.

### Adaptor Categories

#### Direct feed adaptors

`ChainlinkAdaptor` and `RedstoneClassicAdaptor` configure one feed per asset and denomination. A denomination is selected by the `inUSD` flag:

* `true`: the feed prices the asset in USD.
* `false`: the feed prices the asset in the deployment's native token.

If the requested denomination is not configured, each direct feed adaptor checks the opposite denomination and returns the actual denomination in `PricingResult.inUSD`.

#### Pendle token adaptors

`PendlePrincipalTokenAdaptor` prices Pendle Principal Tokens by combining:

* a Pendle TWAP-derived asset rate, and
* the quote asset price returned by `OracleManager.getPrice(config.quoteAsset, inUSD, getLower)`.

Both adaptors require the quote asset to already be supported by the `OracleManager`.

#### Wrapped aggregators

Wrapped aggregators expose feed-compatible functions such as `latestRoundData()` and `decimals()`. They are not `IOracleAdaptor` implementations. They are configured as aggregator inputs for a direct feed adaptor.

`BaseWrappedAggregator` reads an underlying aggregator and lets child contracts adjust the answer. The covered child contracts are:

* `CombinedAggregator`: multiplies a primary feed answer by a secondary feed answer.
* `VaultAggregator`: adjusts an underlying asset feed by an ERC4626 vault exchange rate.
* `WstETHAggregator`: adjusts an stETH feed by the current wstETH to stETH conversion.
* `PendlePTAggregator`: adjusts an underlying asset feed by a time-to-expiry Principal Token discount model.

### Shared Data Types

#### `PricingResult`

`IOracleAdaptor.PricingResult` is returned by `getPrice()`.

```solidity
struct PricingResult {
    uint256 price;
    bool inUSD;
    bool hadError;
}
```

* `price`: normalized price value.
* `inUSD`: `true` when `price` is denominated in USD, `false` when it is denominated in the deployment's native token.
* `hadError`: `true` when the adaptor could not return a usable price.

#### `PriceGuard`

`IOracleAdaptor.PriceGuard` stores optional minimum and maximum bounds for an asset price.

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

* `timestampStart`: when dynamic guard scaling starts.
* `ips`: increase per second, in WAD.
* `basePrice`: maximum guarded price before dynamic scaling.
* `minPrice`: minimum accepted price before dynamic scaling.

If `basePrice` is zero, the guard is disabled. If `ips` is zero, the guard is static and `timestampStart` must be zero. If `ips` is nonzero, `timestampStart` must be nonzero, cannot be in the future, and must be at least seven days before the current block timestamp.

### `BaseOracleAdaptor` Behavior

`BaseOracleAdaptor` stores the shared `centralRegistry`, the adaptor type, supported asset flags, and price guard configuration.

```solidity
mapping(address => bool) public isSupportedAsset;
mapping(address => mapping(bool => PriceGuard)) public priceGuards;
```

The base `getPrice()` path checks `isSupportedAsset[asset]` before calling the child adaptor's `_getPrice(asset, inUSD)`.

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

The base implementation ignores `getLower`. Pendle adaptors override `getPrice()` because they pass `getLower` through to the `OracleManager` when pricing the quote asset.

#### Price guards

Price guards are applied after the raw feed answer is normalized to WAD scale. The adjustment logic:

1. Returns the price unchanged if no guard is configured.
2. Returns zero if the price is below the configured minimum.
3. Caps the price at `basePrice` for static guards.
4. For dynamic guards, increases both `minPrice` and `basePrice` over time using `ips`.

A zero adjusted price is treated as an error by feed paths that validate the adjusted answer.

#### Asset removal

`removeAsset()` requires elevated permissions. It deletes the supported flag, wipes child-specific asset config, disables both USD and native-token price guards, calls `notifyFeedRemoval(asset)` on the `OracleManager`, and emits `AssetRemoved(asset)`.

### Chainlink and Redstone Classic Adaptors

`ChainlinkAdaptor` and `RedstoneClassicAdaptor` share the same configuration model. Each stores config by asset and denomination.

#### Chainlink `AssetConfig`

```solidity
struct AssetConfig {
    bool isConfigured;
    IChainlink aggregatorProxy;
    uint8 decimals;
    uint24 heartbeat;
}
```

`ChainlinkAdaptor.addAsset()` stores the aggregator proxy, reads its decimals, stores the heartbeat, marks the asset as supported, and emits `AssetAdded(asset, config, isUpdate)`.

```solidity
function addAsset(
    address asset,
    bool inUSD,
    address aggregatorProxy,
    uint256 heartbeat
) external;
```

#### Redstone Classic `AssetConfig`

```solidity
struct AssetConfig {
    bool isConfigured;
    IRedstone feedProxy;
    uint8 decimals;
    uint24 heartbeat;
}
```

`RedstoneClassicAdaptor.addAsset()` validates that the provided string `id` matches `IRedstone(feedProxy).getDataFeedId()` before storing the feed config.

```solidity
function addAsset(
    address asset,
    bool inUSD,
    address feedProxy,
    uint256 heartbeat,
    string memory id
) external;
```

#### Feed validation

Both direct feed adaptors:

* require elevated permissions for `addAsset()`;
* reject `address(0)` assets;
* add `HEARTBEAT_GRACE_PERIOD` to nonzero heartbeat inputs;
* reject heartbeats above `DEFAULT_HEARTBEAT`;
* call `latestRoundData()` on the configured feed;
* return `hadError = true` if the feed answer is less than or equal to zero;
* normalize the positive answer to WAD scale before applying price guards;
* set `hadError` based on stale or future-dated update timestamps.

### Pendle Token Adaptors

Pendle token adaptors compose a Pendle rate with an already supported quote asset price.

#### Pendle LP `AssetConfig`

```solidity
struct AssetConfig {
    address pt;
    uint32 twapDuration;
    address quoteAsset;
    uint8 quoteAssetDecimals;
}
```

`PendleLPTokenAdaptor.addAsset()` validates that the market's PT matches `config.pt`, the TWAP duration is at least `MINIMUM_TWAP_DURATION`, the market's standardized yield asset matches `config.quoteAsset`, the PT oracle state is usable for the TWAP duration, and the quote asset is supported by the `OracleManager`.

```solidity
function addAsset(address asset, AssetConfig memory config) external;
```

The LP price path reads `IPMarket(asset).getLpToAssetRate(config.twapDuration)`, prices `config.quoteAsset`, and returns:

```solidity
result.price = _adjustPrice(asset, inUSD, (price * lpRate) / WAD, 18);
```

#### Pendle Principal Token `AssetConfig`

```solidity
struct AssetConfig {
    IPMarket market;
    uint32 twapDuration;
    address quoteAsset;
    uint8 quoteAssetDecimals;
}
```

`PendlePrincipalTokenAdaptor.addAsset()` validates that the configured market's PT is the asset being added, the TWAP duration is at least `MINIMUM_TWAP_DURATION`, the standardized yield asset matches `config.quoteAsset`, the PT oracle state is usable for the TWAP duration, and the quote asset is supported by the `OracleManager`.

```solidity
function addAsset(address asset, AssetConfig memory config) external;
```

The Principal Token price path reads `config.market.getPtToAssetRate(config.twapDuration)`, prices `config.quoteAsset`, and returns:

```solidity
result.price = _adjustPrice(asset, inUSD, (price * ptRate) / WAD, 18);
```

For both Pendle adaptors, a quote asset pricing error sets `result.hadError = true`. A price guard adjustment to zero also sets `result.hadError = true`.

### Wrapped Aggregators

#### `BaseWrappedAggregator`

`BaseWrappedAggregator` implements the round-based feed surface expected by the direct feed adaptors. Its constructor validates that the underlying aggregator returns a positive answer, a nonzero `updatedAt`, and a nonzero round ID.

```solidity
function decimals() external view returns (uint8 result);

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

function latestRound() external view returns (uint256 result);

function getRoundData(uint80 _roundId)
    external
    view
    returns (
        uint80 roundId,
        int256 answer,
        uint256 startedAt,
        uint256 updatedAt,
        uint80 answeredInRound
    );

function getDataFeedId() external view returns (bytes32 result);

function underlyingAggregator() public view returns (IChainlink result);

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

`latestRoundData()` and `getRoundData()` read the underlying aggregator and then call `getAdjustedAnswer(answer)`.

#### `CombinedAggregator`

`CombinedAggregator` combines a primary aggregator with a secondary aggregator. It validates the secondary aggregator during construction, stores a `secondaryHeartbeat`, and multiplies the primary answer by the secondary answer divided by the secondary feed's decimal precision.

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

function getAdjustedAnswer(int256 answer)
    public
    view
    virtual
    override
    returns (int256 result);

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

function disableGuardedPriceConfig() external;

function setSecondaryHeartbeat(uint256 heartbeat) external;
```

The `CombinedAggregator` guard applies only to the secondary answer before it is multiplied into the primary answer. The primary feed is validated by the adaptor that reads the combined aggregator.

If the secondary feed is stale or future-dated, `latestRoundData()` sets `updatedAt = 0`, and `getAdjustedAnswer()` returns zero. The direct feed adaptor then treats the wrapped answer as unusable.

#### `VaultAggregator`

`VaultAggregator` adjusts an underlying asset feed by the vault exchange rate.

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

The exchange rate is read with:

```solidity
IERC4626(vault).convertToAssets(10 ** IERC4626(vault).decimals())
```

The constructor validates that the vault's `asset()` matches the configured underlying asset.

#### `WstETHAggregator`

`WstETHAggregator` is a specialized `VaultAggregator`. It validates that the wrapped token's `stETH()` address matches the configured underlying asset and uses `getStETHByWstETH(WAD)` as the exchange rate.

#### `PendlePTAggregator`

`PendlePTAggregator` applies a deterministic time-to-expiry discount to an underlying asset feed. The constructor:

* converts `_discountOneYearBPS` from BPS to WAD;
* rejects a zero discount or a discount above WAD;
* validates the Principal Token's standardized yield asset against the configured underlying asset;
* rejects configurations where the Principal Token expires more than one year after deployment.

Before expiry, the exchange rate is:

```solidity
WAD - ((timeToExpiry * _discountOneYear * WAD) / (SECONDS_PER_YEAR * WAD))
```

At or after expiry, the exchange rate is `WAD`, pricing the Principal Token one-to-one with its underlying asset.

### Data Normalization

Adaptor prices are normalized to WAD scale before they are returned to the caller. Direct feed adaptors read feed decimals from the configured feed and normalize with:

```solidity
price = FixedPointMathLib.fullMulDiv(price, WAD, 10 ** decimals);
```

Pendle token adaptors compose a WAD-scaled Pendle rate with the quote asset's WAD-scaled price, then call `_adjustPrice(..., 18)`.

Wrapped aggregators preserve the underlying feed's decimals through `decimals()` and adjust the feed answer inside `latestRoundData()` or `getAdjustedAnswer()`. When a wrapped aggregator is configured inside a direct feed adaptor, the direct feed adaptor still performs its own normalization and heartbeat validation.

### Permissions

Read functions are permissionless.

Elevated permissions from `centralRegistry.hasElevatedPermissions(msg.sender)` are required for:

* `ChainlinkAdaptor.addAsset()`
* `RedstoneClassicAdaptor.addAsset()`
* `PendleLPTokenAdaptor.addAsset()`
* `PendlePrincipalTokenAdaptor.addAsset()`
* `BaseOracleAdaptor.removeAsset()`

Market permissions from `centralRegistry.hasMarketPermissions(msg.sender)` are required for:

* `BaseOracleAdaptor.setGuardedPriceConfig()`
* `BaseOracleAdaptor.disableGuardedPriceConfig()`
* `CombinedAggregator.setGuardedPriceConfig()`
* `CombinedAggregator.disableGuardedPriceConfig()`
* `CombinedAggregator.setSecondaryHeartbeat()`

### Integration Considerations

* Treat `hadError = true` as an unusable price result.
* Check `PricingResult.inUSD` instead of assuming the requested denomination was returned.
* Source real adaptor, aggregator, and asset addresses from the deployment registry.
* For direct feed adaptors, configure the feed in the adaptor before registering the adaptor with the `OracleManager`.
* For Pendle adaptors, ensure the quote asset is already supported by the `OracleManager`.
* For Pendle TWAPs, call the relevant Pendle observation setup before adding the asset if the PT oracle reports that increased cardinality is required.
* For wrapped aggregators, remember that the wrapper is read by a direct feed adaptor. The wrapper is not registered as an `IOracleAdaptor`.
* For `CombinedAggregator`, configure any secondary-answer guard on the combined aggregator itself and any final composed-price guard on the direct feed adaptor that reads it.

## User Interaction Functions

### Shared adaptor reads

#### **getPrice()**

**Description:** Prices a supported asset and returns the normalized price result.

**Contract:** `IOracleAdaptor`, implemented by `BaseOracleAdaptor` and overridden by the Pendle adaptors.

**Function signature:**

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

**Inputs:**

<table><thead><tr><th width="113">Type</th><th width="108">Name</th><th>Description</th></tr></thead><tbody><tr><td><code>address</code></td><td><code>asset</code></td><td>Asset to price.</td></tr><tr><td><code>bool</code></td><td><code>inUSD</code></td><td>Requested denomination.</td></tr><tr><td><code>bool</code></td><td><code>getLower</code></td><td>Selector passed through by Pendle adaptors when pricing quote assets.</td></tr></tbody></table>

**Return data:**

<table><thead><tr><th width="173">Type</th><th>Description</th></tr></thead><tbody><tr><td><code>PricingResult</code></td><td>Price, denomination flag, and error flag.</td></tr></tbody></table>

**Events:** None.

{% hint style="info" %}
Reverts if `asset` is not supported. The base implementation ignores `getLower`.
{% endhint %}

#### **getPriceGuard()**

**Description:** Reads the configured price guard for an asset and denomination.

**Contract:** `BaseOracleAdaptor`

**Function signature:**

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

**Inputs:**

<table><thead><tr><th width="99">Type</th><th width="107">Name</th><th>Description</th></tr></thead><tbody><tr><td><code>address</code></td><td><code>asset</code></td><td>Asset to inspect.</td></tr><tr><td><code>bool</code></td><td><code>inUSD</code></td><td>Denomination of the guard to inspect.</td></tr></tbody></table>

**Return data:**

<table><thead><tr><th width="151">Type</th><th width="128">Name</th><th>Description</th></tr></thead><tbody><tr><td><code>PriceGuard</code></td><td><code>result</code></td><td>Current guard fields.</td></tr></tbody></table>

**Events:** None.

#### **isSupportedAsset()**

**Description:** Returns whether the adaptor currently supports an asset.

**Contract:** `IOracleAdaptor`, implemented by `BaseOracleAdaptor`

**Function signature:**

```solidity
function isSupportedAsset(address asset) external view returns (bool);
```

**Inputs:**

<table><thead><tr><th width="135">Type</th><th width="100">Name</th><th>Description</th></tr></thead><tbody><tr><td><code>address</code></td><td><code>asset</code></td><td>Asset to inspect.</td></tr></tbody></table>

**Return data:**

<table><thead><tr><th width="100">Type</th><th>Description</th></tr></thead><tbody><tr><td><code>bool</code></td><td>Whether the adaptor supports the asset.</td></tr></tbody></table>

**Events:** None.

#### **adaptorType()**

**Description:** Returns the adaptor type value derived from the adaptor name.

**Contract:** `BaseOracleAdaptor`

**Function signature:**

```solidity
function adaptorType() external view returns (uint256 result);
```

**Inputs:** None.

**Return data:**

<table><thead><tr><th width="119">Type</th><th width="116">Name</th><th>Description</th></tr></thead><tbody><tr><td><code>uint256</code></td><td><code>result</code></td><td>Adaptor type hash value.</td></tr></tbody></table>

**Events:** None.

### Wrapped aggregator reads

#### **latestRoundData()**

**Description:** Reads the underlying aggregator and returns the adjusted answer.

**Contract:** `BaseWrappedAggregator`, overridden by `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="140">Type</th><th width="164">Name</th><th>Description</th></tr></thead><tbody><tr><td><code>uint80</code></td><td><code>roundId</code></td><td>Round ID from the underlying or primary aggregator.</td></tr><tr><td><code>int256</code></td><td><code>answer</code></td><td>Adjusted answer.</td></tr><tr><td><code>uint256</code></td><td><code>startedAt</code></td><td>Round start timestamp.</td></tr><tr><td><code>uint256</code></td><td><code>updatedAt</code></td><td>Round update timestamp.</td></tr><tr><td><code>uint80</code></td><td><code>answeredInRound</code></td><td>Round in which the answer was computed.</td></tr></tbody></table>

**Events:** None.

{% hint style="info" %}
`CombinedAggregator` combines two feeds, so round metadata should not be used as the source of correctness for the composed answer.
{% endhint %}

#### **getAdjustedAnswer()**

**Description:** Returns the adjusted answer for a supplied underlying answer.

**Contract:** `BaseWrappedAggregator` children

**Function signature:**

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

**Inputs:**

<table><thead><tr><th width="102">Type</th><th width="104">Name</th><th>Description</th></tr></thead><tbody><tr><td><code>int256</code></td><td><code>answer</code></td><td>Underlying feed answer to adjust.</td></tr></tbody></table>

**Return data:**

<table><thead><tr><th width="113">Type</th><th width="116">Name</th><th>Description</th></tr></thead><tbody><tr><td><code>int256</code></td><td><code>result</code></td><td>Adjusted answer.</td></tr></tbody></table>

**Events:** None.

Notes:

{% hint style="info" %}
Child contracts define the exchange-rate or composition logic.
{% endhint %}

#### **underlyingAggregator()**

**Description:** Returns the underlying aggregator read by the wrapper.

**Contract:** `BaseWrappedAggregator`

**Function signature:**

```solidity
function underlyingAggregator() public view returns (IChainlink result);
```

**Inputs:** None.

**Return data:**

<table><thead><tr><th width="145">Type</th><th width="113">Name</th><th>Description</th></tr></thead><tbody><tr><td><code>IChainlink</code></td><td><code>result</code></td><td>Underlying aggregator.</td></tr></tbody></table>

**Events:** None.
