Data Oracle
Prices and other off-chain values, reported by independent nodes. Each report needs a threshold of signatures from distinct authorized signers, and the contract stores the median.
Reading a feed
Every feed has a proxy that implements the standard AggregatorV3Interface, so existing price-feed code works unchanged:
AggregatorV3Interface feed = AggregatorV3Interface(ETH_USD_PROXY);
(, int256 answer, , uint256 updatedAt, ) = feed.latestRoundData();
require(block.timestamp - updatedAt < 1 hours, "stale price");
uint8 decimals = feed.decimals(); // 8 for USD pairsThe proxy addresses for each network are in deployments/<network>.json under oracle.feeds.
How a report is accepted
- Each node signs
keccak256(domain, chainId, oracle, feedId, roundId, answer, observedAt)as an EIP-191 message. - Anyone submits
transmit(feedId, roundId, observations), with signers sorted ascending. - The contract checks:
- the signatures and that each signer is authorized
- that there are no duplicate signers
- that observations are fresh (5 minutes by default)
- that
roundIdis the next round
- The median is stored. For an even count it's the mean of the two middle values.
A minority of faulty or malicious signers can't move the median, and replaying an old report fails because signatures are bound to the round.
Update policy
The bundled node reports when the price moves at least ORACLE_DEVIATION_BPS (0.5%), or when ORACLE_HEARTBEAT_SEC (60s) passes without an update. Always check updatedAt in your contract.
Feeds on a new deployment
| Feed | Decimals | Signers |
|---|---|---|
| ETH / USD | 8 | 5 of 8 |
| BTC / USD | 8 | 5 of 8 |
A report needs 5 signatures from the 8 signers and stores their median. It stays correct while up to 2 signers are compromised, and it keeps updating while up to 3 are offline. Run each signer on a separate machine or operator. That independence matters as much as the threshold.
The contract owner adds feeds with createFeed(description, decimals, signers, threshold, maxAge).