Diffusal

DiffusalOracle

Unified oracle providing volatility, premium, and aggregated price data

The DiffusalOracle contract serves as the central source of truth for all market data in the Diffusal protocol. It aggregates prices from multiple sources (Pyth Network, Chainlink, and native prices), stores implied volatility and premium rates per trading pair, and provides the global risk-free rate for Black-Scholes calculations.


Overview

The oracle provides these categories of data:

CategoryPurposePrecision
Price DataSpot prices for underlying assets8 decimals (int64)
VolatilityImplied volatility per trading pair8 decimals (50% = 50_000_000)
PremiumPremium rate per trading pair8 decimals
Risk-Free RateGlobal interest rate for Black-Scholes8 decimals (5% = 5_000_000)
Tick DecimalsDynamic tick precision based on spot priceuint8 (2-12)

The oracle uses a most-recent-wins price selection strategy. When multiple price sources have data, the most recently updated price is used. When timestamps are equal, priority is: Native > Chainlink > Pyth.


Key Concepts

Price Aggregation

The oracle queries up to three price sources for each pair:

  1. Native Prices (Primary) — Admin-set prices for protocol control
  2. Chainlink (Secondary) — Decentralized oracle network
  3. Pyth Network (Tertiary) — High-frequency, low-latency price feeds
┌──────────────────────────────────────┐
│  1. Query all configured sources     │
└───────────────────┬──────────────────┘

┌──────────────────────────────────────┐
│  2. Compare timestamps               │
└───────────────────┬──────────────────┘

┌──────────────────────────────────────┐
│  3. Return most recent price         │
└───────────────────┬──────────────────┘

┌──────────────────────────────────────┐
│  4. On timestamp tie:                │
│     Native > Chainlink > Pyth        │
└──────────────────────────────────────┘

Pair Identification

Trading pairs are identified by the keccak256 hash of their name string:

bytes32 pairId = keccak256(bytes("ETH-USDC"));

The oracle maintains a registry of pair names and their associated feed configurations.

Precision Model

All values use 8 decimal precision (matching Pyth/Chainlink standards):

ValueExampleRepresentation
$3,000.00ETH price300_000_000_000 with expo=-8
50%Volatility50_000_000
5%Risk-free rate5_000_000

Storage & State

The contract uses ERC-7201 namespaced storage to prevent storage collisions:

/// @custom:storage-location erc7201:diffusal.storage.DiffusalOracle
struct DiffusalOracleStorage {
    address owner;
    mapping(address => bool) admins;
    mapping(bytes32 pairId => int64) volatilities;
    mapping(bytes32 pairId => int64) premiums;
    int64 riskFreeRate;
    address pythOracle;
    mapping(bytes32 pairId => Price) nativePrices;
    mapping(bytes32 nameHash => bytes32) pythFeeds;
    mapping(bytes32 nameHash => address) chainlinkFeeds;
    string[] registeredPairs;
    mapping(bytes32 nameHash => bool) _isRegistered;
}

Price Struct

struct Price {
    int64 price;       // Price value
    uint64 conf;       // Confidence interval
    int32 expo;        // Exponent (typically -8)
    uint256 publishTime; // Timestamp of price update
}

External Functions

Price Reading

getPriceUnsafe

Returns the most recent price without staleness checks.

function getPriceUnsafe(bytes32 pairId) external view returns (Price memory priceData)
ParameterTypeDescription
pairIdbytes32Trading pair identifier

Returns: Price struct with price, confidence, exponent, and publish time.

Access: Public (view)


getPriceNoOlderThan

Returns price only if it was updated within the specified age.

function getPriceNoOlderThan(bytes32 pairId, uint256 age) external view returns (Price memory priceData)
ParameterTypeDescription
pairIdbytes32Trading pair identifier
ageuint256Maximum age in seconds

Reverts: StalePrice if price is older than age seconds.

Access: Public (view)


getPrice

Returns price components separately.

function getPrice(bytes32 pairId) external view returns (int64 price, int32 expo, uint256 publishTime)

Access: Public (view)


Volatility & Parameters

getVolatility

Returns the implied volatility for a trading pair.

function getVolatility(bytes32 pairId) external view returns (int64 volatility)

