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 │
│ - 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:
| Constant | Value | Purpose |
|---|---|---|
BUFFER_SIZE | 240 | Maximum snapshots stored per pair |
BUFFER_DURATION | 2 hours | TWAP-relevant history window |
TWAP_WINDOW | 1 hour | Settlement averaging window |
MIN_SNAPSHOT_INTERVAL | 90 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: 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:
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 90 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 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)| 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 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 onlyAdminEmits: OracleUpdated
addAdmin / removeAdmin
Owner-managed admin set for operational controls.
function addAdmin(address admin) external onlyOwner
function removeAdmin(address admin) external onlyOwnertransferOwnership
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 onlyAdmin| 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-USD 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 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 limitNote: 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.
Related
- Options Settlement (Protocol) — How TWAP enables fair settlement
- DiffusalOptionsSeriesRegistry — Series settlement using TWAP
- DiffusalOracle — Price source for snapshots