Solidity par l’Exemple

Vote

Le contrat suivant est assez complexe, mais il présente de nombreuses caractéristiques de Solidity. Il implémente un contrat de vote. Bien entendu, le principal problème du vote électronique est de savoir comment attribuer les droits de vote aux bonnes personnes et éviter les manipulations. Nous ne résoudrons pas tous les problèmes ici, mais nous montrerons au moins comment le vote délégué peut être effectué de manière à ce que le dépouillement soit à la fois automatique et totalement transparent.

L’idée est de créer un contrat par bulletin de vote, en donnant un nom court à chaque option. Ensuite, le créateur du contrat qui agit à titre de président donnera le droit de vote à chaque adresse individuellement.

Les personnes derrière les adresses peuvent alors choisir de voter elles-mêmes ou de déléguer leur vote à une personne en qui elles ont confiance.

A la fin du temps de vote, la winningProposal() (proposition gagnante) retournera la proposition avec le plus grand nombre de votes.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
/// @title Vote par délegation.
contract Ballot {
    // Ceci déclare un type complexe, représentant
    // un votant, qui sera utilisé
    // pour les variables plus tard.
    struct Voter {
        uint weight; // weight (poids), qui s'accumule avec les délégations
        bool voted;  // si true, cette personne a déjà voté
        address delegate; // Cette personne a délégué son vote à
        uint vote;   // index la la proposition choisie
    }

    // Type pour une proposition.
    struct Proposal {
        bytes32 name;   // nom court (jusqu'à 32 octets)
        uint voteCount; // nombre de votes cumulés
    }

    address public chairperson;

    // Ceci déclare une variable d'état qui stocke
    // un élément de structure 'Voters' pour  chaque votant.
    mapping(address => Voter) public voters;

    // Un tableau dynamique de structs `Proposal`.
    Proposal[] public proposals;
<<<<<<< HEAD

/// Créé un nouveau bulletin pour choisir l’un des proposalNames. constructor(bytes32[] memory proposalNames) {

>>>>>>> 47d77931747aba8e364452537d989b795df7ca04

chairperson = msg.sender; voters[chairperson].weight = 1;

// Pour chacun des noms proposés, // crée un nouvel objet proposal // à la fin du tableau. for (uint i = 0; i < proposalNames.length; i++) {

// Proposal({…}) créé un objet temporaire // Proposal et proposals.push(…) // l’ajoute à la fin du tableau proposals. proposals.push(Proposal({

name: proposalNames[i], voteCount: 0

}));

}

}

// Donne à un voter un droit de vote pour ce scrutin. // Peut seulement être appelé par chairperson. function giveRightToVote(address voter) external {

// Si le premier argument passé à require s’évalue // à false, l’exécution s’arrete et tous les changements // à l’état et aux soldes sont annulés. // Cette opération consommait tout le gas dans // d’anciennes versions de l’EVM, plus maintenant. // Il est souvent une bonne idée d’appeler require // pour vérifier si les appels de fonctions // s’effectuent correctement. // Comme second argument, vous pouvez fournir une // phrase explicative de ce qui s’est mal passé. require(

msg.sender == chairperson, « Only chairperson can give right to vote. »

); require(

!voters[voter].voted, « The voter already voted. »

); require(voters[voter].weight == 0); voters[voter].weight = 1;

}

/// Delegue son vote au votant to. function delegate(address to) external {

// assigne les références Voter storage sender = voters[msg.sender]; require(!sender.voted, « You already voted. »);

require(to != msg.sender, « Self-delegation is disallowed. »);

// Relaie la délégation tant que to // est également en délégation de vote. // En général, ce type de boucles est très dangereux, // puisque s’il tourne trop longtemps, l’opération // pourrait demander plus de gas qu’il n’est possible // d’en avoir dans un bloc. // Dans ce cas, la délégation ne se ferait pas, // mais dans d’autres circonstances, ces boucles // peuvent complètement paraliser un contrat. while (voters[to].delegate != address(0)) {

to = voters[to].delegate;

// On a trouvé une boucle dans la chaine // de délégations => interdit. require(to != msg.sender, « Found loop in delegation. »);

}

// Comme sender est une référence, ceci // modifie voters[msg.sender].voted Voter storage delegate_ = voters[to];

// Voters cannot delegate to wallets that cannot vote. require(delegate_.weight >= 1); sender.voted = true; sender.delegate = to; if (delegate_.voted) {

// Si le délégué a déjà voté, // on ajoute directement le vote aux autres proposals[delegate_.vote].voteCount += sender.weight;

} else {

// Sinon, on l’ajoute au poids de son vote. delegate_.weight += sender.weight;

}

}

/// Voter (incluant les procurations par délégation) /// pour la proposition proposals[proposal].name. function vote(uint proposal) external {

Voter storage sender = voters[msg.sender]; require(!sender.voted, « Already voted. »); sender.voted = true; sender.vote = proposal;

// Si proposal n’est pas un index valide, // une erreur sera levée et l’exécution annulée proposals[proposal].voteCount += sender.weight;

}

/// @dev Calcule la proposition gagnante /// en prenant tous les votes précédents en compte. function winningProposal() public view

returns (uint winningProposal_)

{

uint winningVoteCount = 0; for (uint p = 0; p < proposals.length; p++) {

if (proposals[p].voteCount > winningVoteCount) {

winningVoteCount = proposals[p].voteCount; winningProposal_ = p;

}

}

}

// Appelle la fonction winningProposal() pour avoir // l’index du gagnant dans le tableau de propositions // et retourne le nom de la proposition gagnante. function winnerName() external view

returns (bytes32 winnerName_)

{

winnerName_ = proposals[winningProposal()].name;

}

}

Améliorations possibles

