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                                 │
│   - 90-second minimum between snapshots                                      │
│   - Up to 6 hours of history (240 slots x 90s minimum interval)              │
└─────────────────────────────────────┬───────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────────┐
│                             TWAP WINDOW                                      │
│                                                                              │
│   ┌─────────────────────────────────────────────────────────────────────┐   │
│   │                    Buffer: up to 6 hours (240 slots)                             │   │
│   └─────────────────────────────────┬───────────────────────────────────┘   │
│                                      ▼                                       │
│   ┌─────────────────────────────────────────────────────────────────────┐   │
│   │              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 hoursTWAP-relevant history window
TWAP_WINDOW1 hourSettlement averaging window
MIN_SNAPSHOT_INTERVAL90 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: 90-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 90 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 90 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 90 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)

Admin / Owner Functions

setOracle

Updates the oracle reference.

function setOracle(address oracle_) external onlyAdmin

Emits: OracleUpdated


addAdmin / removeAdmin

Owner-managed admin set for operational controls.

function addAdmin(address admin) external onlyOwner
function removeAdmin(address admin) external onlyOwner

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 onlyAdmin
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-USD 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 40 snapshots per hour (60 min ÷ 90s)
  • 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: 90-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 90-second minimum interval:

  • Prevents DoS attacks
  • Limits storage consumption
  • Still allows ~40 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 TWAP_WINDOW = 1 hours;
uint256 internal constant MIN_SNAPSHOT_INTERVAL = 90 seconds;
uint256 internal constant MAX_SNAPSHOT_PRICE_AGE = 60;  // 60 seconds staleness limit

Note: BUFFER_DURATION (2 hours) exists only in TypeScript constants and test helpers, not in the production contract. The buffer can hold up to 240 × 90s = 6 hours of data at minimum interval spacing.


On this page