Le mappage (mapping) avec Solidity

Mapping Solidity Développement

Le mappage, aussi connu sous le nom de « mapping » est un système de stockage sous forme de clé-valeur indispensable à connaître lorsqu’on apprend le développement avec le langage Solidity. De plus, c’est aussi une structure très simple et pratique à utiliser lorsqu’on la maîtrise alors pourquoi s’en priver.

Afin que le mapping n’ai plus de secrets pour vous, découvrons comment il fonctionne, les différences entre les mappings et les tableaux classiques (array) au travers de différentes exemples afin de développer les meilleurs contrats intelligents sur Ethereum et toutes les blockchains compatibles avec Solidity comme Polygon ou encore Optimism.

C’est quoi un mapping avec Solidity ?

Le mappage ou « mapping » en anglais dans le langage Solidity, est ce que l’on appelle une table de hachage. Ces tables de hachages permettent de stocker les données de manière associative et dans notre cas, sous forme de paires clé-valeur. Elles utilisent un tableau comme support de stockage et utilisent la technique du hachage pour établir un index à partir duquel un élément doit être inséré ou localisé.

Table de hachage mapping Solidity
Représentation graphique du fonctionnement du mapping dans Solidity

Lorsque l’index des données nécessaires est connu, cela permet de retourner extrêmement rapidement les valeurs associées. Par conséquent, les tables de hachage sont des structures de données dans lesquelles les opérations d’insertion et de recherche sont extrêmement rapides, quelle que soit la quantité de données.

Sans plus attendre, voici un exemple simple de l’instanciation d’un mapping dans un contrat intelligent Solidity classique.

pragma solidity >=0.7.0 <0.9.0;

contract TestContrat
{
    // Adresse reliée à un entier non signé
    mapping(address => uint) public userBalance;
}

Quand utiliser le mapping ?

Dans la grande majorité des cas, les mappings sont utilisés pour connecter une adresse unique à un type de valeur correspondant. Par exemple, c’est le système utilisé pour savoir si une adresse à mint ou non un NFT lors d’une nouvelle collection. Néanmoins, elles peuvent aussi prendre des formes plus complexes en imbriquant un mapping dans un autre avec le double mapping.

Ce qu’il faut retenir, c’est que ce type de structure de données permet d’optimiser la vitesse des opérations d’insertion et de recherche. En fonction des travaux que vous allez mener, il est souvent plus intéressant de les utiliser que les tableaux (array).

Enfin, notez que contrairement aux tableaux (array), les mapping ne sont pas itérables. Cela signifie que l’on ne peut pas les parcourir comme un objet ou un tableau classique avec une boucle.

En résumé :

  • Les mappings ne sont pas itérable ;
  • Les mappings n’ont pas de longueur ;
  • Les mappings ne peuvent être utilisés que pour les variables d’état qui servent de types de référence de stockage.

Quelles types de données pour les clés et les valeurs ?

Les clés

En ce qui concerne le type de donnée compatibles, la clé peut contenir n’importe quelle type de donnée courant dans Solidity comme :

  • une adresse Ethereum (adresse) ;
  • une suite de caractère (string) ;
  • des entiers numériques (int ou uint) ;
  • des bytes ou byte
  • d’autres mapping (principe du double mapping)

Les valeurs

Pour les valeurs, vous pouvez stocker n’importe quelle type de données et même des structures (struct), ce qui est terriblement pratique dans certains cas pour conserver les données de vos utilisateurs. Voici quelques exemples pour mieux appréhender ce concept.

// Adresse reliée à un booléen (pour savoir si une adresse à mint un NFT)
mapping(address => bool) public addressInteraction; 

// Adresse reliée à une valeur numérique non signée (pour compter les interactions)
mapping(address => uint) public addressInteractionCount; 

// Structure utilisateur
struct User {
    uint id;
    string username;
    uint balance;
    string description;
    bool admin;
 }

// Adresse reliée à une structure (struct)
mapping(address => User) public userStructs;

// Entier relié à une valeur numérique non signée
mapping(uint => uint) public articleCounter;

Vous l’aurez compris c’est terriblement efficace quand il s’agit de faire des recherches rapidement. Malgré une certaine similarité avec les tableaux (array), il réside d’importantes différences.

Quelle est la différence entre les mappings et les arrays ?

Les tableaux sont plus adaptés à l’itération dans un groupe de données que l’on utilise généralement à l’aide d’une boucle for par rapport aux mappings qui sont plus appropriés lorsque vous pouvez obtenir des valeurs sur la base d’une clé connue et donc, que vous n’avez pas besoin de parcourir les données.