À l’heure actuelle, de nombreuses opérations sont nécessaires pour attribuer les droits de vote à tous les participants. Pouvez-vous trouver un meilleur moyen ?

Enchères à l’aveugle

Dans cette section, nous allons montrer à quel point il est facile de créer un contrat d’enchères à l’aveugle sur Ethereum. Nous commencerons par une enchère ouverte où tout le monde pourra voir les offres qui sont faites, puis nous prolongerons ce contrat dans une enchère aveugle où il n’est pas possible de voir l’offre réelle avant la fin de la période de soumission.

Enchère ouverte simple

L’idée générale du contrat d’enchère simple suivant est que chacun peut envoyer ses offres pendant une période d’enchère. Les ordres incluent l’envoi d’argent / éther afin de lier les soumissionnaires à leur offre. Si l’enchère est la plus haute, l’enchérisseur qui avait fait l’offre la plus élevée auparavant récupère son argent. Après la fin de la période de soumission, le contrat doit être appelé manuellement pour que le bénéficiaire reçoive son argent - les contrats ne peuvent pas s’activer eux-mêmes.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract SimpleAuction {
    // Paramètres de l'enchère
    // temps unix absolus (secondes depuis 01-01-1970)
    // ou des durées en secondes.
    address payable public beneficiary;
    uint public auctionEndTime;

    // État actuel de l'enchère.
    address public highestBidder;
    uint public highestBid;

    // Remboursements autorisés d'enchères précédentes
    mapping(address => uint) pendingReturns;

    // Mis à true à la fin, interdit tout changement.
    // Par defaut à `false`, comme un grand.
    bool ended;

    // Évènements déclenchés aux changements.
    event HighestBidIncreased(address bidder, uint amount);
    event AuctionEnded(address winner, uint amount);

    // Errors that describe failures.

    // The triple-slash comments are so-called natspec
    // comments. They will be shown when the user
    // is asked to confirm a transaction or
    // when an error is displayed.

    /// The auction has already ended.
    error AuctionAlreadyEnded();
    /// There is already a higher or equal bid.
    error BidNotHighEnough(uint highestBid);
    /// The auction has not ended yet.
    error AuctionNotYetEnded();
    /// The function auctionEnd has already been called.
    error AuctionEndAlreadyCalled();

    /// Create a simple auction with `biddingTime`
    /// seconds bidding time on behalf of the
    /// beneficiary address `beneficiaryAddress`.
    constructor(
        uint biddingTime,
        address payable beneficiaryAddress
    ) {
        beneficiary = beneficiaryAddress;
        auctionEndTime = block.timestamp + biddingTime;
    }

    /// Faire une offre avec la valeur envoyée
    /// avec cette transaction.
    /// La valeur ne sera remboursée que si
    // l'enchère est perdue.
    function bid() external payable {
        // Aucun argument n'est nécessaire, toute
        // l'information fait déjà partie
        // de la transaction. Le mot-clé payable
        // est requis pour autoriser la fonction
        // à recevoir de l'Ether.

        // Annule l'appel si l'enchère est termminée
        if (block.timestamp > auctionEndTime)
            revert AuctionAlreadyEnded();

        // Rembourse si l'enchère est trop basse
        if (msg.value <= highestBid)
            revert BidNotHighEnough(highestBid);

        if (highestBid != 0) {
            // Renvoyer l'argent avec un simple
            // highestBidder.send(highestBid) est un risque de sécurité
            // car ça pourrait déclencher un appel à un contrat.
            // Il est toujours plus sûr de laisser les utilisateurs
            // retirer leur argent eux-mêmes.
            pendingReturns[highestBidder] += highestBid;
        }
        highestBidder = msg.sender;
        highestBid = msg.value;
        emit HighestBidIncreased(msg.sender, msg.value);
    }

    /// Retirer l'argent d'une enchère dépassée
    function withdraw() external returns (bool) {
        uint amount = pendingReturns[msg.sender];
        if (amount > 0) {
            // Il est important de mettre cette valeur à zéro car l'utilisateur
            // pourrait rappeler cette fonction avant le retour de `send`.
            pendingReturns[msg.sender] = 0;

            // msg.sender is not of type `address payable` and must be
            // explicitly converted using `payable(msg.sender)` in order
            // use the member function `send()`.
            if (!payable(msg.sender).send(amount)) {
                // No need to call throw here, just reset the amount owing
                pendingReturns[msg.sender] = amount;
                return false;
            }
        }
        return true;
    }

    /// Met fin à l'enchère et envoie
    /// le montant de l'enchère la plus haute au bénéficiaire.
    function auctionEnd() external {
        // C'est une bonne pratique de structurer les fonctions qui
        // intéragissent avec d'autres contrats (appellent des
        // fonctions ou envoient de l'Ether) en trois phases:
        // 1. Vérifier les conditions
        // 2. éffectuer les actions (potentiellement changeant les conditions)
        // 3. interagir avec les autres contrats
        // Si ces phases sont mélangées, l'autre contrat pourrait rappeler
        // le contrat courant et modifier l'état ou causer des effets
        // (paiements en Ether par ex) qui se produiraient plusieurs fois.
        // Si des fonctions appelées en interne effectuent des appels
        // à des contrats externes, elles doivent aussi êtres considérées
        // comme concernées par cette norme.

        // 1. Conditions
        if (block.timestamp < auctionEndTime)
            revert AuctionNotYetEnded();
        if (ended)
            revert AuctionEndAlreadyCalled();

        // 2. Éffets
        ended = true;
        emit AuctionEnded(highestBidder, highestBid);

        // 3. Interaction
        beneficiary.transfer(highestBid);
    }
}

Enchère aveugle

L’enchère ouverte précédente est étendue en une enchère aveugle dans ce qui suit. L’avantage d’une enchère aveugle est qu’il n’y a pas de pression temporelle vers la fin de la période de soumission. La création d’une enchère aveugle sur une plate-forme informatique transparente peut sembler une contradiction, mais la cryptographie vient à la rescousse.

