Chapter 6 of 9
Buying and selling
The token is bought by sending POL to the contract with buy() and sold back with sell(). The contract reads the two prices from the oracle and applies a proportion.
The arithmetic, with round numbers
Suppose petrol costs 2.00 € per litre and 1 POL is worth 0.10 €. A litre therefore costs 2.00 ÷ 0.10 = 20 POL, and 1 POL buys one twentieth of a litre:
| Operation | Formula | Example |
|---|---|---|
| Buy with POL | tokens = POL × POLprice ÷ petrolPrice | 1 POL × 0.10 ÷ 2.00 = 0.05 tokens |
| Sell tokens | POL = tokens × petrolPrice ÷ POLprice | 0.05 tokens × 2.00 ÷ 0.10 = 1 POL |
In the contract prices are integers with six decimals (2000000 and 100000) and amounts are in units of 10-18. The arithmetic is identical: 10**18 * 100000 / 2000000 = 5 * 10**16, that is 0.05 tokens. Integer division rounds down, so a round trip never makes something out of nothing.
contract FuelToken is ERC20 {
FuelPriceOracle public immutable oracle;
uint64 public maxAge = 3 days;
constructor(address oracle_) ERC20("FuelToken", "FUEL") {
oracle = FuelPriceOracle(oracle_);
}
function buy() external payable {
(uint256 f, uint256 p) = _prices();
_mint(msg.sender, msg.value * p / f);
}
function sell(uint256 amount) external {
(uint256 f, uint256 p) = _prices();
uint256 polOut = amount * f / p;
require(address(this).balance >= polOut, "reserve too low");
_burn(msg.sender, amount);
payable(msg.sender).transfer(polOut);
}
function reserve() external view returns (uint256) {
return address(this).balance;
}
function _prices() internal view returns (uint256 f, uint256 p) {
require(oracle.isFresh(maxAge), "price is old");
(f,) = oracle.fuelPrice();
(p,) = oracle.polPrice();
}
}
oracleis the address of the chapter 4 oracle, fixed at publication (immutable);maxAgeis the maximum age of the data, 3 days.buy()ispayable: it accepts POL.msg.valueis how much POL came with the call._mintcreates new tokens for the caller (msg.sender): the formula from the table.sellcomputes the POL owed and checks the drawer holds enough._burndestroys the returned tokens andtransfersends the POL to the seller.reserve()shows the contract's POL balance, that is, the drawer._prices()refuses everything if the oracle is not fresh, then reads the two prices.
The full project adds preview functions (quoteBuy, quoteSell) to show the result before signing, an emergency pause, clearer custom errors, and a check that the POL payment succeeds.
Why a reserve
Tokens in circulation are promises to give POL back. If the drawer is empty, sell must fail instead of inventing coin: that is the reserve too low check. In this demo the reserve is made of the POL paid by buyers; a rising price can leave the drawer short. It is a property worth knowing, and the reason a real product has far stricter rules (see the FAQ).
Try it
Redo the arithmetic with other numbers: petrol at 1.80 € and POL at 0.09 €. How many tokens do you get for 2 POL? (2 × 0.09 ÷ 1.80 = 0.10 tokens.) Then work out how much POL you get back selling them.
What you learned
- Buying is POL × POLprice ÷ petrolPrice; selling is the inverse proportion.
- With petrol at 2.00 € and POL at 0.10 €, 1 POL is 0.05 tokens.
- The POL reserve is the drawer that makes selling back possible: if empty,
sellstops.