Dans Solidity, l’itération sur un tableau peut être très coûteuse en termes de ressource par rapport à l’extraction de données à partir d’un mapping. La complexité de l’opération va alors se répercuter sur la quantité de gaz nécessaire à la transaction et donc son prix. C’est d’ailleurs l’une des raisons d’être du mappage car il permet justement de faire des économies.

De plus, certains développeurs peuvent vouloir stocker à la fois une valeur et sa clé dans un contrat intelligent pour plus de simplicité. C’est pour cette raison qu’ils créent un tableau de clés qui sert de référence à des données qui peuvent ensuite être récupérées à l’intérieur d’un mapping.

Soyez également très vigilant sur la taille de vos tableaux dans Solidity. En effet, l’itération dans un grand tableau pourrait coûter plus en frais de gaz que la valeur de la transaction, ce qui serait dommage quand le mappage à été prévu pour ça.

Enfin, nous l’avons évoqué en introduction : les mapping ne sont pas itérables contrairement aux tableaux. On peut donc en conclure que l’utilisation des mappings permet une mise en œuvre plus efficiente du contrat intelligent et plus économe en gaz.

ComparaisonMappingArray
ItérableNonOui
RapideOuiNon
Economique en gazOuiNon
Comparatif des mappings et des tableaux dans Solidity

Comment utiliser simplement un mapping avec Solidity

Solidity utiliser mapping facilement

Maintenant que vous savez ce qu’est un mapping, voyons un peu comment s’en servir.

1. La déclaration du mapping dans le contrat intelligent

La première étape est bien évidemment de le déclarer comme n’importe quelle variable. Dans notre cas, nous allons créer un mappage d’une adresse reliée à l’âge de l’utilisateur. Nous allons donc créer le contrat et y ajouter le mapping.

pragma solidity ^0.8.13;

contract MyContrat {

    // Adresse reliée à un nombre
    mapping(address => uint) public userAge;
}

2. Ajouter et modifier des paires clé-valeur

Maintenant ce qui pourrait être intéressant, c’est d’avoir la possibilité de définir la valeur liée à l’adresse. Pour cela, nous allons créer une fonction setAge() par exemple, prenant en référence, l’adresse à modifier ainsi que la valeur de son âge.

// Fonction permettant de définir l'âge lié à une adresse
function setAge(address _address, uint _age) public {
    userAge[_address] = _age; // On met à jour le mapping avec l'âge en paramètre
}

Petite parenthèse importante, la fonction ci-dessus permet à n’importe quel utilisateur de modifier l’âge de n’importe quelle adresse. Si vous souhaitez que ce soit uniquement accessible pour le détenteur de l’adresse, vous devez utiliser la variable msg.sender.

// Fonction permettant de définir l'âge lié à une adresse
function setAge(uint _age) public {
    userAge[msg.sender] = _age; // On met à jour le mapping avec l'âge en paramètre
}

3. Récupérer la valeur de la clé d’un mapping avec Solidité

Ceci étant fait, nous allons maintenant créer une nouvelle fonction pour récupérer l’âge d’une adresse que l’on appelle aussi un getters et qui portera le nom de getAge.

// Fonction permettant de récupérer l'âge lié à une adresse
function getAge(address _address) public view returns (uint) {
    return userAge[_address];
}

Ici, pas besoin d’utiliser msg.sender, sauf si vous souhaitez que seul le propriétaire de l’adresse puisse récupérer son âge. De plus, cette fonction est uniquement nécessaire si votre mapping userAge n’est pas accessible de façon public.

4. Supprimer les données et la clé

Enfin, une dernière fonction importante dans le mapping est celle de la suppression des données. Nous allons donc créer la fonction removeAge() pour supprimer définitivement la valeur en mémoire. Comme nous faisons les choses bien, nous allons utiliser msg.sender afin que seul le propriétaire de l’adresse puisse réaliser cette suppression.

// Fonction permettant de supprimer l'âge lié à une adresse
function removeAge() public {
    delete userAge[msg.sender];
}

Voilà, c’est terminé, si vous avez réalisé ce petit exercice en parallèle, vous devriez avoir le code suivant ou quelque chose qui s’en rapproche :

pragma solidity ^0.8.13;

contract MyContrat {

    // Adresse reliée à un nombre
    mapping(address => uint) public userAge;

    // Fonction permettant de définir l'âge lié à une adresse
    function setAge(uint _age) public {
        userAge[msg.sender] = _age; // On met à jour le mapping avec l'âge en paramètre
    }

    // Fonction permettant de récupérer l'âge lié à une adresse
    function getAge(address _address) public view returns (uint) {
        return userAge[_address];
    }

    // Fonction permettant de supprimer l'âge lié à une adresse
    function removeAge() public {
        delete userAge[msg.sender];
    }
}