Pendant la période de soumission, un soumissionnaire n’envoie pas son offre, mais seulement une version hachée de celle-ci. Puisqu’il est actuellement considéré comme pratiquement impossible de trouver deux valeurs (suffisamment longues) dont les valeurs de hachage sont égales, le soumissionnaire s’engage à l’offre par cela. Après la fin de la période de soumission, les soumissionnaires doivent révéler leurs offres : Ils envoient leurs valeurs en clair et le contrat vérifie que la valeur de hachage est la même que celle fournie pendant la période de soumission.

Un autre défi est de savoir comment rendre l’enchère contraignante et aveugle en même temps : La seule façon d’éviter que l’enchérisseur n’envoie pas l’argent après avoir gagné l’enchère est de le lui faire envoyer avec l’enchère. Puisque les transferts de valeur ne peuvent pas être aveuglés dans Ethereum, tout le monde peut voir la valeur.

Le contrat suivant résout ce problème en acceptant toute valeur supérieure à l’offre la plus élevée. Comme cela ne peut bien sûr être vérifié que pendant la phase de révélation, certaines offres peuvent être invalides, et c’est fait exprès (il fournit même un marqueur explicite pour placer des offres invalides avec des transferts de grande valeur) : Les soumissionnaires peuvent brouiller la concurrence en plaçant plusieurs offres invalides hautes ou basses.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract BlindAuction {
    struct Bid {
        bytes32 blindedBid;
        uint deposit;
    }

    address payable public beneficiary;
    uint public biddingEnd;
    uint public revealEnd;
    bool public ended;

    mapping(address => Bid[]) public bids;

    address public highestBidder;
    uint public highestBid;

    // Remboursements autorisés d'enchères précédentes
    mapping(address => uint) pendingReturns;

    event AuctionEnded(address winner, uint highestBid);

    // Errors that describe failures.

    /// The function has been called too early.
    /// Try again at `time`.
    error TooEarly(uint time);
    /// The function has been called too late.
    /// It cannot be called after `time`.
    error TooLate(uint time);
    /// The function auctionEnd has already been called.
    error AuctionEndAlreadyCalled();

    /// Les Modifiers sont une façon pratique de valider des entrées.
    /// `onlyBefore` est appliqué à `bid` ci-dessous:
    /// Le corps de la fonction sera placé dans le modifier
    /// où `_` est placé.
    modifier onlyBefore(uint time) {
        if (block.timestamp >= time) revert TooLate(time);
        _;
    }
    modifier onlyAfter(uint time) {
        if (block.timestamp <= time) revert TooEarly(time);
        _;
    }

    constructor(
        uint biddingTime,
        uint revealTime,
        address payable beneficiaryAddress
    ) {
        beneficiary = beneficiaryAddress;
        biddingEnd = block.timestamp + biddingTime;
        revealEnd = biddingEnd + revealTime;
    }

    /// Placer une enchère à l'aveugle avec `_blindedBid` =
    /// keccak256(abi.encodePacked(value, fake, secret)).
    ///  L'éther envoyé n'est remboursé que si l'enchère est correctement
    /// révélée dans la phase de révélation. L'offre est valide si
    /// l'éther envoyé avec l'offre est d'au moins "valeur" et
    /// "fake" n'est pas true. Régler "fake" à true et envoyer
    /// envoyer un montant erroné sont des façons de masquer l'enchère
    /// mais font toujours le dépot requis. La même addresse peut placer
    /// plusieurs ordres
    function bid(bytes32 _blindedBid)
        external
        payable
        onlyBefore(biddingEnd)
    {
        bids[msg.sender].push(Bid({
            blindedBid: blindedBid,
            deposit: msg.value
        }));
    }

    /// Révèle vos ench1eres aveugles. Vous serez remboursé pour toutes
    /// les enchères invalides et toutes les autres exceptée la plus haute
    /// le cas échéant.
    function reveal(
        uint[] calldata values,
        bool[] calldata fakes,
        bytes32[] calldata secrets
    )
        external
        onlyAfter(biddingEnd)
        onlyBefore(revealEnd)
    {
        uint length = bids[msg.sender].length;
        require(values.length == length);
        require(fakes.length == length);
        require(secrets.length == length);

        uint refund;
        for (uint i = 0; i < length; i++) {
            Bid storage bidToCheck = bids[msg.sender][i];
            (uint value, bool fake, bytes32 secret) =
                    (values[i], fakes[i], secrets[i]);
            if (bidToCheck.blindedBid != keccak256(abi.encodePacked(value, fake, secret))) {
                // L'enchère n'a pas été révélée.
                // Ne pas rembourser.
                continue;
            }
            refund += bidToCheck.deposit;
            if (!fake && bidToCheck.deposit >= value) {
                if (placeBid(msg.sender, value))
                    refund -= value;
            }
            // Rendre impossible un double remboursement
            bidToCheck.blindedBid = bytes32(0);
        }
        payable(msg.sender).transfer(refund);
    }

    /// Se faire rembourser une enchère battue.
    function withdraw() public {
        uint amount = pendingReturns[msg.sender];
        if (amount > 0) {
            // Il est important de mettre cette valeur à zéro car l'utilisateur
            // pourrait rappeler cette fonction avant le retour de `send`.
            // (voir remarque sur conditions -> effets -> interaction).
            pendingReturns[msg.sender] = 0;

            payable(msg.sender).transfer(amount);
        }
    }

    /// Met fin à l'enchère et envoie
    /// le montant de l'enchère la plus haute au bénéficiaire.
    function auctionEnd()
        external
        onlyAfter(revealEnd)
    {
        if (ended) revert AuctionEndAlreadyCalled();
        emit AuctionEnded(highestBidder, highestBid);
        ended = true;
        beneficiary.transfer(highestBid);
    }
    // Cette fonction interne ("internal") ne peut être appelée que
    // que depuis l'intérieur du contrat (ou ses contrats dérivés).
    function placeBid(address bidder, uint value) internal
            returns (bool success)
    {
        if (value <= highestBid) {
            return false;
        }
        if (highestBidder != address(0)) {
            // Rembourse la précédent leader.
            pendingReturns[highestBidder] += highestBid;
        }
        highestBid = value;
        highestBidder = bidder;
        return true;
    }
}