Returns: Volatility with 8 decimals (50% = 50_000_000)


getPremium

Returns the premium rate for a trading pair.

function getPremium(bytes32 pairId) external view returns (int64 premium)

getRiskFreeRate

Returns the global risk-free interest rate.

function getRiskFreeRate() external view returns (int64 rate)

Returns: Rate with 8 decimals (5% = 5_000_000)


getTickDecimals

Returns the tick decimal precision for a trading pair based on the current spot price. Higher-priced assets use fewer decimals for larger tick sizes.

function getTickDecimals(bytes32 pairId) external view returns (uint8 tickDecimals)
ParameterTypeDescription
pairIdbytes32Trading pair identifier

Returns: Tick decimal precision (range: 2-12)

Dynamic Scaling:

Spot PriceTick DecimalsTick Size
>= $10020.01
>= $1030.001
>= $140.0001
>= $0.150.00001
>= $0.0160.000001
< $0.017-12Finer precision

Pair Management

getPairId

Generates a pair ID from asset symbols.

function getPairId(string calldata base, string calldata quote) external view returns (bytes32 pairId)
ParameterTypeDescription
basestringBase asset (e.g., "ETH") or full pair name
quotestringQuote asset (e.g., "USD") or empty string

Example:

bytes32 pairId = oracle.getPairId("ETH", "USDC");  // Returns hash of "ETH-USDC"
bytes32 pairId = oracle.getPairId("ETH-USDC", ""); // Same result

getRegisteredPairs

Returns all registered trading pair names.

function getRegisteredPairs() external view returns (string[] memory pairs)

isPairRegistered

Checks if a pair is registered in the oracle.

function isPairRegistered(string calldata pairName) external view returns (bool isRegistered)

getRegisteredPairCount

Returns the total number of registered trading pairs.

function getRegisteredPairCount() external view returns (uint256 count)

getPythFeedId

Returns the Pyth feed ID configured for a trading pair.

function getPythFeedId(string calldata pairName) external view returns (bytes32 feedId)

Returns: The Pyth feed ID (bytes32(0) if not configured)


getChainlinkFeedAddress

Returns the Chainlink feed address configured for a trading pair.

function getChainlinkFeedAddress(string calldata pairName) external view returns (address feedAddress)

Returns: The Chainlink feed address (address(0) if not configured)


isAdmin

Checks if an address has admin privileges.

function isAdmin(address account) external view returns (bool)

Returns: true if the account has admin privileges, false otherwise.


Storage Getters

The following functions provide direct access to internal storage state:

volatilities

Returns the stored volatility for a pair.

function volatilities(bytes32 pairId) external view returns (int64)

premiums

Returns the stored premium rate for a pair.

function premiums(bytes32 pairId) external view returns (int64)

riskFreeRate

Returns the stored global risk-free rate.

function riskFreeRate() external view returns (int64)

nativePrices

Returns a native price override for a pair (if set).

function nativePrices(bytes32 pairId) external view returns (int64 price)

pythFeeds

Returns the Pyth feed ID for a pair (by name hash).

function pythFeeds(bytes32 nameHash) external view returns (bytes32 feedId)

chainlinkFeeds

Returns the Chainlink feed address for a pair (by name hash).

function chainlinkFeeds(bytes32 nameHash) external view returns (address)

registeredPairs

Returns a registered pair name by index.

function registeredPairs(uint256 index) external view returns (string memory)

Admin Functions

These functions require the onlyAdmin modifier.

Parameter Setting

setVolatility

Sets implied volatility for a trading pair.

function setVolatility(bytes32 pairId, int64 volatility) external onlyAdmin

Emits: VolatilityUpdated(pairId, volatility)


setVolatilityBatch

Sets volatility for multiple pairs in one transaction.

function setVolatilityBatch(bytes32[] calldata pairIds, int64[] calldata volatilityValues) external onlyAdmin

setPremium

Sets premium rate for a trading pair.

function setPremium(bytes32 pairId, int64 premium) external onlyAdmin

Emits: PremiumUpdated(pairId, premium)


setPremiumBatch

Sets premium rates for multiple pairs in one transaction.

function setPremiumBatch(bytes32[] calldata pairIds, int64[] calldata premiumValues) external onlyAdmin

