Solidity basique : contract, state et function

1. Aperçu général

Solidity est le langage principal pour écrire des smart contracts sur Ethereum et les chaînes compatibles EVM. Cet article vous aide à maîtriser les trois blocs de construction les plus fondamentaux : contract, les variables d’état et function — à travers un simple contrat Wallet. Une fois que vous aurez fini, vous saurez écrire un contrat capable de sauvegarder un état, contrôler l’accès avec des modificateurs, et écrire vos premiers tests avec Hardhat.

2. Qu’est-ce qu’un contract

En Solidity, un contract fonctionne comme une class en Java — il définit l’état (les données) et le comportement (les fonctions) qui seront déployés sur la blockchain à une adresse unique.

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

contract Wallet {
    // les variables d'état et les fonctions iront ici
}
  • La ligne // SPDX-License-Identifier déclare la licence ; le compilateur affichera un avertissement si elle est manquante.
  • pragma solidity ^0.8.24 verrouille la version du compilateur à partir de 0.8.24 (mais en dessous de 0.9.0).
  • Chaque contrat déployé aura sa propre adresse, comme un « objet » qui vit éternellement sur la chaîne.

Attention : Ne jamais utiliser un pragma aussi vague que >=0.8.0 <0.9.0 en production — les nouveaux compilateurs peuvent modifier le comportement par défaut (par exemple, la vérification des débordements).

3. Variable d’état — les données sauvegardées on-chain

Les variables d’état sont des variables écrites directement dans le stockage de la blockchain, qui persistent entre les appels de fonction et consomment du gas lors de l’écriture.

contract Wallet {
    address public owner;
    uint256 public balance;
    mapping(address => uint256) public allowance;

    constructor() {
        owner = msg.sender;
    }
}
  • address public owner: stocke l’adresse du propriétaire du contrat. Le mot-clé public génère automatiquement un getter owner() gratuit.
  • uint256 public balance: le solde, un type d’entier non signé 256-bit — le type numérique le plus courant dans Solidity.
  • mapping(address => uint256): comme un HashMap<Address, BigInteger>, mais les clés ne sont pas énumérables, seulement consultables par clé.
  • constructor() s’exécute exactement une fois au déploiement, msg.sender est l’adresse qui exécute la transaction de déploiement.

3.1. Distinguer storage, memory, calldata

function updateAllowance(address spender, uint256 amount) external {
    // amount est dans calldata (lecture seule, bon marché en gas)
    allowance[spender] = amount; // écrit dans le storage
}
  • storage: données permanentes sur la chaîne (variables d’état).
  • memory: données temporaires pendant l’exécution de la fonction, perdues après la fin de la fonction.
  • calldata: comme memory mais en lecture seule, utilisé pour les paramètres des fonctions external — plus économe en gas que memory.

Attention : Oublier de déclarer memory/calldata pour les paramètres de type array, struct, string causera une erreur du compilateur — c’est très différent de Java, où vous n’avez pas besoin de gérer les zones mémoire.

4. Fonction et visibilité

Les fonctions en Solidity doivent obligatoirement déclarer leur visibilité explicitement, il n’y a pas de défaut « package-private » comme en Java.

contract Wallet {
    address public owner;
    uint256 public balance;

    constructor() {
        owner = msg.sender;
    }

    function deposit() external payable {
        balance += msg.value;
    }

    function withdraw(uint256 amount) external {
        require(msg.sender == owner, "Wallet: not owner");
        require(amount <= balance, "Wallet: insufficient balance");

        balance -= amount;
        (bool success, ) = owner.call{value: amount}("");
        require(success, "Wallet: transfer failed");
    }

    function getBalance() external view returns (uint256) {
        return balance;
    }
}
  • external: ne peut être appelée que de l’extérieur du contrat (par exemple via MetaMask, un autre contrat) — moins chère en gas que public quand les paramètres sont des arrays/strings volumineux.
  • payable: permet à la fonction de recevoir de l’ETH avec la transaction ; msg.value est le nombre de wei envoyés.
  • view: promet de ne pas modifier l’état, ne consomme pas de gas quand elle est appelée hors-chaîne (via eth_call).
  • require(condition, "message"): vérifie une condition, annule toute la transaction avec un message si elle est fausse — similaire à lever une IllegalArgumentException, mais annule aussi l’état et le gas consommé (sauf le gas de la vérification).
  • owner.call{value: amount}(""): la façon recommandée d’envoyer l’ETH au lieu de transfer()/send() car elle n’impose pas une limite stricte de 2300 gas.

Les quatre niveaux de visibilité :

Visibilité Appelable de l’intérieur Appelable d’un contrat enfant Appelable de l’extérieur
private ✅ ❌ ❌
internal ✅ ✅ ❌
public ✅ ✅ ✅
external ❌ (doit utiliser this.foo()) ❌ ✅

5. Modificateur — réutiliser les conditions de vérification

Répéter require(msg.sender == owner, ...) dans plusieurs fonctions est un signe qu’il faut l’extraire dans un modificateur.