Achat distant sécurisé

Purchasing goods remotely currently requires multiple parties that need to trust each other. The simplest configuration involves a seller and a buyer. The buyer would like to receive an item from the seller and the seller would like to get money (or an equivalent) in return. The problematic part is the shipment here: There is no way to determine for sure that the item arrived at the buyer.

There are multiple ways to solve this problem, but all fall short in one or the other way. In the following example, both parties have to put twice the value of the item into the contract as escrow. As soon as this happened, the money will stay locked inside the contract until the buyer confirms that they received the item. After that, the buyer is returned the value (half of their deposit) and the seller gets three times the value (their deposit plus the value). The idea behind this is that both parties have an incentive to resolve the situation or otherwise their money is locked forever.

This contract of course does not solve the problem, but gives an overview of how you can use state machine-like constructs inside a contract.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract Purchase {
    uint public value;
    address payable public seller;
    address payable public buyer;

    enum State { Created, Locked, Release, Inactive }
    // The state variable has a default value of the first member, `State.created`
    State public state;

    modifier condition(bool condition_) {
        require(condition_);
        _;
    }

    /// Only the buyer can call this function.
    error OnlyBuyer();
    /// Only the seller can call this function.
    error OnlySeller();
    /// The function cannot be called at the current state.
    error InvalidState();
    /// The provided value has to be even.
    error ValueNotEven();

    modifier onlyBuyer() {
        if (msg.sender != buyer)
            revert OnlyBuyer();
        _;
    }

    modifier onlySeller() {
        if (msg.sender != seller)
            revert OnlySeller();
        _;
    }

    modifier inState(State state_) {
        if (state != state_)
            revert InvalidState();
        _;
    }

    event Aborted();
    event PurchaseConfirmed();
    event ItemReceived();
    event SellerRefunded();

    // Ensure that `msg.value` is an even number.
    // Division will truncate if it is an odd number.
    // Check via multiplication that it wasn't an odd number.
    constructor() payable {
        seller = payable(msg.sender);
        value = msg.value / 2;
        if ((2 * value) != msg.value)
            revert ValueNotEven();
    }

    /// Annule l'achat et rembourse l'ether du dépot.
    /// Peut seulement être appelé par le vendeur
    /// avant le verrouillage du contrat
    function abort()
        external
        onlySeller
        inState(State.Created)
    {
        emit Aborted();
        state = State.Inactive;
        // We use transfer here directly. It is
        // reentrancy-safe, because it is the
        // last call in this function and we
        // already changed the state.
        seller.transfer(address(this).balance);
    }

    /// Confirm the purchase as buyer.
    /// Transaction has to include `2 * value` ether.
    /// The ether will be locked until confirmReceived
    /// is called.
    function confirmPurchase()
        external
        inState(State.Created)
        condition(msg.value == (2 * value))
        payable
    {
        emit PurchaseConfirmed();
        buyer = payable(msg.sender);
        state = State.Locked;
    }

    /// Confirm that you (the buyer) received the item.
    /// This will release the locked ether.
    function confirmReceived()
        external
        onlyBuyer
        inState(State.Locked)
    {
        emit ItemReceived();
        // It is important to change the state first because
        // otherwise, the contracts called using `send` below
        // can call in again here.
        state = State.Release;

        buyer.transfer(value);
    }

    /// This function refunds the seller, i.e.
    /// pays back the locked funds of the seller.
    function refundSeller()
        external
        onlySeller
        inState(State.Release)
    {
        emit SellerRefunded();
        // It is important to change the state first because
        // otherwise, the contracts called using `send` below
        // can call in again here.
        state = State.Inactive;

        seller.transfer(3 * value);
    }
}

Canaux de micro-paiement

Dans cette section, nous allons apprendre comment construire une implémentation simple d’un canal de paiement. Il utilise des signatures cryptographiques pour effectuer des transferts répétés d’Ether entre les mêmes parties en toute sécurité, instantanément et sans frais de transaction. Pour ce faire, nous devons comprendre comment signer et vérifier les signatures, et configurer le canal de paiement.

Création et vérification des signatures

Imaginez qu’Alice veuille envoyer une quantité d’Ether à Bob, c’est-à-dire qu’Alice est l’expéditeur et Bob est le destinataire.

Alice n’a qu’à envoyer des messages cryptographiquement signés hors chaîne (par exemple par e-mail) à Bob et cela sera très similaire à la rédaction de chèques.

Les signatures sont utilisées pour autoriser les transactions et sont un outil généraliste à la disposition des contrats intelligents. Alice construira un simple contrat intelligent qui lui permettra de transmettre des Ether, mais d’une manière inhabituelle, au lieu d’appeler une fonction elle-même pour initier un paiement, elle laissera Bob le faire, et donc payer les frais de transaction.

Le contrat fonctionnera comme suit :

  1. Alice déploie le contrat ReceiverPays en y attachant suffisamment d’éther pour couvrir les paiements qui seront effectués.

  2. Alice autorise un paiement en signant un message avec sa clé privée.

  3. Alice envoie le message signé cryptographiquement à Bob. Le message n’a pas besoin d’être gardé secret (vous le comprendrez plus tard), et le mécanisme pour l’envoyer n’a pas d’importance.

  4. Bob réclame leur paiement en présentant le message signé au contrat intelligent, il vérifie l’authenticité du message et libère ensuite les fonds.

