Solidity Oracle Design
An oracle is a price feed that other contracts read. A consumer contract trusts the feed for the current price, so the feed must also say how fresh that price is.
A consumer reads the mock feed price
The feed starts at 100. The consumer reads 100, and after the owner updates the feed it reads 120.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract MockFeed {
address public owner;
uint256 public price;
uint256 public updatedAt;
constructor(uint256 initialPrice) {
owner = msg.sender;
price = initialPrice;
updatedAt = block.timestamp;
}
modifier onlyOwner() {
require(msg.sender == owner, "not owner");
_;
}
function setPrice(uint256 newPrice) public onlyOwner {
require(newPrice > 0, "price must be positive");
price = newPrice;
updatedAt = block.timestamp;
}
}
contract PriceConsumer {
MockFeed public feed;
uint256 public maxAge;
constructor(MockFeed feed_, uint256 maxAge_) {
feed = feed_;
maxAge = maxAge_;
}
function readPrice() public view returns (uint256) {
require(block.timestamp - feed.updatedAt() <= maxAge, "stale price");
return feed.price();
}
}
A price older than the max age reverts reads
The max age is 3600 seconds. At age 3600 the read still works, and at age 3601 it reverts as stale.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract MockFeed {
address public owner;
uint256 public price;
uint256 public updatedAt;
constructor(uint256 initialPrice) {
owner = msg.sender;
price = initialPrice;
updatedAt = block.timestamp;
}
modifier onlyOwner() {
require(msg.sender == owner, "not owner");
_;
}
function setPrice(uint256 newPrice) public onlyOwner {
require(newPrice > 0, "price must be positive");
price = newPrice;
updatedAt = block.timestamp;
}
}
contract PriceConsumer {
MockFeed public feed;
uint256 public maxAge;
constructor(MockFeed feed_, uint256 maxAge_) {
feed = feed_;
maxAge = maxAge_;
}
function readPrice() public view returns (uint256) {
require(block.timestamp - feed.updatedAt() <= maxAge, "stale price");
return feed.price();
}
}
A zero price and a stranger update are both rejected
A price of zero would break every consumer calculation that divides by price. A stranger cannot update the feed either, since only the owner may set the price.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract MockFeed {
address public owner;
uint256 public price;
uint256 public updatedAt;
constructor(uint256 initialPrice) {
owner = msg.sender;
price = initialPrice;
updatedAt = block.timestamp;
}
modifier onlyOwner() {
require(msg.sender == owner, "not owner");
_;
}
function setPrice(uint256 newPrice) public onlyOwner {
require(newPrice > 0, "price must be positive");
price = newPrice;
updatedAt = block.timestamp;
}
}
contract PriceConsumer {
MockFeed public feed;
uint256 public maxAge;
constructor(MockFeed feed_, uint256 maxAge_) {
feed = feed_;
maxAge = maxAge_;
}
function readPrice() public view returns (uint256) {
require(block.timestamp - feed.updatedAt() <= maxAge, "stale price");
return feed.price();
}
}
Example
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract MockFeed {
address public owner;
uint256 public price;
uint256 public updatedAt;
constructor(uint256 initialPrice) {
owner = msg.sender;
price = initialPrice;
updatedAt = block.timestamp;
}
modifier onlyOwner() {
require(msg.sender == owner, "not owner");
_;
}
function setPrice(uint256 newPrice) public onlyOwner {
require(newPrice > 0, "price must be positive");
price = newPrice;
updatedAt = block.timestamp;
}
}
contract PriceConsumer {
MockFeed public feed;
uint256 public maxAge;
constructor(MockFeed feed_, uint256 maxAge_) {
feed = feed_;
maxAge = maxAge_;
}
function readPrice() public view returns (uint256) {
require(block.timestamp - feed.updatedAt() <= maxAge, "stale price");
return feed.price();
}
}
Key points
- Oracle: An oracle is a feed that supplies outside data, such as a price, to contracts.
- Mock feed: A mock feed is a tiny test feed whose price its owner sets by hand.
- Consumer: A consumer is a contract that reads a price from a feed instead of storing its own.
- Staleness: Staleness is how old a price is, measured from the time it was last updated.
- Max age: A max age is the oldest price a consumer still accepts before it refuses to read.
- Restatement: A feed supplies the price, and the consumer checks that the price is fresh enough to use.
In this example, the feed starts at 100 and the owner updates it to 150. The consumer reads 150 while the price is still fresh after warping forward 1800 seconds. After warping to 4000 seconds, the price is older than the max age of 3600, so the read reverts instead of returning a stale value.
Note
A mock feed is only for teaching, since a real feed would need many independent price sources.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Remix IDE
Exercise
A consumer has a max age of 3600 seconds and the feed was updated at time 1. At what time does a read first revert?