Oracle
The Oracle system provides two types of data to the Cap protocol: asset prices (USD values for backing assets, collateral tokens, and Cap-native tokens) and interest rates (borrow rates for vault utilization and per-Operator restaker rates). Both systems share the same adapter architecture — a central contract stores a payload per asset, and delegates the actual calculation to a stateless library via staticcall. This makes the oracle fully composable: any data source can be integrated by deploying a new adapter library.
Mechanics
Adapter Architecture
Each asset has an OracleData struct stored on-chain containing two fields:
adapter— the address of the library or contract that computes the valuepayload— ABI-encoded calldata passed to the adapter
When a price or rate is requested, the Oracle does a low-level staticcall to adapter(payload). Errors in the adapter are silently swallowed — a failed call returns zero rather than reverting. This means any data source failure falls through to the backup rather than halting the protocol.
Price Oracle: Primary and Backup
Each asset has two OracleData slots — primary and backup. getPrice attempts the primary first:
Call
primary.adapterwithprimary.payload.If the returned price is zero or the timestamp is older than
staleness[asset], fall through.Call
backup.adapterwithbackup.payload.If backup also fails or is stale, revert with
PriceError.
All prices are normalized to 8 decimals (USD). Adapters are responsible for decimal conversion.
Rate Oracle: Market and Utilization
Rates drive two separate interest streams:
Market rate — an external floor rate (e.g. Aave borrow rate). Used as a benchmark.
Utilization rate — derived from the cUSD vault's time-weighted utilization (via VaultAdapter). This is the actual rate Operators pay on vault interest.
Both are fetched via the same adapter pattern, but rate adapters use call rather than staticcall because VaultAdapter writes state (it updates the utilization index snapshot).
Additionally, two admin-set rates are stored directly:
benchmarkRate[asset]— a manual floor rate per asset (ray)restakerRate[agent]— per-Operator restaker interest rate (ray)
Architecture
PriceOracle
Abstract contract. Inherited by the central Oracle contract.
getPrice(address _asset)
getPrice(address _asset)Returns the asset's USD price (8 decimals) and the timestamp of the last update. Tries the primary adapter first; falls back to the backup if the primary returns zero or a stale price. Reverts with PriceError if both fail.
Admin Functions
setPriceOracleData(address _asset, OracleData)
Set primary price adapter + payload for an asset
setPriceBackupOracleData(address _asset, OracleData)
Set backup price adapter + payload for an asset
setStaleness(address _asset, uint256 _staleness)
Set max age (seconds) for a valid price
View Functions
priceOracleData(address)
OracleData
Primary adapter + payload for the asset
priceBackupOracleData(address)
OracleData
Backup adapter + payload for the asset
staleness(address)
uint256
Max acceptable age in seconds for the price
RateOracle
Abstract contract. Inherited by the central Oracle contract.
marketRate(address _asset)
marketRate(address _asset)Returns the current market rate for _asset in ray format (1e27 = 100% annualised). Calls the configured market adapter (e.g. AaveAdapter). Returns zero if the adapter fails.
utilizationRate(address _asset)
utilizationRate(address _asset)Returns the utilization-based interest rate for _asset in ray. Calls the configured utilization adapter (VaultAdapter). This is the rate Operators pay on borrowed vault principal.
Admin Functions
setMarketOracleData(address _asset, OracleData)
Configure the market rate adapter + payload
setUtilizationOracleData(address _asset, OracleData)
Configure the utilization rate adapter + payload
setBenchmarkRate(address _asset, uint256 _rate)
Set a manual benchmark rate floor (ray)
setRestakerRate(address _agent, uint256 _rate)
Set the restaker interest rate for an Operator (ray)
View Functions
benchmarkRate(address)
uint256 (ray)
Manually set rate floor for the asset
restakerRate(address)
uint256 (ray)
Per-Operator restaker interest rate
marketOracleData(address)
OracleData
Market adapter + payload
utilizationOracleData(address)
OracleData
Utilization adapter + payload
Data Structures
OracleData
OracleDataadapter
Contract or library address called via staticcall (prices) or call (rates)
payload
ABI-encoded calldata passed directly to the adapter
Price Adapters
ChainlinkAdapter
Reads a Chainlink price feed aggregator. Normalizes the returned value to 8 decimals regardless of the feed's native decimals.
Payload: abi.encodeCall(ChainlinkAdapter.price, (_source)) where _source is the Chainlink aggregator address.
Returns: (uint256 price, uint256 lastUpdated) — price in 8 decimals, lastUpdated from latestRoundData.
ChainlinkAdapterChained
Multiplies multiple Chainlink feeds together to derive a cross-rate. For example, to price an asset quoted in ETH: feed A/ETH × feed ETH/USD. The final price is normalized to 8 decimals; lastUpdated is the minimum across all feeds (most conservative).
Payload: abi.encodeCall(ChainlinkAdapterChained.price, (_sources)) where _sources is an array of aggregator addresses.
wstETHAdapter
Prices wstETH by reading the stETH/USD Chainlink feed and multiplying by the stETH-to-wstETH conversion ratio from the Lido contract (getPooledEthByShares(1 ether)).
Payload: abi.encodeCall(wstETHAdapter.price, (_source, _stETH)) where _source is the stETH Chainlink aggregator and _stETH is the stETH token address.
CapTokenAdapter
Prices a cUSD vault token (e.g. cUSD itself) as the weighted average of its backing assets:
Calls back into the Oracle via msg.sender to get each underlying asset's price. lastUpdated is the minimum across all underlying prices. Returns 1e8 if supply is zero.
Payload: abi.encodeCall(CapTokenAdapter.price, (_asset)) where _asset is the Cap vault token address.
StakedCapAdapter
Prices stcUSD (staked cUSD) by reading the cUSD price and multiplying by the convertToAssets ratio of the ERC4626 staking vault:
Payload: abi.encodeCall(StakedCapAdapter.price, (_asset)) where _asset is the stcUSD address.
FixedPriceOracle
A standalone contract (not a library) that always returns 1e8 (1 USD). Used for stablecoins or test deployments.
Payload: abi.encodeCall(FixedPriceOracle.price, ()) — no parameters.
UniswapV3Adapter
On-chain TWAP oracle using Uniswap V3 pool observations. Supports chained routes (e.g. TOKEN → WETH → USDC) to derive prices for any token with sufficient liquidity.
Payload: abi.encode(tokens[], pools[], twapPeriods[]) where:
tokens— ordered token route (length =pools.length + 1)pools— Uniswap V3 pool addresses for each hoptwapPeriods— TWAP window in seconds per hop
The first token in the route must already have a registered price in the Oracle (used as the base price). The adapter validates the route via validateData before the payload is stored.
Rate Adapters
AaveAdapter
Reads Aave's getReserveData to fetch the current variable borrow rate for an asset. Used as a market rate benchmark.
Payload: abi.encodeCall(AaveAdapter.rate, (_aaveDataProvider, _asset)).
VaultAdapter
Computes a utilization-based interest rate using Cap's own vault utilization history. This is the primary rate source for vault interest.
The adapter tracks a per-vault per-asset utilization snapshot (index, lastUpdate) and computes the time-weighted average utilization since the last call. The rate is determined by a two-slope model with a dynamic multiplier:
Below kink: rate =
slope0 × utilization / kink × multiplier. Multiplier decays when utilization is low.Above kink: rate =
(slope0 + slope1 × excess) × multiplier. Multiplier grows when utilization is high.
The multiplier is bounded by [minMultiplier, maxMultiplier] and adjusts at speed rate (ray per second).
Admin Functions
setSlopes(address _asset, SlopeData)
Configure the two-slope curve for an asset
setLimits(uint256 _max, uint256 _min, uint256 _rate)
Set multiplier bounds and adjustment speed
SlopeData
SlopeDataUsage Examples
1. Reading an asset price
2. Configuring a Chainlink price feed
3. Setting a restaker rate for an Operator
Last updated