Adieu transfer() et send()

Publié le 11/01/2026 par Frédéric dans la catégorie "Solidity" Réagir / Commenter

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 à :

  1. Le risque de reentrancy (même avec transfer() ou send(), historiquement considérés comme "sûrs").
  2. Les problèmes de compatibilité avec le gas limit variable (depuis EIP-150 et London Hard Fork).
  3. L’adoption de call avec 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 ?

  1. Problème du gas stipend (2300 gas) :

    • Depuis EIP-150 (2016), transfer() et send() 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
  2. 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.
  3. 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.

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 :

  1. 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");
    }
  2. 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)

  1. Solidity Docs : Address call → Explications sur call, staticcall, et delegatecall.
  2. OpenZeppelin : ReentrancyGuard → Implémentation du verrou contre la reentrancy.
  3. EIP-150 : Gas Cost Changes → Pourquoi transfer()/send() sont devenues obsolètes.

À retenir :

"transfer() et send() 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)