# 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.