Maintenant, vous devriez être capable de faire vos propres mapping assez facilement. Notez que vous pouvez utiliser les mappings avec n’importe quelle type de donnée, ce qui les rend particulièrement puissants.

Mais attention, rien n’est joué et nous allons maintenant découvrir le concept de double mapping.

Contourner la limite d’itération du mapping

Pour certaines raisons, vous auriez voulu rendre le système de mapping itérable et le parcourir comme un tableau classique. Bonne nouvelle, cette limite n’est pas une fatalité et même s’il n’est pas possible de faire une itération avec le mapping, il existe une parade.

Pour cela, nous allons prendre un exemple simple avec une adresse reliée à un nom d’utilisateur. Dans un premier temps, il vous suffit de créer :

  • le mapping userName avec la clé comme adresse et la valeur prenant le nom de l’utilisateur
  • le tableau d’adresse ListUsers qui contiendra les différentes références (adresses) vers le mapping

Ce qui nous donne le code suivant :

// Adresse reliée à une chaîne de caractère
mapping(address => string) public userName;

// Liste des adresses
address[] public ListUsers;

Ensuite, nous allons ajouter une fonction permettant de définir le nom de l’utilisateur en fonction de son adresse. Afin d’éviter d’ajouter la même adresse plusieurs fois, on vérifie quand même son existence au préalable.

// Mise à jour du nom de l'utilisateur
function setName(string memory _name) public {
    if (!userName[msg.sender]) {
        ListUsers.push(msg.sender);
    }

    userName[msg.sender] = _name;
}

Ce qui finalement, nous donne le code suivant :

pragma solidity >=0.7.0 <0.9.0;

contract TestContrat
{
    // Adresse reliée à une chaîne de caractère
    mapping(address => string) public userName;
    
    // Liste des adresses
    address[] public ListUsers;

    // Mise à jour du nom de l'utilisateur
    function setName(string memory _name) public {
        if (!userName[msg.sender]) {
            ListUsers.push(msg.sender);
        }

        userName[msg.sender] = _name;
    }
}

De cette façon, le tableau ListUsers contiendra toutes les adresses des utilisateurs sous cette forme :

  • ListUsers[0] = "0x123456...";
  • ListUsers[1] = "0x456789...";

Néanmoins attention, vous ne devez jamais laisser un tableau dans Solidity devenir trop grand. En cas d’itération de celui-ci, vous allez payer une grande quantité de frais de gaz car l’opération sera complexe.

L’imbrication de mappings (double mapping)

Une autre notion intéressante à connaître est le mappage imbriqué, qui porte aussi le nom de double mappage ou encore double mapping dans Solidity. Concrètement, c’est une méthode qui consiste à imbriquer un mapping dans un autre mapping.

Par exemple, si nous disposons d’une adresse et de l’identifiant d’un token et que nous voulons stocker ces informations pour connaître la quantité totale de token envoyé par l’utilisateur, nous pourrions utiliser le double mappage.

// Adresse reliée à l'ID d'un jeton
mapping (address => mapping (uint256 => tokenID) ) public PaymentDetails;

Nous pourrions ensuite créer une fonction pour ajouter les jetons en cas de paiement de la part de l’utilisateur.

// Fonction permettant d'ajouter un nouveau paiement
function addPaymentDetail (uint _tokenIndex, uint _payment) public {
    uint _totalPaid = PaymentDetails[msg.sender][_tokenIndex] + _payment;
    PaymentDetails[msg.sender][_tokenIndex] = _totalPaid ;
}

Enfin, il pourrait être aussi intéressant d’avoir une fonction pour récupérer la quantité totale en fonction de l’adresse et de l’identifiant du token en question.

// Fonction permettant de récupérer la quantité totale dépensée
function getPaymentDetail(address _address, uint _tokenIndex) public view returns(uint256) {
    return PaymentDetails[_address][_tokenIndex];
}

Dans ce contrat, nous avons donc construit un mapping imbriqué, que l’on appelle PaymentDetails. Celui-ci contient un premier identifiant relatif à l’adresse d’un utilisateur qui contient à son tour l’ID d’un jeton. La finalité nous permet d’avoir la quantité totale de jetons dépensés par identifiant en fonction d’un utilisateur précis.

pragma solidity >=0.7.0 <0.9.0;

contract TestContrat
{
    // Adresse reliée à l'ID d'un jeton
    mapping (address => mapping (uint256 => tokenID) ) public PaymentDetails;
    
    // Fonction permettant d'ajouter un nouveau paiement
    function addPaymentDetail (uint _tokenIndex, uint _payment) public {
        uint _totalPaid = PaymentDetails[msg.sender][_tokenIndex] + _payment;
        PaymentDetails[msg.sender][_tokenIndex] = _totalPaid ;
    }

    // Fonction permettant de récupérer la quantité totale dépensée
    function getPaymentDetail(address _address, uint _tokenIndex) public view returns(uint256) {
        return PaymentDetails[_address][_tokenIndex];
    }
}

