Tutoriel·9 min de lecture·Par Solingo

Comprendre les Mappings Solidity — Du Basique aux Patterns Avancés

Maîtrisez les mappings Solidity, de la syntaxe de base aux patterns itérables, en comprenant leurs impacts sur les coûts en gas.

# Comprendre les Mappings Solidity — Du Basique aux Patterns Avancés

Les mappings sont l'une des structures de données les plus fondamentales en Solidity. Contrairement aux arrays, ils offrent un accès constant O(1) et ne nécessitent pas de connaître à l'avance toutes les clés. Pourtant, leur utilisation va bien au-delà du simple stockage clé-valeur.

Les Bases des Mappings

Un mapping en Solidity fonctionne comme une table de hachage où chaque clé est mappée à une valeur. La syntaxe est simple :

contract BasicMapping {

mapping(address => uint256) public balances;

function setBalance(address user, uint256 amount) public {

balances[user] = amount;

}

function getBalance(address user) public view returns (uint256) {

return balances[user]; // Returns 0 if key doesn't exist

}

}

Point crucial : une clé inexistante retourne toujours la valeur par défaut du type (0 pour uint, false pour bool, address(0) pour address). Il n'existe pas de notion de "undefined" comme en JavaScript.

Mappings Imbriqués

Pour des structures plus complexes, on peut imbriquer des mappings :

contract NestedMappings {

// Token allowances: owner => spender => amount

mapping(address => mapping(address => uint256)) public allowances;

// Multi-sig: proposal => voter => hasVoted

mapping(uint256 => mapping(address => bool)) public votes;

function approve(address spender, uint256 amount) public {

allowances[msg.sender][spender] = amount;

}

function vote(uint256 proposalId) public {

require(!votes[proposalId][msg.sender], "Already voted");

votes[proposalId][msg.sender] = true;

}

}

Cette technique est utilisée massivement dans les tokens ERC20 pour gérer les allowances.

Le Problème de l'Itération

Le défaut majeur des mappings est qu'on ne peut pas les itérer. Solidity ne stocke pas la liste des clés, uniquement les valeurs hachées. Ce code est impossible :

// ❌ IMPOSSIBLE

for (address user in balances) {

// ...

}

Cette limitation est intentionnelle : stocker toutes les clés coûterait énormément en gas lors d'ajouts/suppressions.

Pattern 1 : Iterable Mapping avec Array

La solution classique consiste à maintenir un array des clés en parallèle :

contract IterableMapping {

mapping(address => uint256) public balances;

mapping(address => bool) public isUser;

address[] public users;

function addUser(address user, uint256 balance) public {

if (!isUser[user]) {

users.push(user);

isUser[user] = true;

}

balances[user] = balance;

}

function getAllBalances() public view returns (uint256[] memory) {

uint256[] memory allBalances = new uint256[](users.length);

for (uint256 i = 0; i < users.length; i++) {

allBalances[i] = balances[users[i]];

}

return allBalances;

}

}

Coût en gas :

  • Ajout d'un nouvel utilisateur : ~45 000 gas (SSTORE × 2 + array push)
  • Ajout d'un utilisateur existant : ~5 000 gas (SSTORE uniquement)
  • Lecture de tous les balances : O(n) en lecture, peut échouer si n > ~500

Pattern 2 : Mapping avec Linked List

Pour optimiser les suppressions, une linked list peut remplacer l'array :

contract LinkedListMapping {

struct Entry {

uint256 value;

address next;

bool exists;

}

mapping(address => Entry) public entries;

address public head;

uint256 public size;

function add(address key, uint256 value) public {

require(!entries[key].exists, "Key already exists");

entries[key] = Entry({

value: value,

next: head,

exists: true

});

head = key;

size++;

}

function remove(address key) public {

require(entries[key].exists, "Key doesn't exist");

if (key == head) {

head = entries[key].next;

} else {

address current = head;

while (entries[current].next != key) {

current = entries[current].next;

}

entries[current].next = entries[key].next;

}

delete entries[key];

size--;

}

}

Cette approche évite le problème du "array gap" (déplacer le dernier élément lors d'une suppression).

Pattern 3 : EnumerableMap d'OpenZeppelin

Pour la production, utilisez plutôt la librairie battle-tested d'OpenZeppelin :

import "@openzeppelin/contracts/utils/structs/EnumerableMap.sol";

contract UsingEnumerableMap {

using EnumerableMap for EnumerableMap.AddressToUintMap;

EnumerableMap.AddressToUintMap private balances;

function set(address user, uint256 amount) public {

balances.set(user, amount);

}

function getAll() public view returns (

address[] memory keys,

uint256[] memory values

) {

uint256 length = balances.length();

keys = new address[](length);

values = new uint256[](length);

for (uint256 i = 0; i < length; i++) {

(keys[i], values[i]) = balances.at(i);

}

}

}

EnumerableMap combine mapping + array en interne, avec gestion automatique des suppressions.

Coûts en Gas Comparés

Benchmarks sur 100 entrées :

| Opération | Mapping Simple | + Array | EnumerableMap |

|-----------|---------------|---------|---------------|

| Add (first) | 22 000 | 45 000 | 47 000 |

| Add (existing) | 5 000 | 5 000 | 5 000 |

| Remove | 5 000 | 15 000 | 12 000 |

| Iterate all | - | ~200k | ~200k |

Conclusion : n'ajoutez l'itération que si nécessaire. Pour un simple token ERC20, un mapping pur suffit.

Pattern Avancé : Packed Mappings

Pour optimiser le stockage, packez plusieurs valeurs dans un uint256 :

contract PackedMapping {

// Store balance (96 bits) + lastUpdate (160 bits) in one slot

mapping(address => uint256) private packed;

function setBalance(address user, uint96 balance) public {

uint256 data = packed[user];

uint160 lastUpdate = uint160(block.timestamp);

packed[user] = (uint256(balance) << 160) | uint256(lastUpdate);

}

function getBalance(address user) public view returns (

uint96 balance,

uint160 lastUpdate

) {

uint256 data = packed[user];

balance = uint96(data >> 160);

lastUpdate = uint160(data);

}

}

Économie : 20 000 gas économisés par utilisateur (1 SSTORE au lieu de 2).

Quand Utiliser Quoi ?

  • Mapping simple : tokens, allowances, ownership (pas besoin d'itération)
  • Mapping + array : whitelist, airdrop snapshots (petites listes, <500 entrées)
  • EnumerableMap : governance, registry on-chain (sécurité > gas)
  • Linked list : queues, priority lists (nombreuses suppressions)

Les mappings sont puissants mais inflexibles. Choisissez la bonne structure dès le départ car la migration coûte cher en gas.

Prêt à mettre en pratique ?

Applique ces concepts avec des exercices interactifs sur Solingo.

Commencer gratuitement