Chapter 4 of 9
The price contract
Our oracle is a contract that stores three things: the petrol price, the POL price and the time of the last update. Anyone can read; only someone with a specific role can write.
Two rules about numbers. First: the blockchain has no decimals, so prices are integers multiplied by one million: 1.989 € becomes 1989000. Second: the time comes from the blockchain itself (block.timestamp); the writer does not choose it.
contract FuelPriceOracle is AccessControl {
bytes32 public constant UPDATER_ROLE = keccak256("UPDATER_ROLE");
uint256 private _fuelEurPerLitre;
uint256 private _polEur;
uint64 private _updatedAt;
uint64 private _sourceDate;
constructor(address admin) {
_grantRole(DEFAULT_ADMIN_ROLE, admin);
}
function setPrices(uint256 fuelEurPerLitre, uint256 polEur, uint64 sourceDate)
external onlyRole(UPDATER_ROLE)
{
require(fuelEurPerLitre > 0 && polEur > 0, "zero price");
_fuelEurPerLitre = fuelEurPerLitre;
_polEur = polEur;
_sourceDate = sourceDate;
_updatedAt = uint64(block.timestamp);
}
function fuelPrice() external view returns (uint256, uint64) {
return (_fuelEurPerLitre, _updatedAt);
}
}
AccessControlis a module from OpenZeppelin: it manages roles.UPDATER_ROLEis the name of the "who may update" role (a label derived from a text).- The four
privatevariables are the blackboard: petrol price, POL price, instant of the last update and the data's date (sourceDate, the day the prices refer to). - The
constructorruns once, at publication, and gives the admin role to whoever publishes. setPriceswrites the two prices and the data's date;onlyRole(UPDATER_ROLE)rejects anyone without the role.requirerejects a price equal to zero, which would break the divisions in chapter 6.block.timestampis the time of the block in which the write is recorded.fuelPriceisview: reading is free and needs no signature.
The full project adds the function for the POL price and the freshness check, which the token will use in the next chapters, plus an event (PricesUpdated) that records every update:
function polPrice() external view returns (uint256, uint64) {
return (_polEur, _updatedAt);
}
function isFresh(uint64 maxAgeSeconds) external view returns (bool) {
return _updatedAt != 0
&& block.timestamp <= uint256(_updatedAt) + maxAgeSeconds;
}
polPricereturns the POL price and the instant, likefuelPrice.isFreshanswers "yes" only if prices exist (_updatedAt != 0) and are no older thanmaxAgeSecondsseconds.
Try it
Open the Remix site (remix.ethereum.org), paste the contract with the OpenZeppelin import lines and pragma solidity ^0.8.24; at the top, compile it and deploy it to Remix's test environment. Then try calling setPrices without the role and watch it get rejected.
What you learned
- A minimal oracle is a blackboard: values, a date and one authorised writer.
- Prices are integers with six decimals: 1.989 € is 1989000.
isFreshlets other contracts refuse data that is too old.