Adieu transfer() et send()
L’abandon progressif des fonctions transfer(), send(), et des limitations sur call.value() dans les bonnes pratiques Solidity modernes (notamment depuis Solidity 0.8.x+ et les recommandations post-2022).
Cette évolution est liée à :
- Le risque de reentrancy (même avec
transfer()ousend(), historiquement considérés comme "sûrs"). - Les problèmes de compatibilité avec le gas limit variable (depuis EIP-150 et London Hard Fork).
- L’adoption de
callavec des vérifications explicites comme nouvelle norme.
Ce qui a changé (2023–2026) :
| Fonction/Méthode | Statut actuel (2026) | Pourquoi abandonnée ? | Alternative recommandée |
|---|---|---|---|
address.transfer() |
Déconseillée (mais toujours fonctionnelle) | - Gas stipend fixe (2300 gas) → Échec si le receveur est un contrat avec logique. | call.value() + vérification manuelle du succès. |
address.send() |
Déconseillée | - Retourne false en cas d’échec (silencieux → risque de bugs). |
call.value() + require(success, "Transfer failed"). |
call.value() |
Nouvelle norme (mais à utiliser avec précaution) | - Flexible (gas personnalisable), mais risque de reentrancy si mal utilisé. | Pattern Checks-Effects-Interactions + ReentrancyGuard (OpenZeppelin). |
staticcall/delegatecall |
Stable, mais restrictions accrues | - delegatecall peut corrompre le stockage si mal utilisé. |
Vérifier les layouts de stockage avec storageLayout (nouveauté Solidity 0.9.x). |
Pourquoi transfer() et send() sont abandonnées ?
-
Problème du gas stipend (2300 gas) :
- Depuis EIP-150 (2016),
transfer()etsend()allouent un gas fixe de 2300 pour l’exécution côté receveur. - Problème : Si le receveur est un contrat avec une logique (ex : un
fallback()qui écrit en stockage), l’opération échouera silencieusement (car 2300 gas sont insuffisants). - Exemple d’échec :
// Ce code peut échouer si `receiver` est un contrat ! address(receiver).transfer(1 ether); // ❌ Revert silencieux si gas > 2300
- Depuis EIP-150 (2016),
-
Fausse sensation de sécurité :
transfer()était historiquement considérée comme "safe" contre la reentrancy (car gas limité).- Réalité : Un attaquant peut forcer un échec de
transfer()en déployant un contrat receveur qui consomme tout le gas, bloquant ainsi les retraits.
-
Comportement incohérent avec les wallets :
- Certains portefeuilles (ex : contrats Gnosis Safe) nécessitent plus de 2300 gas pour traiter les fonds, rendant
transfer()inutilisable.
- Certains portefeuilles (ex : contrats Gnosis Safe) nécessitent plus de 2300 gas pour traiter les fonds, rendant
Comment faire en 2026 ?
✅ Remplacez transfer()/send() par call.value() + vérifications
// Ancienne méthode (déconseillée)
bool success = payable(receiver).sendValue(1 ether);
require(success, "Transfer failed"); // Mais 'success' peut être false sans raison claire!
// Nouvelle méthode (recommandée)
(bool success, ) = payable(receiver).call{value: 1 ether}("");
require(success, "ETH transfer failed");
Pourquoi
call?
- Gas personnalisable (le receveur reçoit tout le gas restant, sauf limite explicite).
- Retourne un booléen clair (pas de silencieux
false).- Compatible avec tous les types de receveurs (wallets, contrats).
✅ Protégez-vous contre la reentrancy
Même avec call.value(), le risque de reentrancy existe. Utilisez :
- Le pattern Checks-Effects-Interactions :
function withdraw() external { uint256 amount = balances[msg.sender]; balances[msg.sender] = 0; // Effet (mettre à jour l'état AVANT l'appel externe) (bool success, ) = msg.sender.call{value: amount}(""); // Interaction require(success, "Transfer failed"); } -
Un
ReentrancyGuard(OpenZeppelin) :import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; contract MyContract is ReentrancyGuard { function withdraw() external nonReentrant { // Logique ici... } }
✅ Pour delegatecall, vérifiez le layout de stockage
Solidity 0.9.x introduit des avertissements explicites si les layouts de stockage sont incompatibles :
// ❌ Risque de corruption si `Library` a un layout différent
(bool success, ) = address(library).delegatecall(abi.encodeWithSignature("foo()"));
// ✅ Solution : Utiliser des bibliothèques avec un layout vérifié
contract MyContract {
struct StorageLayout {
uint256 value;
address owner;
}
StorageLayout private storageLayout;
}
Résumé des actions à prendre
| Ancienne pratique | Nouvelle pratique (2026) |
|---|---|
receiver.transfer(amount) |
(bool success, ) = receiver.call{value: amount}(""); require(success, "Failed") |
receiver.send(amount) |
Même solution que ci-dessus. |
| Pas de protection reentrancy | Utiliser ReentrancyGuard + Checks-Effects-Interactions. |
delegatecall sans vérification |
Vérifier les layouts de stockage avec storageLayout (Solidity 0.9.x). |
Ressources officielles (2026)
- Solidity Docs : Address
call→ Explications surcall,staticcall, etdelegatecall. - OpenZeppelin : ReentrancyGuard → Implémentation du verrou contre la reentrancy.
- EIP-150 : Gas Cost Changes
→ Pourquoi
transfer()/send()sont devenues obsolètes.
À retenir :
"
transfer()etsend()sont comme des ceintures de sécurité cassées : elles donnent une fausse impression de sécurité.call.value()+ des vérifications explicites est la nouvelle norme." — Consensus de la communauté Solidity (2023–2026)