Chapter 6 of 9

Buying and selling

Goal. Understand the arithmetic that turns POL into litres and back, and why a POL reserve is needed.

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:

OperationFormulaExample
Buy with POLtokens = POL × POLprice ÷ petrolPrice1 POL × 0.10 ÷ 2.00 = 0.05 tokens
Sell tokensPOL = tokens × petrolPrice ÷ POLprice0.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.

Analogy. The contract is a currency exchange with a drawer. When you buy, it puts your POL in the drawer and prints a receipt (the tokens). When you sell back, it takes the receipt and returns POL from the drawer.
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();
    }
}

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