<<<<<<< HEAD Création de la signature ————————

Alice n’a pas besoin d’interagir avec le réseau Ethereum pour signer la transaction, le processus est complètement hors ligne. Dans ce tutoriel, nous allons signer les messages dans le navigateur en utilisant web3.js et MetaMask. En particulier, nous utiliserons la méthode standard décrite dans EIP-762, car elle offre un certain nombre d’autres avantages en matière de sécurité. =======

  1. Alice deploys the ReceiverPays contract, attaching enough Ether to cover the payments that will be made.

  2. Alice authorises a payment by signing a message with her private key.

  3. Alice sends the cryptographically signed message to Bob. The message does not need to be kept secret (explained later), and the mechanism for sending it does not matter.

  4. Bob claims his payment by presenting the signed message to the smart contract, it verifies the authenticity of the message and then releases the funds.

Creating the signature

Alice does not need to interact with the Ethereum network to sign the transaction, the process is completely offline. In this tutorial, we will sign messages in the browser using web3.js and MetaMask, using the method described in EIP-712, as it provides a number of other security benefits. >>>>>>> 47d77931747aba8e364452537d989b795df7ca04

/// Hasher d'abord simplifie un peu les choses
var hash = web3.sha3("message to sign");
web3.personal.sign(hash, web3.eth.defaultAccount, function () {...});
<<<<<<< HEAD

Notez que web3.personal.sign préfixe les données signées de la longueur du message. Mais comme nous avons hashé en premier, le message sera toujours exactement 32 octets de long, et donc ce préfixe de longueur est toujours le même, ce qui facilite tout.

Que signer

Dans le cas d’un contrat qui effectue des paiements, le message signé doit inclure :

  1. Adresse du destinataire

  2. le montant à transférer

  3. Protection contre les attaques de rediffusion

Une attaque de rediffusion se produit lorsqu’un message signé est réutilisé pour revendiquer l’autorisation pour une deuxième action. Pour éviter les attaques par rediffusion, nous utiliserons la même méthode que pour les transactions Ethereum elles-mêmes, ce qu’on appelle un nonce, qui est le nombre de transactions envoyées par un compte. Le contrat intelligent vérifiera si un nonce est utilisé plusieurs fois.

Il existe un autre type d’attaques de redifussion, il se produit lorsque le propriétaire déploie un smart contract ReceiverPays, effectue certains paiements, et ensuite détruit le contrat. Plus tard, il décide de déployer ReceiverPays encore une fois, mais le nouveau contrat ne peut pas connaître les nonces utilisés dans le déploiement précédent, donc l’attaquant peut réutiliser les anciens messages.

Alice peut s’en protéger, notamment en incluant l’adresse du contrat dans le message, et seulement les messages contenant l’adresse du contrat lui-même seront acceptés. Cette fonctionnalité se trouve dans les deux premières lignes de la fonction claimPayment() du contrat complet à la fin de ce chapitre.

Encoder les arguments

Maintenant que nous avons déterminé quelles informations inclure dans le message signé, nous sommes prêts à assembler le message, à le hacher, et le signer. Par souci de simplicité, nous ne faisons que concaténer les données. La bibliothèque ethereumjs-abi fournit une fonction appelée soliditySHA3 qui imite le comportement de la fonction keccak256 de Solidity appliquée aux arguments codés en utilisant abi.encododePacked. En résumé, voici une fonction JavaScript qui crée la signature appropriée pour l’exemple ReceiverPays : =======

The web3.eth.personal.sign prepends the length of the message to the signed data. Since we hash first, the message will always be exactly 32 bytes long, and thus this length prefix is always the same.

What to Sign

For a contract that fulfils payments, the signed message must include:

  1. The recipient’s address.

  2. The amount to be transferred.

  3. Protection against replay attacks.

A replay attack is when a signed message is reused to claim authorization for a second action. To avoid replay attacks we use the same technique as in Ethereum transactions themselves, a so-called nonce, which is the number of transactions sent by an account. The smart contract checks if a nonce is used multiple times.

Another type of replay attack can occur when the owner deploys a ReceiverPays smart contract, makes some payments, and then destroys the contract. Later, they decide to deploy the RecipientPays smart contract again, but the new contract does not know the nonces used in the previous deployment, so the attacker can use the old messages again.

Alice can protect against this attack by including the contract’s address in the message, and only messages containing the contract’s address itself will be accepted. You can find an example of this in the first two lines of the claimPayment() function of the full contract at the end of this section.

Packing arguments

Now that we have identified what information to include in the signed message, we are ready to put the message together, hash it, and sign it. For simplicity, we concatenate the data. The ethereumjs-abi library provides a function called soliditySHA3 that mimics the behaviour of Solidity’s keccak256 function applied to arguments encoded using abi.encodePacked. Here is a JavaScript function that creates the proper signature for the ReceiverPays example: >>>>>>> 47d77931747aba8e364452537d989b795df7ca04

// recipient est l'addresse à payer.
// amount, en wei, spécifie combien d'Ether doivent être envoyés.
// nonce peut être n'importe quel nombre unique pour prévenir les attques par redifusion
// contractAddress est utilisé pour éviter les attaque par redifusion de messages inter-contrats
function signPayment(recipient, amount, nonce, contractAddress, callback) {
    var hash = "0x" + ethereumjs.ABI.soliditySHA3(
        ["address", "uint256", "uint256", "address"],
        [recipient, amount, nonce, contractAddress]
    ).toString("hex");

    web3.personal.sign(hash, web3.eth.defaultAccount, callback);
}

<<<<<<< HEAD Récupérer le signataire du message en Solidity ———————————————-

