Diffusal

DiffusalPriceHistory

Price snapshot storage for TWAP-based settlement

The DiffusalPriceHistory contract stores historical price snapshots in a circular buffer and calculates Time-Weighted Average Prices (TWAP) for option settlement. This enables fair settlement prices that are resistant to manipulation by using averaged prices over a 1-hour window.


Overview

Price history serves one critical purpose:

FunctionDescription
TWAP CalculationProvides manipulation-resistant settlement prices for expired options

Snapshot Architecture

┌─────────────────────────────────────────────────────────────────────────────┐
│                           CIRCULAR BUFFER                                    │
│                                                                              │
│   [0] [1] [2] [3] [4] ... [237] [238] [239]                                 │
│                    240 slots                                                 │
│                                                                              │
│   - New snapshots overwrite oldest when full                                 │
│   - 30-second minimum between snapshots                                      │
│   - 2-hour buffer duration (240 x 30s)                                       │
└─────────────────────────────────────┬───────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────────┐
│                             TWAP WINDOW                                      │
│                                                                              │
│   ┌─────────────────────────────────────────────────────────────────────┐   │
│   │                    Buffer: 2 hours total                             │   │
│   └─────────────────────────────────┬───────────────────────────────────┘   │
│                                      ▼                                       │
│   ┌─────────────────────────────────────────────────────────────────────┐   │
│   │              TWAP Window: 1 hour (at settlement)                     │   │
│   └─────────────────────────────────┬───────────────────────────────────┘   │
│                                      ▼                                       │
│   ┌─────────────────────────────────────────────────────────────────────┐   │
│   │     Settlement uses average of all snapshots                         │   │
│   │         in the 1-hour window preceding expiry                        │   │
│   └─────────────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────────────┘

Key Concepts

Buffer Configuration

The contract uses fixed parameters optimized for settlement fairness:

ConstantValuePurpose
BUFFER_SIZE240Maximum snapshots stored per pair
BUFFER_DURATION2 hoursTotal time coverage
TWAP_WINDOW1 hourSettlement averaging window
MIN_SNAPSHOT_INTERVAL30 secondsRate limiting between snapshots

Operator-Restricted Snapshots

Snapshots can only be taken by authorized operators or the contract owner:

function takeSnapshot(bytes32 pairId) external onlyOperatorOrOwner {
    _takeSnapshot(pairId);
}
  • Authorization: Only operators (typically keeper bots) or the owner can take snapshots
  • Prevents manipulation: Restricts who can influence TWAP calculations
  • Rate limiting: 30-second minimum between snapshots prevents spam
  • Keeper responsibility: Protocol operators ensure regular snapshot collection

TWAP Calculation

At settlement, the registry queries TWAP for the 1-hour window ending at expiry:

TWAP=i=1nPin\text{TWAP} = \frac{\sum_{i=1}^{n} P_i}{n}

Where PiP_i are the prices of all snapshots within the window.

Edge cases:

  • No snapshots: Falls back to current spot price
  • Partial window: Uses available snapshots
  • Stale data: Snapshots older than the window are ignored

Storage & State

The contract uses ERC-7201 namespaced storage for upgradeability:

/// @custom:storage-location erc7201:diffusal.storage.PriceHistory
struct PriceHistoryStorage {
    address owner;
    address pendingOwner;                              // Two-step ownership transfer
    address oracle;
    mapping(bytes32 => PriceSnapshot[240]) snapshots;  // Circular buffer
    mapping(bytes32 => uint256) head;                  // Next write position
    mapping(bytes32 => uint256) count;                 // Snapshots stored (max 240)
    mapping(bytes32 => uint256) lastSnapshotTime;      // Rate limiting
    mapping(address => bool) operators;                // Authorized snapshot takers
}

PriceSnapshot Struct

struct PriceSnapshot {
    uint256 timestamp;  // Block timestamp of snapshot
    uint256 price;      // Price in WAD (18 decimals)
}

Snapshot Functions

These functions are restricted to operators and owner only.

takeSnapshot

Records a single price snapshot for a trading pair.

function takeSnapshot(bytes32 pairId) external onlyOperatorOrOwner
ParameterTypeDescription
pairIdbytes32Trading pair identifier

Requirements:

  • Caller must be an operator or owner (reverts with NotOperator)
  • At least 30 seconds since last snapshot (reverts with TooFrequent)

Effect:

  1. Fetches current spot price from oracle
  2. Writes to circular buffer at head position
  3. Advances head pointer
  4. Updates last snapshot timestamp

Emits: SnapshotTaken


takeSnapshotBatch

Records snapshots for multiple trading pairs.

function takeSnapshotBatch(bytes32[] calldata pairIds) external onlyOperatorOrOwner

Requirements:

  • Caller must be an operator or owner (reverts with NotOperator)

Behavior:

  • Skips pairs where 30 seconds haven't passed (no revert)
  • Continues processing remaining pairs

Use case: Keeper operators efficiently snapshot all active pairs in one transaction.


View Functions

getTwap

Calculates the Time-Weighted Average Price for a window.

function getTwap(bytes32 pairId, uint256 endTime)
    external view returns (uint256 twapPrice, uint256 snapshotCount)
ParameterTypeDescription
pairIdbytes32Trading pair identifier
endTimeuint256End of TWAP window (typically expiry)