Emits: PremiumUpdated for each pair


setRiskFreeRate

Sets the global risk-free rate.

function setRiskFreeRate(int64 _riskFreeRate) external onlyAdmin

Emits: RiskFreeRateUpdated(oldRate, newRate)


Feed Configuration

setPythFeed

Configures the Pyth feed ID for a trading pair.

function setPythFeed(string calldata pairName, bytes32 feedId) external onlyAdmin

Emits: PythFeedSet(pairName, feedId)

Note: Automatically registers the pair if not already registered.


setChainlinkFeed

Configures the Chainlink feed address for a trading pair.

function setChainlinkFeed(string calldata pairName, address feedAddress) external onlyAdmin

Emits: ChainlinkFeedSet(pairName, feedAddress)


setPair

Configures both Pyth and Chainlink feeds for a pair in one call.

function setPair(
    string calldata pairName,
    bytes32 pythFeedId,
    address chainlinkFeedAddress
) external onlyAdmin

Example:

oracle.setPair(
    "ETH-USDC",
    0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace, // Pyth ETH/USD
    0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419  // Chainlink ETH/USD
);

removePair

Removes a trading pair from the registry.

function removePair(string calldata pairName) external onlyAdmin

Emits: PairRemoved(pairName)


Native Price Setting

setPrice

Sets a native (fallback) price for a trading pair.

function setPrice(bytes32 pairId, int64 price) external onlyAdmin

Note: Uses default exponent (-8) and zero confidence.


setPriceWithConf

Sets a native price with confidence interval.

function setPriceWithConf(bytes32 pairId, int64 price, uint64 conf) public onlyAdmin

setPriceFull

Sets a native price with full parameters.

function setPriceFull(bytes32 pairId, int64 price, uint64 conf, int32 expo) external onlyAdmin

setPriceBatch

Sets native prices for multiple pairs in one transaction.

function setPriceBatch(bytes32[] calldata pairIds, int64[] calldata priceValues) external onlyAdmin

Note: Uses default exponent (-8) and zero confidence for all prices.

Emits: PriceUpdated for each pair


setPairBatch

Configures Pyth and Chainlink feeds for multiple pairs in one transaction.

function setPairBatch(
    string[] calldata pairNames,
    bytes32[] calldata pythFeedIds,
    address[] calldata chainlinkAddresses
) external onlyAdmin
ParameterTypeDescription
pairNamesstring[]Array of pair names
pythFeedIdsbytes32[]Pyth feed IDs (bytes32(0) to skip)
chainlinkAddressesaddress[]Chainlink addresses (address(0) to skip)

Note: Use zero values to skip configuring a specific feed type.


Owner Functions

These functions require the onlyOwner modifier.

Two-Step Ownership Transfer

The contract uses a two-step ownership transfer pattern for safety.

transferOwnership

Initiates ownership transfer to a new address. The new owner must call acceptOwnership() to complete the transfer.

function transferOwnership(address newOwner) external onlyOwner

Emits: OwnershipTransferStarted(currentOwner, newOwner)

Reverts: ZeroAddress if newOwner is zero address.


pendingOwner

Returns the address of the pending owner during a two-step transfer.

function pendingOwner() external view returns (address pendingOwnerAddress)

Returns: The pending owner address (or address(0) if no transfer pending)


acceptOwnership

Completes the ownership transfer. Must be called by the pending owner.

function acceptOwnership() external

Emits: OwnershipTransferred(oldOwner, newOwner)

Reverts: NotPendingOwner if caller is not the pending owner.


Admin Management

addAdmin

Grants admin privileges to an address.

function addAdmin(address admin) external onlyOwner

Reverts: AdminAlreadyExists if already an admin.

Emits: AdminAdded(admin)


removeAdmin

Revokes admin privileges.

function removeAdmin(address admin) external onlyOwner

Reverts: AdminDoesNotExist if not an admin.

Emits: AdminRemoved(admin)


setPythOracle

Updates the Pyth oracle contract address.

function setPythOracle(address _pythOracle) external onlyOwner

Emits: PythOracleUpdated(oldOracle, newOracle)


withdrawEther

Withdraws ETH from the contract. Useful for recovering accidentally sent ETH or reclaiming Pyth update fees.