En général, les signatures ECDSA se composent de deux paramètres, r et s. Les signatures dans Ethereum incluent un troisième paramètre appelé « v », qui peut être utilisé pour récupérer la clé privée du compte qui a été utilisée pour signer le message, l’expéditeur de la transaction. La solidité offre une fonction intégrée ecrecover qui accepte un message avec les paramètres r, s et v et renvoie l’adresse qui a été utilisée pour signer le message.

Extraire les paramètres de signature

Les signatures produites par web3.js sont la concaténation de r, s et v, donc la première étape est de re-séparer ces paramètres. Cela peut être fait sur le client, mais le faire à l’intérieur du smart contract signifie qu’un seul paramètre de signature peut être envoyé au lieu de trois. Diviser un tableau d’octets en plusieurs parties est un peu compliqué. Nous utiliserons l”assembleur en ligne pour faire le travail dans la fonction splitSignature (la troisième fonction dans le contrat complet à la fin du présent chapitre).

Calculer le hash du message

In general, ECDSA signatures consist of two parameters, r and s. Signatures in Ethereum include a third parameter called v, that you can use to verify which account’s private key was used to sign the message, and the transaction’s sender. Solidity provides a built-in function ecrecover that accepts a message along with the r, s and v parameters and returns the address that was used to sign the message.

Extracting the Signature Parameters

Signatures produced by web3.js are the concatenation of r, s and v, so the first step is to split these parameters apart. You can do this on the client-side, but doing it inside the smart contract means you only need to send one signature parameter rather than three. Splitting apart a byte array into its constituent parts is a mess, so we use inline assembly to do the job in the splitSignature function (the third function in the full contract at the end of this section). >>>>>>> 47d77931747aba8e364452537d989b795df7ca04

Le smart contract doit savoir exactement quels paramètres ont été signés, et doit donc recréer le message à partir des paramètres et utiliser cette fonction pour la vérification des signatures. Les fonctions prefixed et recoverSigner s’occupent de cela et leur utilisation peut se trouver dans la fonction claimPayment.

Le contrat complet

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract ReceiverPays {
    address owner = msg.sender;

    mapping(uint256 => bool) usedNonces;

    constructor() payable {}

    function claimPayment(uint256 amount, uint256 nonce, bytes memory signature) external {
        require(!usedNonces[nonce]);
        usedNonces[nonce] = true;

        // Cette ligne recrée le message signé par le client
        bytes32 message = prefixed(keccak256(abi.encodePacked(msg.sender, amount, nonce, this)));

        require(recoverSigner(message, signature) == owner);

        payable(msg.sender).transfer(amount);
    }
<<<<<<< HEAD

/// détruit le contrat et réclame son solde. function shutdown() public {

>>>>>>> 47d77931747aba8e364452537d989b795df7ca04

require(msg.sender == owner); selfdestruct(payable(msg.sender));

}

/// methodes de signature. function splitSignature(bytes memory sig)

internal pure returns (uint8 v, bytes32 r, bytes32 s)

{

require(sig.length == 65);

assembly {

// premiers 32 octets, après le préfixe r := mload(add(sig, 32)) // 32 octets suivants s := mload(add(sig, 64)) // Octet final (premier du prochain lot de 32) v := byte(0, mload(add(sig, 96)))

}

return (v, r, s);

}

function recoverSigner(bytes32 message, bytes memory sig)

internal pure returns (address)

{

(uint8 v, bytes32 r, bytes32 s) = splitSignature(sig);

return ecrecover(message, v, r, s);

}

/// construit un hash préfixé pour mimer le comportement de eth_sign. function prefixed(bytes32 hash) internal pure returns (bytes32) {

return keccak256(abi.encodePacked( »x19Ethereum Signed Message:n32 », hash));

}

}

Écrire un canal de paiement simple

Alice va maintenant construire une implémentation simple mais complète d’un canal de paiement. Les canaux de paiement utilisent des signatures cryptographiques pour effectuer des virements répétés d’Ether en toute sécurité, instantanément et sans frais de transaction.

Qu’est-ce qu’un canal de paiement ?

Les canaux de paiement permettent aux participants d’effectuer des transferts répétés d’Ether sans utiliser de transactions. Cela signifie que les délais et frais associés aux transactions peuvent être évités. Nous allons explorer un canal de paiement unidirectionnel simple entre deux parties (Alice et Bob). Son utilisation implique trois étapes :

<<<<<<< HEAD
  1. Alice déploie un smart contract avec de l’Ether. Cela « ouvre » (opens) le canal de paiement.

  2. Alice signe des messages qui précisent combien d’éther est dû au destinataire. Cette étape est répétée pour chaque paiement.

  3. Bob « ferme » (closes) le canal de paiement, retirant leur part d’Ether et renvoyant le reste à l’expéditeur.

Note

Non seulement les étapes 1 et 3 exigent des transactions Ethereum, mais l’étape 2 signifie que l’expéditeur transmet un message signé cryptographiquement au destinataire par des moyens hors chaîne (par exemple, par courrier électronique). Cela signifie que seulement deux transactions sont nécessaires pour traiter un nombre quelconque de transferts.

Bob est assuré de recevoir ses fonds parce que le contrat bloque les fonds en Ether et respecte des ordres valides et signés. Le smart contract impose également un délai d’attente, Alice est donc assurée de recouvrer ses fonds même si le bénéficiaire refuse de fermer le canal. C’est aux participants d’un canal de paiement de décider combien de temps il doit rester ouvert. Pour une transaction de courte durée, comme payer un cybercafé pour chaque minute d’accès au réseau, ou dans le cas d’une relation de plus longue durée, comme le versement d’un salaire horaire à un employé, un paiement pourrait durer des mois ou des années.