Vous devriez maintenant avoir une vision bien plus claire de ce qu’est le double mapping avec le langage de programmation Solidity.

Quelques exemples d’utilisations des mapping avec Solidity

Voyons maintenant quelques contrats avec des méthodes couramment utilisés

1. La balance des utilisateurs de jetons ERC20

Pour ce premier exemple, rien d’ultra exotique après tout ce que vous avez pu voir. Néanmoins, cela ne fait pas de mal

pragma solidity >=0.7.0 <0.9.0;

contract ContratERC20 is Context, IERC20
{
    using SafeMath for uint256;
    using Address for address;

    // Adresse reliée à sa balance en jeton
    mapping (address => uint256) public balances;

...
}

2. Vérifier qu’une adresse à mint ou non un NFT

Autre type de contrat très courant, les collections de NFT avec la possibilité de minter un seul NFT par adresse. Pour cela, nous allons simplement créer notre mapping contenant la liste des adresses ayant minté des NFT ainsi que la fonction permettant de mint.

pragma solidity >=0.7.0 <0.9.0;

contract ContratMintNFT
{

    // Adresse reliée à sa balance en jeton
    mapping (address => bool) public mintAddresses;

    function mintNFT() public {
        require(mintAddresses[msg.sender] == false, "Address has already mint");
        mintAddresses[msg.sender] = true;
        // La suite de votre code pour créer le NFT
    }
}

Une fois que la fonction mintNFT aura été appelée, le mapping avec l’adresse de l’utilisateur sera enregistré avec la valeur true. Si le même utilisateur essaye d’appeler la fonction une nouvelle fois, il va recevoir le message d’erreur tout simplement.

3. Un système d’élection en Solidity avec les mappings

Enfin, encore un exemple un peu plus complexe, voici un système d’élection qui permet de voter pour des candidats.

Au préalable, il faudra ajouter les adresses des candidats au mapping candidateList. Pour cela, vous pouvez faire ça directement avec un constructeur ou bien, créer une fonction pour ajouter et supprimer des candidats uniquement disponible pour le propriétaire du contrat.

Pour en revenir à l’exemple, c’est un des cas d’utilisation typique des DAO où les membres sont censés voter sur les décisions organisationnelles. Pour cela, chaque personne aura la possibilité de voter pour son représentant.

pragma solidity >=0.7.0 <0.9.0;

contract ContratElection
{
    // Adresse reliée à la quantité de vote de chaque candidats
    mapping (address => uint256) public votesReceived;

    // Adresse des candidats
    mapping (address => bool) public candidateList;

    // Adresse des votants
    mapping (address => bool) public addressList;

    // Vérifie si l'adresse du candidat soumis est valide
    function validCandidate(address _address) constant returns (bool) {
        return candidateList[_address];
    }

    // Vote pour un candidat
    function voteForCandidate(address _address) public {
        require(validCandidate(_address) == true, "Address is not candidate");
        require(addressList[msg.sender] == false, "Address has already vote");
        votesReceived[_address]++;
    }

    // Retourne la quantité de vote pour chaque candidats
    function getVoteForCandidate(address _address) public view returns (uint8) {
        require(validCandidate(_address) == true, "Address is not candidate");
        return votesReceived[_address];
    }
}

Bien sûr, il y a différentes façons de faire. Nous aurions également pu ajouter une liste d’adresses précises avec un pouvoir de vote afin d’éviter que n’importe quelle adresse puisse voter. Ce sera quelque chose à ne pas oublier lors de la création de votre propre DAO par exemple !

Conclusion

Les mappings avec Solidity font partie des éléments fondamentaux à maîtriser pour développer des contrats intelligents performants. Rapides et peu coûteux en gaz, ils vont vous permettre de gagner du temps et de l’énergie pour un système de stockage performant.

Les développeurs habitués aux tableaux classiques comme avec le JavaScript peuvent mettre un peu de temps avant de vraiment prendre goût à les utiliser mais une fois que c’est acquis, c’est un vrai plaisir. Maintenant, il ne vous reste plus qu’à pratiquer et écrire quelques contrats intelligents en Solidity pour que cela devienne une habitude.

Partager l'article sur les réseaux sociaux

Auteur

Depuis 2017, je ne cesse d'explorer l'univers du Bitcoin, de la blockchain des crypto monnaies, des NFT et plus récemment, celui du Web3. Après avoir fondé Au Coin du Bloc en 2021, je met à disposition mes connaissances et tente de vulgariser les aspects obscurs pour rendre abordable et compréhensible cet univers naissant dans lequel je crois fermement.

Articles similaires