function withdrawEther(address payable to, uint256 amount) external onlyOwner nonReentrant
ParameterTypeDescription
toaddress payableRecipient address
amountuint256Amount of ETH to withdraw

Security: Protected by nonReentrant modifier to prevent reentrancy attacks.

Reverts:

  • ZeroAddress if to is zero address
  • InsufficientBalance if contract balance is less than amount
  • TransferFailed if the ETH transfer fails

IPyth-Compatible Functions

These functions provide compatibility with the Pyth Network interface. In the current implementation, they are no-ops or return default values for testing purposes.

updatePriceFeeds

Updates price feeds (IPyth-compatible). Currently a no-op for testing.

function updatePriceFeeds(bytes[] calldata updateData) external payable

Note: In production Pyth, this requires payment. For testing, it's free and does nothing. Prices are set manually via setPrice().


getUpdateFee

Returns the fee required to update price feeds.

function getUpdateFee(bytes[] calldata updateData) external pure returns (uint256 fee)

Returns: Always 0 for testing. In production Pyth, this returns the actual fee.


getValidTimePeriod

Returns the valid time period for prices.

function getValidTimePeriod() external pure returns (uint256 validTimePeriod)

Returns: 60 seconds (default for testing)


Events

EventParametersDescription
OwnershipTransferStartedpreviousOwner, newOwnerTwo-step transfer initiated
OwnershipTransferredpreviousOwner, newOwnerOwnership transfer completed
AdminAddedadminAdmin privileges granted
AdminRemovedadminAdmin privileges revoked
VolatilityUpdatedpairId, volatilityVolatility changed for pair
PremiumUpdatedpairId, premiumPremium changed for pair
RiskFreeRateUpdatedoldRate, newRateGlobal rate changed
PriceUpdatedpairId, price, conf, expo, publishTimeNative price updated
PythOracleUpdatedoldOracle, newOraclePyth address changed
PythFeedSetpairId (indexed), feedId (indexed), pairNamePyth feed configured
ChainlinkFeedSetpairId (indexed), feedAddress (indexed), pairNameChainlink feed configured
PairRemovedpairId (indexed), pairNamePair removed from registry

Integration Points

Depends On

ContractPurpose
Pyth NetworkTertiary price feed source
ChainlinkSecondary price feed source

Used By

ContractPurpose
DiffusalOptionsQuoterGets spot price, volatility, premium, rate for pricing
DiffusalOptionsOrderBookGets tick decimals for dynamic pricing precision
DiffusalLiquidationEngineGets volatility for penalty rate calculation
DiffusalSettlementReadinessLiquidatorGets volatility for penalty rate calculation
DiffusalPriceHistoryGets spot price for TWAP snapshots

Security Considerations

Access Control

Function CategoryAccess Level
View functionsPublic
Parameter settingAdmin only
Feed configurationAdmin only
Ownership transferOwner only
Admin managementOwner only

Price Staleness

The contract provides getPriceNoOlderThan() to enforce freshness requirements. Consumers should specify appropriate staleness thresholds based on their use case:

  • Liquidations: Stricter freshness (e.g., 60 seconds)
  • Price display: More lenient (e.g., 300 seconds)

Trust Assumptions

ComponentTrust Level
Pyth NetworkExternal dependency — prices could be manipulated if Pyth is compromised
ChainlinkExternal dependency — secondary fallback
Admin addressesTrusted to set reasonable volatility/premium values
Native pricesShould only be used for testing or emergency

Precision Boundaries

When converting oracle prices to WAD format for use in Black-Scholes:

// Oracle returns: price=300_000_000_000, expo=-8
// This represents $3000.00000000

// Convert to WAD (1e18):
uint256 wadPrice = uint256(price.price) * 10**(18 + uint256(int256(price.expo)));
// Result: 3000e18

Warning: Be careful with exponent handling. A wrong conversion can cause severe pricing errors.


Code Reference

Source: packages/contracts/src/DiffusalOracle.sol

Interface: packages/contracts/src/interfaces/IDiffusalOracle.sol

Key Constants

// From Constants.sol
uint256 constant ORACLE_DECIMALS = 8;
int32 constant DEFAULT_EXPO = -8;

On this page