Ouverture du canal de paiement

  1. Bob « closes » the payment channel, withdrawing his portion of the Ether and sending the remainder back to the sender.

Note

Only steps 1 and 3 require Ethereum transactions, step 2 means that the sender transmits a cryptographically signed message to the recipient via off chain methods (e.g. email). This means only two transactions are required to support any number of transfers.

Bob is guaranteed to receive his funds because the smart contract escrows the Ether and honours a valid signed message. The smart contract also enforces a timeout, so Alice is guaranteed to eventually recover her funds even if the recipient refuses to close the channel. It is up to the participants in a payment channel to decide how long to keep it open. For a short-lived transaction, such as paying an internet café for each minute of network access, the payment channel may be kept open for a limited duration. On the other hand, for a recurring payment, such as paying an employee an hourly wage, the payment channel may be kept open for several months or years.

Opening the Payment Channel

>>>>>>> 47d77931747aba8e364452537d989b795df7ca04

Pour ouvrir le canal de paiement, Alice déploie le contrat, y attachant de l’Ether en dépot et spécifiant le destinataire prévu, ainsi qu’une durée de vie maximale du canal. C’est la fonction SimplePaymentChannel dans le contrat.

Effectuer des paiements

Alice effectue des paiements en envoyant des messages signés à Bob. Cette étape est entièrement réalisée en dehors du réseau Ethereum. Les messages sont signés cryptographiquement par l’expéditeur puis transmis directement au destinataire.

Chaque message contient les informations suivantes :

  • L’adresse du contrat, utilisé pour empêcher les attaques de redifussion par contrats croisés.

  • Le montant total d’Ether qui est dû au bénéficiaire jusqu’alors.

Un canal de paiement est fermé une seule fois, à la fin d’une série de virements. De ce fait, un seul des messages envoyés sera échangé. C’est pourquoi chaque message spécifie un montant total cumulatif d’éther dû, plutôt que le montant total d’un micropaiement individuel. Le destinataire réclamera naturellement le message le plus récent parce que c’est celui dont le total est le plus élevé. Le nonce par message n’est plus nécessaire, car le smart contract ne va honorer qu’un seul message. L’adresse du contrat intelligent est toujours utilisée pour éviter qu’un message destiné à un canal de paiement ne soit utilisé pour un autre canal.

Voici le code javascript modifié pour signer cryptographiquement un message du chapitre précédent :

function constructPaymentMessage(contractAddress, amount) {
    return abi.soliditySHA3(
        ["address", "uint256"],
        [contractAddress, amount]
    );
}

function signMessage(message, callback) {
    web3.eth.personal.sign(
        "0x" + message.toString("hex"),
        web3.eth.defaultAccount,
        callback
    );
}

// contractAddress détectera la rediffusion de messages à d'autres contrats.
// amount, en wei, précise combien d'Ether doivent être envoyés.

function signPayment(contractAddress, amount, callback) {
    var message = constructPaymentMessage(contractAddress, amount);
    signMessage(message, callback);
}

Fermeture du canal de paiement

Lorsque Bob est prêt à recevoir leurs ses, il est temps de fermer le canal de paiement en appelant une fonction close sur le smart contract. La fermeture du canal paie au destinataire l’Ether qui lui est dû et détruit le contrat, en renvoyant tout Ether restant à Alice. Pour fermer le canal, Bob doit fournir un message signé par Alice.

Le contrat doit vérifier que le message contient une signature valide de l’expéditeur. Le processus de vérification est le même que celui utilisé par le destinataire. Les fonctions Solidity isValidSignature et recoverSigner fonctionnent de la même manière que leurs fonctions JavaScript dans la section précédente. Ce dernier est emprunté au Le contrat ReceiverPays du chapitre précédent.

La fonction close ne peut être appelée que par le destinataire du canal de paiement, qui enverra naturellement le message de paiement le plus récent car c’est celui qui comporte le plus haut total dû. Si l’expéditeur était autorisé à appeler cette fonction, il pourrait fournir un message avec un montant inférieur et escroquer le destinataire de ce qui lui est dû.

La fonction vérifie que le message signé correspond aux paramètres donnés. Si tout se passe bien, le destinataire reçoit sa part d’Ether, et l’expéditeur reçoit le reste par selfdestruct (autodestruction) du contrat. Vous pouvez voir la fonction close dans le contrat complet.

Expiration du canal

Bob peut fermer le canal de paiement à tout moment, mais s’il ne le fait pas, Alice a besoin d’un moyen de récupérer les fonds bloqués. Une durée d”expiration a été définie au moment du déploiement du contrat. Une fois cette heure atteinte, Alice peut appeler pour récupérer leurs fonds. Vous pouvez voir la fonction claimTimeout dans le contrat déployé.

Après l’appel de cette fonction, Bob ne peut plus recevoir d’Ether. Il est donc important que Bob ferme le canal avant que l’expiration ne soit atteinte.

