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:
ChainlinkAdaptorandRedstoneClassicAdaptor, 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
PricingResultIOracleAdaptor.PricingResult is returned by getPrice().
price: normalized price value.inUSD:truewhenpriceis denominated in USD,falsewhen it is denominated in the deployment's native token.hadError:truewhen the adaptor could not return a usable price.
PriceGuard
PriceGuardIOracleAdaptor.PriceGuard stores optional minimum and maximum bounds for an asset price.
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 BehaviorBaseOracleAdaptor stores the shared centralRegistry, the adaptor type, supported asset flags, and price guard configuration.
The base getPrice() path checks isSupportedAsset[asset] before calling the child adaptor's _getPrice(asset, inUSD).
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:
Returns the price unchanged if no guard is configured.
Returns zero if the price is below the configured minimum.
Caps the price at
basePricefor static guards.For dynamic guards, increases both
minPriceandbasePriceover time usingips.
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
AssetConfigChainlinkAdaptor.addAsset() stores the aggregator proxy, reads its decimals, stores the heartbeat, marks the asset as supported, and emits AssetAdded(asset, config, isUpdate).
Redstone Classic AssetConfig
AssetConfigRedstoneClassicAdaptor.addAsset() validates that the provided string id matches IRedstone(feedProxy).getDataFeedId() before storing the feed config.
Feed validation
Both direct feed adaptors:
require elevated permissions for
addAsset();reject
address(0)assets;add
HEARTBEAT_GRACE_PERIODto nonzero heartbeat inputs;reject heartbeats above
DEFAULT_HEARTBEAT;call
latestRoundData()on the configured feed;return
hadError = trueif the feed answer is less than or equal to zero;normalize the positive answer to WAD scale before applying price guards;
set
hadErrorbased 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
AssetConfigPendleLPTokenAdaptor.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.
The LP price path reads IPMarket(asset).getLpToAssetRate(config.twapDuration), prices config.quoteAsset, and returns:
Pendle Principal Token AssetConfig
AssetConfigPendlePrincipalTokenAdaptor.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.
The Principal Token price path reads config.market.getPtToAssetRate(config.twapDuration), prices config.quoteAsset, and returns:
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
BaseWrappedAggregatorBaseWrappedAggregator 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.
latestRoundData() and getRoundData() read the underlying aggregator and then call getAdjustedAnswer(answer).
CombinedAggregator
CombinedAggregatorCombinedAggregator 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.
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
VaultAggregatorVaultAggregator adjusts an underlying asset feed by the vault exchange rate.
The exchange rate is read with:
The constructor validates that the vault's asset() matches the configured underlying asset.
WstETHAggregator
WstETHAggregatorWstETHAggregator 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
PendlePTAggregatorPendlePTAggregator applies a deterministic time-to-expiry discount to an underlying asset feed. The constructor:
converts
_discountOneYearBPSfrom 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:
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:
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 = trueas an unusable price result.Check
PricingResult.inUSDinstead 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:
Inputs:
address
asset
Asset to price.
bool
inUSD
Requested denomination.
bool
getLower
Selector passed through by Pendle adaptors when pricing quote assets.
Return data:
PricingResult
Price, denomination flag, and error flag.
Events: None.
Reverts if asset is not supported. The base implementation ignores getLower.
getPriceGuard()
Description: Reads the configured price guard for an asset and denomination.
Contract: BaseOracleAdaptor
Function signature:
Inputs:
address
asset
Asset to inspect.
bool
inUSD
Denomination of the guard to inspect.
Return data:
PriceGuard
result
Current guard fields.
Events: None.
isSupportedAsset()
Description: Returns whether the adaptor currently supports an asset.
Contract: IOracleAdaptor, implemented by BaseOracleAdaptor
Function signature:
Inputs:
address
asset
Asset to inspect.
Return data:
bool
Whether the adaptor supports the asset.
Events: None.
adaptorType()
Description: Returns the adaptor type value derived from the adaptor name.
Contract: BaseOracleAdaptor
Function signature:
Inputs: None.
Return data:
uint256
result
Adaptor type hash value.
Events: None.
Wrapped aggregator reads
latestRoundData()
Description: Reads the underlying aggregator and returns the adjusted answer.
Contract: BaseWrappedAggregator, overridden by CombinedAggregator
Function signature:
Inputs: None.
Return data:
uint80
roundId
Round ID from the underlying or primary aggregator.
int256
answer
Adjusted answer.
uint256
startedAt
Round start timestamp.
uint256
updatedAt
Round update timestamp.
uint80
answeredInRound
Round in which the answer was computed.
Events: None.
CombinedAggregator combines two feeds, so round metadata should not be used as the source of correctness for the composed answer.
getAdjustedAnswer()
Description: Returns the adjusted answer for a supplied underlying answer.
Contract: BaseWrappedAggregator children
Function signature:
Inputs:
int256
answer
Underlying feed answer to adjust.
Return data:
int256
result
Adjusted answer.
Events: None.
Notes:
Child contracts define the exchange-rate or composition logic.
underlyingAggregator()
Description: Returns the underlying aggregator read by the wrapper.
Contract: BaseWrappedAggregator
Function signature:
Inputs: None.
Return data:
IChainlink
result
Underlying aggregator.
Events: None.
Last updated
Was this helpful?