Returns:

  • twapPrice: Average price in WAD (18 decimals)
  • snapshotCount: Number of snapshots used (0 = fallback to spot)

Window: [endTime - TWAP_WINDOW, endTime] (1 hour)

Fallback behavior:

  • No snapshots in buffer → current spot price
  • No snapshots in window → current spot price

getLatestPrice

Returns the most recent snapshot for a pair.

function getLatestPrice(bytes32 pairId) external view returns (uint256 price, uint256 timestamp)

Returns:

  • price: Latest snapshot price in WAD
  • timestamp: When snapshot was taken

Fallback: If no snapshots, returns current spot price and block.timestamp.


needsSnapshot

Checks if a new snapshot can be taken.

function needsSnapshot(bytes32 pairId) external view returns (bool)

Returns: true if:

  • No snapshots exist for this pair, OR
  • At least 30 seconds since last snapshot

Use case: Operator keepers check before calling takeSnapshot() to avoid reverts.


getSnapshotCount

Returns the number of snapshots stored for a pair.

function getSnapshotCount(bytes32 pairId) external view returns (uint256)

Returns: Count (max 240).


owner

Returns the contract owner.

function owner() external view returns (address)

oracle

Returns the oracle address used for price fetching.

function oracle() external view returns (address)

Owner Functions

setOracle

Updates the oracle reference.

function setOracle(address oracle_) external onlyOwner

Emits: OracleUpdated


transferOwnership

Initiates two-step ownership transfer.

function transferOwnership(address newOwner) external onlyOwner

Emits: OwnershipTransferStarted


acceptOwnership

Accepts pending ownership transfer (two-step pattern).

function acceptOwnership() external

Requirements: Caller must be the pending owner.

Emits: OwnershipTransferred


setOperator

Authorizes or revokes an operator for snapshot taking.

function setOperator(address operator, bool authorized) external onlyOwner
ParameterTypeDescription
operatoraddressAddress to authorize/revoke
authorizedboolTrue to authorize, false to revoke

Emits: OperatorUpdated


isOperator

Checks if an address is an authorized operator.

function isOperator(address operator) external view returns (bool)

Events

EventParametersDescription
SnapshotTakenpairId, timestamp, pricePrice snapshot recorded
OracleUpdatedoldOracle, newOracleOracle reference changed
OwnershipTransferStartedpreviousOwner, newOwnerTwo-step ownership transfer initiated
OwnershipTransferredpreviousOwner, newOwnerOwnership changed
OperatorUpdatedoperator, authorizedOperator authorization changed

TWAP Example

Scenario

ETH-USDC option expires at timestamp 1700000000. Price history:

TimestampPrice
1699996400$2980
1699996430$2985
1699996460$2990
......
1699999970$3010
1700000000$3015

Calculation

(uint256 twap, uint256 count) = priceHistory.getTwap(pairId, 1700000000);
// Window: [1699996400, 1700000000] (1 hour)
// All snapshots within window are averaged
// twap ≈ $2997 (average of ~120 snapshots)
// count = 120

Settlement

// In SeriesRegistry.settle()
(uint256 twapPrice, ) = priceHistory.getTwap(pairId, series.expiry);
// Series settles at $2997 TWAP, not instantaneous price

Integration Points

Depends On

ContractPurpose
DiffusalOracleCurrent spot price for snapshots

Used By

ContractPurpose
DiffusalOptionsSeriesRegistryTWAP for settlement price

Security Considerations

Manipulation Resistance

TWAP averaging prevents single-block price manipulation:

  • Window size: 1 hour of data requires sustained manipulation
  • Snapshot density: Up to 120 snapshots per hour
  • Cost: Manipulating average requires moving price consistently

Operator-Controlled Design

Snapshots are restricted to authorized operators and the owner:

  • Prevents manipulation: Untrusted parties cannot influence TWAP by selective snapshot timing
  • Ensures data quality: Operators are responsible for regular, consistent snapshots
  • Rate limiting: 30-second minimum interval prevents excessive snapshots
  • Trust assumption: Protocol relies on operators to maintain snapshot availability

Fallback Behavior

When insufficient historical data exists:

if (count == 0) {
    return (_getCurrentPrice(pairId), 0);
}

This spot price fallback:

  • Enables trading on new pairs immediately
  • Reduces settlement manipulation resistance
  • Should be avoided for high-value settlements

Oracle Trust

Price snapshots inherit oracle trust assumptions:

  • Snapshots reflect oracle price at snapshot time
  • Stale or manipulated oracle prices get recorded
  • Multiple snapshots dilute impact of single bad price

Rate Limiting

The 30-second minimum interval:

  • Prevents DoS attacks
  • Limits storage consumption
  • Still allows ~120 snapshots in TWAP window

Code Reference

Source: packages/contracts/src/DiffusalPriceHistory.sol

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

Key Constants

Constants are defined in the centralized Constants.sol library:

// From packages/contracts/src/utils/Constants.sol
uint256 internal constant BUFFER_SIZE = 240;
uint256 internal constant BUFFER_DURATION = 2 hours;
uint256 internal constant TWAP_WINDOW = 1 hours;
uint256 internal constant MIN_SNAPSHOT_INTERVAL = 30 seconds;
uint256 internal constant MAX_SNAPSHOT_PRICE_AGE = 60;  // 60 seconds staleness limit

These constants are imported from Constants.sol into DiffusalPriceHistory.sol.


On this page