contract Wallet {
    address public owner;
    uint256 public balance;

    modifier onlyOwner() {
        require(msg.sender == owner, "Wallet: not owner");
        _;
    }

    constructor() {
        owner = msg.sender;
    }

    function withdraw(uint256 amount) external onlyOwner {
        require(amount <= balance, "Wallet: insufficient balance");
        balance -= amount;
        payable(owner).transfer(amount);
    }
}
  • modifier onlyOwner() définit un bloc de vérification ; _; est l’emplacement où le corps de la fonction d’origine sera inséré.
  • Appliquer onlyOwner après la déclaration de withdraw rend le code lisible comme « seul le propriétaire peut retirer des fonds » — plus clair que de répéter require dans chaque fonction.
  • Les modificateurs peuvent accepter des paramètres, par exemple modifier minAmount(uint256 amount) { require(amount > 0, "..."); _; }.

6. Événement — enregistrer les logs pour hors-chaîne

Un contrat ne peut pas retourner directement des données historiques à un frontend ; event est utilisé pour enregistrer des logs qu’une dApp peut écouter.

contract Wallet {
    event Deposited(address indexed from, uint256 amount);
    event Withdrawn(address indexed to, uint256 amount);

    uint256 public balance;

    function deposit() external payable {
        balance += msg.value;
        emit Deposited(msg.sender, msg.value);
    }
}
  • event Deposited(...) déclare la structure du log, avec un maximum de 3 paramètres indexed pour faciliter le filtrage.
  • emit Deposited(...) écrit le log dans le reçu de la transaction, bien moins cher en gas que de sauvegarder dans le stockage.
  • Un frontend utilise une bibliothèque comme ethers.js pour écouter contract.on("Deposited", ...) et mettre à jour l’UI en temps réel.

7. Erreurs courantes quand vous apprenez Solidity

  • Confondre transfer/send/call pour envoyer l’ETH : transfer limite à 2300 gas, ce qui échoue facilement avec un contrat récepteur ayant une logique complexe (proxy, multisig). Utilisez call avec une vérification require comme dans l’exemple à la section 4.
  • Oublier require pour valider les entrées : la validation manquante expose le contrat à des exploits (par exemple retrait négatif, débordement — bien que depuis Solidity 0.8 le débordement soit automatiquement annulé, la logique métier a besoin de sa propre validation).
  • Confondre external et public dans le mauvais contexte : utiliser public pour une fonction appelée uniquement de l’extérieur gaspille du gas quand les paramètres sont des arrays/strings, car Solidity doit copier les données en memory au lieu de les lire directement depuis calldata.
  • Ne pas déclarer view/pure quand c’est possible : les outils comme les explorateurs de blocs et les portefeuilles ne reconnaissent pas une fonction en lecture seule, les utilisateurs la confondent avec une transaction payante.

Quand NE PAS laisser une fonction public : si une fonction n’est jamais appelée depuis un autre contrat ou héritage, utilisez external pour économiser du gas et limiter la surface d’attaque.

8. Écrire des tests avec Hardhat

Ajoutez les dépendances pour un projet Hardhat (Node.js) :

npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox

Testez le comportement de withdraw qui ne permet qu’au propriétaire :

const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("Wallet", function () {
  it("seul le propriétaire peut retirer des fonds", async function () {
    const [owner, stranger] = await ethers.getSigners();
    const Wallet = await ethers.getContractFactory("Wallet");
    const wallet = await Wallet.deploy();

    await wallet.connect(owner).deposit({ value: ethers.parseEther("1") });

    await expect(
      wallet.connect(stranger).withdraw(ethers.parseEther("1"))
    ).to.be.revertedWith("Wallet: not owner");

    await expect(
      wallet.connect(owner).withdraw(ethers.parseEther("1"))
    ).to.changeEtherBalance(owner, ethers.parseEther("1"));
  });
});
  • ethers.getContractFactory("Wallet") compile et charge l’artifact du contrat.
  • wallet.connect(stranger) simule une transaction envoyée par une adresse autre que le propriétaire.
  • revertedWith("...") affirme que la transaction a été annulée avec exactement ce message du require.
  • changeEtherBalance vérifie que le solde ETH du propriétaire augmente de exactement 1 ETH après le retrait — une façon courante de tester au lieu de simplement vérifier la valeur de retour.

Exécutez les tests :

npx hardhat test
Wallet
  ✔ seul le propriétaire peut retirer des fonds (120ms)

1 passing (120ms)

9. Conclusion

Vous venez de parcourir quatre concepts fondamentaux de Solidity : contract comme unité de déploiement, les variables d’état pour stocker les données on-chain avec trois zones mémoire (storage/memory/calldata), les fonctions avec visibilité et modificateurs pour contrôler l’accès, et les événements pour communiquer avec hors-chaîne. Ces concepts forment le fondement pour progresser vers des sujets plus avancés comme l’héritage, les interfaces, ou les normes ERC-20/ERC-721 — tous construits sur ces quatre blocs.

solidityblockchainsmart-contractethereumweb3