Olive oil traceability, step by step

6. Who can do what

Goal: Restrict each phase to the right role and enforce phase order inside the contract.

Like the keys of a building: the doorkeeper has the entrance key, the plumber the one for the technical rooms. Nobody can open a door that is not theirs.

OpenZeppelin's AccessControl manages roles: a role is a name (computed with keccak256) plus a list of addresses holding it. The onlyRole modifier blocks anyone without it. In the Pietre di Vita registry the roles are PRODUTTORE_ROLE, FRANTOIO_ROLE, LABORATORIO_ROLE and IMBOTTIGLIATORE_ROLE; here we use two.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";

contract PassiMinimi is AccessControl {
    bytes32 public constant FRANTOIO_ROLE = keccak256("FRANTOIO_ROLE");
    bytes32 public constant LABORATORIO_ROLE = keccak256("LABORATORIO_ROLE");

    enum Fase { Raccolta, Molitura, Analisi }
    struct Passo { Fase fase; address attore; uint64 timestamp; bytes32 docHash; }

    Passo[] private _passi;
    error FaseFuoriOrdine(Fase fase);

    constructor() {
        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _registra(Fase.Raccolta, bytes32(0));
    }

    function molisci(bytes32 docHash) external onlyRole(FRANTOIO_ROLE) { _avanza(Fase.Molitura, docHash); }
    function analizza(bytes32 docHash) external onlyRole(LABORATORIO_ROLE) { _avanza(Fase.Analisi, docHash); }

    function _avanza(Fase fase, bytes32 docHash) private {
        Fase ultima = _passi[_passi.length - 1].fase;
        if (uint8(fase) != uint8(ultima) + 1) revert FaseFuoriOrdine(fase);
        _registra(fase, docHash);
    }

    function _registra(Fase fase, bytes32 docHash) private {
        _passi.push(Passo(fase, msg.sender, uint64(block.timestamp), docHash));
    }
}

Line by line

  1. The two constants are the role names; keccak256 turns them into a fixed number.
  2. Passo is one row of the register: phase, who wrote it, when, document fingerprint.
  3. _passi is the list of steps, append-only; error defines the message used when the order is wrong.
  4. The constructor assigns the admin (who grants and removes roles) and records Raccolta as the first step.
  5. molisci and analizza can only be called by the right role (onlyRole).
  6. _avanza reads the last recorded phase: the new one must be exactly the next, otherwise revert cancels everything.
  7. _registra adds the step with its signer (msg.sender) and time (block.timestamp).

The full contract does the same with more phases: it checks the role (RuoloNonAutorizzato) and the order (FaseFuoriOrdine), and for bottling it uses a separate function, imbottiglia, which also fixes the bottle count once.

To give a role, the admin calls grantRole(FRANTOIO_ROLE, address): this is what the demo's setup script does.

Try it yourself: the trial mode of the Supply chain area

On the Supply chain area page anyone can walk the supply chain without a password. You pick the four roles one at a time (Farmer, Mill, Laboratory, Bottler) and record each one's step on a trial lot of your own, with a code such as PROVA-K7Q2. A counter always tells you where you are ("You are at step 3 of 5") and a large button takes you to the next role. At the end you open your own bottle: the same public page and the same QR as the real bottles, with the story checked on the chain.

The server's wallet pays for the chain writes, so the trial has limits designed to keep it running:

The trial lot is separate from the sample lots: the visitor writes only on their own lot, with a session distinct from the real roles' one, and the lot appears in no public list (you find it again with your own link). The lab report is a sample PDF generated by the server, with fictional data. Everything happens on a test network.

Try it yourself

Add a third role, IMBOTTIGLIATORE_ROLE, and a phase Imbottigliamento after Analisi with its function. Recompile: if you copied the molisci pattern it takes a few minutes.

What you learned

To go deeper