Le contrat complet

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract SimplePaymentChannel {
    address payable public sender;      // Le compte emvoyant les paiements.
    address payable public recipient;   // Le compte destinataire des paiements.
    uint256 public expiration;  // Expitration si le destinataire ne clot pas le canal.


    constructor (address payable recipientAddress, uint256 duration)
        payable
    {
        sender = payable(msg.sender);
        recipient = recipientAddress;
        expiration = block.timestamp + duration;
    }

    /// Le destinataire peut clore le canal à tout moment en présentant le dernier montant
    /// signé par l'expéditeur des fonds. Le destinataire se verra verser ce montant,
    /// et le reste sera rendu à l'emetteur des fonds.
    function close(uint256 amount, bytes memory signature) external {
        require(msg.sender == recipient);
        require(isValidSignature(amount, signature));

        recipient.transfer(amount);
        selfdestruct(sender);
    }

    /// L'emetteur peut modifier la date d'expiration à tout moment
    function extend(uint256 newExpiration) external {
        require(msg.sender == sender);
        require(newExpiration > expiration);

        expiration = newExpiration;
    }

    /// Si l'expiration est atteinte avant cloture par le destinataire,
    /// l'Ether est renvoyé à l'emetteur
    function claimTimeout() external {
        require(now >= expiration);
        selfdestruct(sender);
    }

    function isValidSignature(uint256 amount, bytes memory signature)
        internal
        view
        returns (bool)
    {
        bytes32 message = prefixed(keccak256(abi.encodePacked(this, amount)));

        // check that the signature is from the payment sender
        return recoverSigner(message, signature) == sender;
    }

    /// Toutes les fonctions ci-dessous sont tirées
    /// du chapitre 'créer et vérifier les signatures'.

    function splitSignature(bytes memory sig)
        internal
        pure
        returns (uint8 v, bytes32 r, bytes32 s)
    {
        require(sig.length == 65);

        assembly {
            // first 32 bytes, after the length prefix
            r := mload(add(sig, 32))
            // second 32 bytes
            s := mload(add(sig, 64))
            // final byte (first byte of the next 32 bytes)
            v := byte(0, mload(add(sig, 96)))
        }

        return (v, r, s);
    }

    function recoverSigner(bytes32 message, bytes memory sig)
        internal
        pure
        returns (address)
    {
        (uint8 v, bytes32 r, bytes32 s) = splitSignature(sig);

        return ecrecover(message, v, r, s);
    }

    /// construit un hash préfixé pour mimer le comportement de eth_sign.
    function prefixed(bytes32 hash) internal pure returns (bytes32) {
        return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash));
    }
}

Note

La fonction splitSignature est très simple et n’utilise pas tous les contrôles de sécurité. Une implémentation réelle devrait utiliser une bibliothèque plus rigoureusement testée de ce code, tel que le fait openzepplin avec version of this code.

Vérification des paiements

Contrairement à notre chapitre précédent, les messages dans un canal de paiement ne sont pas appliqués tout de suite. Le destinataire conserve la trace du dernier message et le fait parvenir au réseau quand il est temps de fermer le canal de paiement. Cela signifie qu’il est essentiel que le destinataire effectue sa propre vérification de chaque message. Sinon, il n’y a aucune garantie que le destinataire sera en mesure d’être payé à la fin.

Le destinataire doit vérifier chaque message à l’aide du processus suivant :

  1. Vérifiez que l’adresse du contact dans le message correspond au canal de paiement.

  2. Vérifiez que le nouveau total est le montant prévu.

  3. Vérifier que le nouveau total ne dépasse pas la quantité d’éther déposée.

  4. Vérifiez que la signature est valide et provient de l’expéditeur du canal de paiement.

Nous utiliserons la librairie ethereumjs-util pour écrire ces vérifications. L’étape finale peut se faire de plusieurs façons, ici en JavaScript, Le code suivant emprunte la fonction constructPaymentMessage du code JavaScript de signature ci-dessous :

// Cette ligne mine le fonctionnement de la méthode JSON-RPC de eth_sign.
function prefixed(hash) {
    return ethereumjs.ABI.soliditySHA3(
        ["string", "bytes32"],
        ["\x19Ethereum Signed Message:\n32", hash]
    );
}

function recoverSigner(message, signature) {
    var split = ethereumjs.Util.fromRpcSig(signature);
    var publicKey = ethereumjs.Util.ecrecover(message, split.v, split.r, split.s);
    var signer = ethereumjs.Util.pubToAddress(publicKey).toString("hex");
    return signer;
}

function isValidSignature(contractAddress, amount, signature, expectedSigner) {
    var message = prefixed(constructPaymentMessage(contractAddress, amount));
    var signer = recoverSigner(message, signature);
    return signer.toLowerCase() ==
        ethereumjs.Util.stripHexPrefix(expectedSigner).toLowerCase();
}

Les contrats modulaires

Une approche modulaire pour créer vos contrats vous aide pour réduire sa complexité et améliore la lisibilité qui va aider pour identifier les bugs et les vulnérabilités durant le développement et la revue du code. If you specify and control the behaviour or each module in isolation, the interactions you have to consider are only those between the module specifications and not every other moving part of the contract. In the example below, the contract uses the move method of the Balances library to check that balances sent between addresses match what you expect. In this way, the Balances library provides an isolated component that properly tracks balances of accounts. It is easy to verify that the Balances library never produces negative balances or overflows and the sum of all balances is an invariant across the lifetime of the contract.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;

library Balances {
    function move(mapping(address => uint256) storage balances, address from, address to, uint amount) internal {
        require(balances[from] >= amount);
        require(balances[to] + amount >= balances[to]);
        balances[from] -= amount;
        balances[to] += amount;
    }
}

contract Token {
    mapping(address => uint256) balances;
    using Balances for *;
    mapping(address => mapping (address => uint256)) allowed;

    event Transfer(address from, address to, uint amount);
    event Approval(address owner, address spender, uint amount);

    function transfer(address to, uint amount) external returns (bool success) {
        balances.move(msg.sender, to, amount);
        emit Transfer(msg.sender, to, amount);
        return true;

    }

    function transferFrom(address from, address to, uint amount) external returns (bool success) {
        require(allowed[from][msg.sender] >= amount);
        allowed[from][msg.sender] -= amount;
        balances.move(from, to, amount);
        emit Transfer(from, to, amount);
        return true;
    }

    function approve(address spender, uint tokens) external returns (bool success) {
        require(allowed[msg.sender][spender] == 0, "");
        allowed[msg.sender][spender] = tokens;
        emit Approval(msg.sender, spender, tokens);
        return true;
    }

    function balanceOf(address tokenOwner) external view returns (uint balance) {
        return balances[tokenOwner];
    }
}