Suite au premier article visant à expliquer les concepts de base de Solidity, nous allons maintenant apprendre à rédiger notre premier contract. Vous verrez qu’apprendre solidity n’est pas si complexe si vous avez déjà touché à du Javascript, la syntaxe est très similaire. Mais trêve de bavardages: entrons désormais dans le vif du sujet ! Nous allons apprendre à créer un système de vote utilisant la Blockchain.
Ce projet est une version simplifiée de ce qui est proposé sur la documentation officielle.

Structure de notre projet

Pour commencer un contract nous allons avoir besoin de deux choses: préciser la version de solidity que nous voulons utiliser puis créer notre « classe » de contract:

pragma solidity ^0.8.4; // On définit la version utilisée

contract MyVoteContract {} // On crée notre classe

Maintenant que nous avons notre base, nous allons définir de quelles entités nous allons avoir besoin. Pour aller au plus simple nous allons avoir besoin uniquement de votants et de propositions de choses à voter. Nos votants seront composés d’un booléen indiquant s’ils ont déjà voté ou non ainsi que d’un integer indiquant l’id de la proposition votée. Nos propositions, elles, seront composée d’un titre sous forme de string et d’un integer permettant de connaître le nombre de gens ayant voté pour. Nous allons passer pour cela par deux structs:

struct Voter {
    bool voted;
    uint vote;
}

struct Proposal {
    string name;
    uint voteCount;
}

Ici nous avons donc la structure de base de nos votants et de nos propositions, mais pour les stocker nous allons avoir besoin de deux variables. Commençons par la plus simple: celle des propositions. Il s’agit simplement d’un tableau pouvant contenir des propositions. Et comme expliqué dans l’article précédent, nous devons préciser la privacité de la variable, ici elle sera publique:

Proposal[] public proposals;

Les adresses des votants

Maintenant pour nos votants, nous allons avoir besoin d’un poil plus. Vous avez sans doutes remarqué que nous n’avons pour le moment aucun moyen d’identifier nos votants. Et c’est normal ! Nous allons le faire depuis un outil très particulier de Solidity: le mapping. Voyez ça comme un dictionnaire. Ici, nous allons pouvoir lier un Voter à une adresse via laquelle il enverra la requête. La clé sera l’adresse, et la valeur notre Voter. Le gros plus du mapping est que quand nous créons un mapping d’adresses, il va automatiquement stocker un struct Voter vide pour chaque adresse possible:

mapping(address => Voter) public voters;

Notre constructeur

Ici, nous voulons voter pour des propositions et sortir la plus votée. Nous allons donc faire en sorte de générer notre contrat avec une liste de propositions. Pour cela nous allons passer par le constructeur du contrat. Comme vous l’avez sûrement deviné, c’est la fonction qui sera appelée à l’initialisation du contrat.
Notre constructor prendra donc en paramètre un tableau de strings et créera pour chaque élément un struct Proposal. Son titre sera la string entrée en paramètre et on initiera le voteCount à 0:

constructor(string[] memory proposalNames) {
    for (uint i = 0; i < proposalNames.length; i++) {
        proposals.push(Proposal({
            name: proposalNames[i],
            voteCount: 0
        }));
    }
}

Vous vous demandez sûrement ce que fait le mot clé « memory » ici, nous y reviendrons un tout petit peu plus tard. Patience !

Un getter pour nos proposals

Autant pour les variables simples, le getter n’a pas besoin d’être créé à la main. Par exemple un integer peut être récupéré juste par le nom de la variable. Pour des variables plus complexes comme les array ou les mapping, il faut créer un getter à la main. Cependant comme nous savons que nous n’allons pas avoir des millions de propositions, notre getter sera tout simple:

function get_proposals() public view returns (Proposal[] memory) {
    return proposals;
}

Petite nouveauté ici: le view. Ce mot clé permet de préciser que rien ne sera stocké dans la blockchain. L’avantage ? C’est qu’une fonction view ne vous coûtera rien en gas fees ! Utilisé à bon escient, une fonction view permet donc de mettre à disposition certaines données et d’en faire côté front une visualisation. Payer pour ça serait embêtant !

La fonction attendue: pouvoir voter

Ici nous allons avoir besoin de plusieurs notions intéressantes. Mais avant toute chose, créons déjà notre fonction. Cette fonction n’a pas lieu d’être appelée en interne, on la mettra donc en external et non en public. Et en paramètre, nous prendrons juste l’id d’un external sous forme d’int:

function vote(uint proposalId) external {
  // on mettra ici la suite de cette partie
}

Maintenant nous allons récupérer notre votant. Pour cela rien de plus simple: récupérons simplement le struct stocké dans notre mapping via son adresse. Comment ? Si vous vous souvenez du premier article, via l’objet msg présent dans toutes les fonctions en solidity, nous pouvons récupérer la variable sender:

Voter sender = voters[msg.sender]; // Ne récupérez pas encore ceci, nous allons le modifier

SAUF QUE ! Nous devons ici faire un choix. Comme nous récupérons une valeur stockée dans la blockchain nous devons préciser s’il s’agit ici de storage ou de memory. Pour faire simple, storage va permettre de persister la valeur là ou memory va agir un peu comme de la ram. Memory est essentiellement utilisé pour faire des calculs intermédiaires avant de stocker tout ça dans le storage. Cependant, même si nous n’avons pas encore vraiment parlé des gas, ce choix aura un coût (et bien réel). Memory va être bien moins énergivore que storage, donc coûter moins de gas fees. Cependant ici nous avons besoin de changer notre votant pour indiquer son vote, donc nous allons avoir besoin du storage:

Voter storage sender = voters[msg.sender];

Nous allons pouvoir maintenant faire trois actions

  • Dire que le votant a voté
  • Enregistrer pour qui il a voté
  • Ajouter un vote au compte de la proposition choisie

Pour cela je vous épargne les explications, le code est très simple:

sender.voted = true;
sender.vote = proposalId;

proposals[proposalId].voteCount += 1;

Et voilà notre fonction de vote prête à l’emploi !

Bin euh… Du coup on peut voter deux fois ?

Ah oui pardon, si nous enregistrons le fait que quelqu’un ait déjà voté c’est justement pour éviter ce genre de cas de figure ! Et pour éviter ça nous avons une fonction bien pratique en solidity: le require. Contrairement au Javascript (tout ne pouvait pas être identique) require est ici une fonction que l’on appelle à l’intérieur des fonction pour vérifier une condition et quitter la fonction si besoin. Par exemple, si notre votant a déjà voté, nous refuserons son vote et retournerons un message d’erreur. La syntaxe est comme suit:

require(!sender.voted, "Already voted.");

Nous allons donc placer ce code en dessous de l’initialisation de notre variable sender, et au dessus des modifications qui suivent. Ce qui nous donne du coup la fonction suivante:

function vote(uint proposalId) external {
    Voter storage sender = voters[msg.sender];
    require(!sender.voted, "Already voted.");
    sender.voted = true;
    sender.vote = proposalId;

    proposals[proposalId].voteCount += 1; 
}

Détecter un gagnant

Et oui, maintenant que nous savons voter, la dernière étape est de savoir qui donc a remporté la victoire ! Pour cela nous allons itérer sur nos proposals et regarder lequel a le plus de votes. À peu de choses près vous avez normalement ce qu’il faut pour faire la fonction vous même. Nous devons aussi préciser le format de sortie, ici nous allons simplement retourner une string. Et comme notre fonction peut être aussi bien appelée à l’intérieur qu’à l’extérieur de notre contract (soyons fous) elle sera publique. Et comme nous n’allons rien stocker dans la blockchain, elle sera en view.

Nous allons donc déclarer une fonction publique en view qui ne prend pas de paramètres et qui retourne une string.

function winningProposal() public view returns (string memory) {
  // on mettra ici la suite de cette partie
}

Nous allons donc maintenant créer la variable dont nous allons avoir besoin: le nombre de votes de la proposition gagnante. Nous allons l’initier à 0. Si en bouclant sur notre tableau nous trouvons un nombre de vote plus grand que cette variable, nous stockons le nouveau nombre de votes ainsi que le nom du gagnant. Une fois la boucle finie nous sommes garantis d’avoir la proposition avec le plus de votes. Ensuite nous rédigeons la boucle, pour cela rien de sorcier: la syntaxe est la même que dans n’importe quel autre langage de ce type.

function winningProposal() public view returns (string memory) {
    string memory winnerName;
    uint winningVoteCount = 0;
    for (uint i = 0; i < proposals.length; i++) {
        if (proposals[i].voteCount > winningVoteCount) {
            winningVoteCount = proposals[i].voteCount;
            winnerName = proposals[i].name;
        }
    }
    return winnerName;
}

Et voilà ! Notre contract est désormais terminé: nous pouvons créer le contract avec des propositions, puis voter pour ces propositions et enfin voir laquelle a emporté le plus de votes en étant sûrs que chaque personne a bien voté une seule fois !

Voici le code complet:

pragma solidity ^0.8.4;

contract MyVoteContract {
    struct Voter {
        bool voted;
        uint vote;
    }

    struct Proposal {
        string name;
        uint voteCount;
    }

    mapping(address => Voter) public voters;

    Proposal[] public proposals;

    constructor(string[] memory proposalNames) {
        for (uint i = 0; i < proposalNames.length; i++) {
            proposals.push(Proposal({
                name: proposalNames[i],
                voteCount: 0
            }));
        }
    }

    function get_proposals() public view returns (Proposal[] memory) {
        return proposals;
    }

    function vote(uint proposalId) external {
        Voter storage sender = voters[msg.sender];
        require(!sender.voted, "Already voted.");
        sender.voted = true;
        sender.vote = proposalId;

        proposals[proposalId].voteCount += 1; 
    }

    function winningProposal() public view returns (string memory) {
        string memory winnerName;
        uint winningVoteCount = 0;
        for (uint i = 0; i < proposals.length; i++) {
            if (proposals[i].voteCount > winningVoteCount) {
                winningVoteCount = proposals[i].voteCount;
                winnerName = proposals[i].name;
            }
        }
        return winnerName;
    }
}

Aller plus loin pour apprendre Solidity

Sur l’exemple de la documentation vous avez certaines subtilités en plus, notamment le fait de permettre au créateur du contract d’assigner un droit de vote à quelqu’un afin d’éviter les multi-comptes. Je vous laisse regarder mais nous reparlerons plus en détail du système de droits plus tard !

Vous pouvez aussi réfléchir (voire le faire) à comment permettre à chacun de voter plusieurs fois en s’assurant que personne ne vote pour une même proposition plusieurs fois ?

Et une fois ceci fait, pourquoi ne pas récupérer les x plus votées plutôt que juste la première ? Afin de pouvoir utiliser ce contract pour par exemple décider d’une liste de choses à faire en vacances entre amis ou en famille.

Lancez vous des petits défis du style pour pratiquer tout ce que l’on vient de voir. Quant à moi je vous prépare quelques articles en plus sur le sujet afin d’approfondir, car croyez moi: il y a beaucoup à dire sur Solidity !