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:
| Function | Description |
|---|---|
| TWAP Calculation | Provides 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:
| Constant | Value | Purpose |
|---|---|---|
BUFFER_SIZE | 240 | Maximum snapshots stored per pair |
BUFFER_DURATION | 2 hours | Total time coverage |
TWAP_WINDOW | 1 hour | Settlement averaging window |
MIN_SNAPSHOT_INTERVAL | 30 seconds | Rate 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:
Where 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| Parameter | Type | Description |
|---|---|---|
pairId | bytes32 | Trading pair identifier |
Requirements:
- Caller must be an operator or owner (reverts with
NotOperator) - At least 30 seconds since last snapshot (reverts with
TooFrequent)
Effect:
- Fetches current spot price from oracle
- Writes to circular buffer at head position
- Advances head pointer
- Updates last snapshot timestamp
Emits: SnapshotTaken
takeSnapshotBatch
Records snapshots for multiple trading pairs.
function takeSnapshotBatch(bytes32[] calldata pairIds) external onlyOperatorOrOwnerRequirements:
- 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)| Parameter | Type | Description |
|---|---|---|
pairId | bytes32 | Trading pair identifier |
endTime | uint256 | End 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 WADtimestamp: 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 onlyOwnerEmits: OracleUpdated
transferOwnership
Initiates two-step ownership transfer.
function transferOwnership(address newOwner) external onlyOwnerEmits: OwnershipTransferStarted
acceptOwnership
Accepts pending ownership transfer (two-step pattern).
function acceptOwnership() externalRequirements: 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| Parameter | Type | Description |
|---|---|---|
operator | address | Address to authorize/revoke |
authorized | bool | True 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
| Event | Parameters | Description |
|---|---|---|
SnapshotTaken | pairId, timestamp, price | Price snapshot recorded |
OracleUpdated | oldOracle, newOracle | Oracle reference changed |
OwnershipTransferStarted | previousOwner, newOwner | Two-step ownership transfer initiated |
OwnershipTransferred | previousOwner, newOwner | Ownership changed |
OperatorUpdated | operator, authorized | Operator authorization changed |
TWAP Example
Scenario
ETH-USDC option expires at timestamp 1700000000. Price history:
| Timestamp | Price |
|---|---|
| 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 = 120Settlement
// In SeriesRegistry.settle()
(uint256 twapPrice, ) = priceHistory.getTwap(pairId, series.expiry);
// Series settles at $2997 TWAP, not instantaneous priceIntegration Points
Depends On
| Contract | Purpose |
|---|---|
| DiffusalOracle | Current spot price for snapshots |
Used By
| Contract | Purpose |
|---|---|
| DiffusalOptionsSeriesRegistry | TWAP 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 limitThese constants are imported from Constants.sol into DiffusalPriceHistory.sol.
Related
- Options Settlement (Protocol) — How TWAP enables fair settlement
- DiffusalOptionsSeriesRegistry — Series settlement using TWAP
- DiffusalOracle — Price source for snapshots