Photo by Pierre Borthiry - Peiobty / Unsplash

Comment créer un contrat upgradable Solidity à l'aide d'OpenZeppelin Proxy et ProxyAdmin

blockchain 19 mars 2023

L'un des principaux défis de la programmation de contrats intelligents sur Ethereum est que les contrats sont immuables, ce qui signifie qu'une fois qu'ils sont déployés, ils ne peuvent plus être modifiés. Cela peut poser des problèmes si des bogues sont découverts ou si des modifications doivent être apportées au contrat. Heureusement, il existe des solutions pour rendre les contrats intelligents "upgradables", ce qui signifie qu'ils peuvent être mis à jour sans perdre les données existantes.

Dans ce tutoriel, nous allons explorer comment créer un contrat upgradable Solidity à l'aide du framework OpenZeppelin et de ses contrats Proxy et ProxyAdmin. Nous allons créer un contrat simple de gestion de tokens et utiliser les contrats Proxy et ProxyAdmin pour le rendre upgradable.

Prérequis

Pour suivre ce tutoriel, vous aurez besoin des éléments suivants :

  • Un environnement de développement Solidity (par exemple, Remix ou Truffle).
  • Un compte Ethereum.
  • Les contrats OpenZeppelin (nous utiliserons la version 4.4.0 dans ce tutoriel).
  • Des connaissances de base en Solidity et en programmation de contrats intelligents Ethereum.

Dans ce tutoriel vous aurez idéalement besoin de :

  1. Node.js : Le code JavaScript de ce tutoriel utilise Node.js pour exécuter des scripts en ligne de commande. Si vous n'avez pas Node.js installé, vous pouvez le télécharger sur le site officiel.
  2. Truffle : Truffle est un framework de développement de contrats intelligents pour Ethereum. Vous pouvez l'installer en utilisant la commande suivante : npm install -g truffle
  3. Ganache : Ganache est un simulateur de réseau Ethereum local qui vous permet de tester vos contrats intelligents. Vous pouvez le télécharger à partir du site officiel.
  4. OpenZeppelin : OpenZeppelin est une bibliothèque de contrats intelligents réutilisables qui implémentent des fonctionnalités courantes telles que les jetons et les contrats de sécurité. Pour utiliser les contrats OpenZeppelin dans votre projet, vous pouvez les installer à l'aide de la commande suivante : npm install @openzeppelin/contracts-upgradeable
  5. Un fournisseur web3 : Dans le code de ce tutoriel, nous utilisons le fournisseur web3 fourni par Truffle pour interagir avec le réseau Ethereum. Vous pouvez l'importer dans votre code comme suit :
const { web3 } = require('@openzeppelin/test-helpers');

Étape 1 : Créer le contrat initial

La première étape consiste à créer le contrat initial qui sera utilisé comme base pour les versions ultérieures. Dans cet exemple, nous allons créer un contrat de gestion de tokens simple qui permettra aux utilisateurs de transférer des tokens entre eux.

Créez un nouveau fichier Solidity et nommez-le Token.sol. Ajoutez les imports suivants pour importer les contrats OpenZeppelin :

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

Ensuite, créez le contrat Token qui hérite des contrats ERC20 et Ownable d'OpenZeppelin. Ajoutez les fonctions de base pour transférer des tokens et définir les propriétaires :

contract Token is ERC20, Ownable {
    constructor() ERC20("My Token", "MTK") {}

    function mint(address account, uint256 amount) public onlyOwner {
        _mint(account, amount);
    }

    function transfer(address recipient, uint256 amount) public virtual override returns (bool) {
        _transfer(_msgSender(), recipient, amount);
        return true;
    }
}

Dans ce contrat, nous avons créé une nouvelle instance de ERC20 et défini le nom et le symbole du token dans le constructeur. Nous avons également ajouté une fonction mint qui permettra à l'administrateur de mint des nouveaux tokens. Enfin, nous avons ajouté une fonction transfer qui hérite de la fonction transfer d'OpenZeppelin mais qui doit être définie pour les tokens personnalisés.

Étape 2 : Créer le contrat Proxy

Maintenant que nous avons notre contrat initial, nous allons créer un contrat Proxy qui servira d'interface entre l'utilisateur et le contrat initial. Le contrat Proxy permettra aux utilisateurs d'interagir avec le contrat initial sans connaître l'adresse exacte du contrat initial et sans avoir à redéployer le contrat initial chaque fois que des modifications sont apportées.

Dans le même fichier Token.sol, ajoutez le code suivant pour créer le contrat Proxy :

contract TokenProxy is ERC20Upgradeable {
    constructor(address _logic, address _admin, bytes memory _data) payable {
        assert(_ADMIN_SLOT == bytes32(uint256(keccak256("eip1967.proxy.admin")) - 1));
        assert(_OWNER_SLOT == bytes32(uint256(keccak256("eip1967.proxy.owner")) - 1));
        _setAdmin(_admin);
        _setImplementation(_logic);

        if (_data.length > 0) {
            (bool success,) = _logic.delegatecall(_data);
            require(success, "TokenProxy: failed to initialize");
        }
    }
}

Dans ce contrat, nous avons créé une nouvelle instance de ERC20Upgradeable au lieu de ERC20 pour pouvoir rendre notre contrat upgradable. Nous avons également ajouté un constructeur qui prend trois arguments : l'adresse du contrat initial (_logic), l'adresse de l'administrateur (_admin) et les données d'initialisation (_data).

Lorsque le contrat Proxy est déployé, il est initialisé en appelant le contrat initial avec les données d'initialisation. Si le contrat initial n'a pas besoin d'initialisation, _data peut être laissé vide.

Étape 3 : Créer le contrat ProxyAdmin

Maintenant que nous avons notre contrat Proxy, nous avons besoin d'un moyen de le déployer et de le gérer. C'est là que le contrat ProxyAdmin d'OpenZeppelin entre en jeu. Le contrat ProxyAdmin permet de déployer et de gérer les contrats Proxy.

Dans le même fichier Token.sol, ajoutez le code suivant pour créer le contrat TokenProxyAdmin :

contract TokenProxyAdmin is OwnableUpgradeable {
    function upgrade(TokenProxy proxy, address newImplementation) public onlyOwner {
        proxy.upgradeTo(newImplementation);
    }

    function getProxyImplementation(TokenProxy proxy) public view onlyOwner returns (address) {
        return proxy.implementation();
    }

    function getProxyAdmin(TokenProxy proxy) public view onlyOwner returns (address) {
        return proxy.admin();
    }
}

Dans ce contrat, nous avons créé trois fonctions : upgrade, getProxyImplementation et getProxyAdmin.

La fonction upgrade permet à l'administrateur de mettre à jour le contrat initial en appelant la fonction upgradeTo du contrat Proxy.

Les fonctions getProxyImplementation et getProxyAdmin permettent à l'administrateur de récupérer l'adresse du contrat initial et l'adresse de l'administrateur du contrat Proxy.

Étape 4 : Déployer le contrat

Maintenant que nous avons créé les contrats nécessaires, nous allons les déployer sur le réseau Ethereum. Pour ce faire, nous avons besoin d'un script de déploiement qui instancie les contrats et les relie les uns aux autres.

Créez un nouveau fichier JavaScript et nommez-le deploy.js. Ajoutez le code suivant pour importer les contrats OpenZeppelin et le fournisseur web3 :

const { web3 } = require('@openzeppelin/test-helpers');
const { deployProxy, upgradeProxy } = require('@openzeppelin/truffle-upgrades');
const Token = artifacts.require('Token');
const TokenProxy = artifacts.require('TokenProxy');
const TokenProxyAdmin = artifacts.require('TokenProxyAdmin');

Ensuite, ajoutez le code suivant pour déployer le contrat initial et le contrat Proxy :

module.exports = async function (deployer, network, accounts) {
  const initialSupply = web3.utils.toWei('10000', 'ether');
  const [admin] = accounts;

  const token = await deployProxy(Token, [], { deployer });
  const tokenProxy = await deployProxy(TokenProxy, [token.address, admin, []], { deployer, unsafeAllowCustomTypes: true });

  console.log(`Token deployed at address: ${token.address}`);
  console.log(`Token proxy deployed at address: ${tokenProxy.address}`);
};

Dans ce script, nous avons défini l'approvisionnement initial du token (initialSupply) et l'adresse de l'administrateur (admin). Ensuite, nous avons déployé le contrat initial en appelant deployProxy et stocké l'adresse du contrat dans la variable token. Nous avons ensuite déployé le contrat Proxy en appelant deployProxy avec les arguments suivants : l'adresse du contrat initial (token.address), l'adresse de l'administrateur (admin) et un tableau vide ([]) pour les données d'initialisation.

Enfin, nous avons imprimé les adresses des contrats déployés.

Étape 5 : Mettre à jour le contrat

Maintenant que nous avons déployé notre contrat initial et notre contrat Proxy, nous pouvons mettre à jour le contrat initial sans perdre les données existantes. Pour mettre à jour le contrat, nous devons créer une nouvelle version du contrat initial et la déployer. Ensuite, nous utiliserons le contrat ProxyAdmin pour mettre à jour le contrat Proxy afin qu'il pointe vers la nouvelle version.

Imaginons que nous souhaitons ajouter une fonction permettant de bloquer les transferts des tokens, nous pourrions écrire un TokenV2.sol tel que celui-ci :

// TokenV2.sol
// Nouvelle version du contrat de jeton ERC20 avec une fonctionnalité supplémentaire

pragma solidity ^0.8.0;

import "./Token.sol";

contract TokenV2 is Token {
    bool private _transfersPaused;

    function pauseTransfers() public onlyOwner {
        _transfersPaused = true;
    }

    function unpauseTransfers() public onlyOwner {
        _transfersPaused = false;
    }

    function transfersPaused() public view returns (bool) {
        return _transfersPaused;
    }

    function transfer(address recipient, uint256 amount) public virtual override returns (bool) {
        require(!_transfersPaused, "Transfers are currently paused");
        return super.transfer(recipient, amount);
    }
}

Créez un nouveau fichier JavaScript et nommez-le upgrade.js. Ajoutez le code suivant pour importer les contrats OpenZeppelin et le fournisseur web3 :

const { web3 } = require('@openzeppelin/test-helpers');
const { upgradeProxy } = require('@openzeppelin/truffle-upgrades');
const TokenV2 = artifacts.require('TokenV2');
const TokenProxy = artifacts.require('TokenProxy');
const TokenProxyAdmin = artifacts.require('TokenProxyAdmin');

Ensuite, ajoutez le code suivant pour mettre à jour le contrat initial et le contrat Proxy :

module.exports = async function (deployer, network, accounts) {
  const [admin] = accounts;

  const tokenProxy = await TokenProxy.deployed();
  const tokenProxyAdmin = await TokenProxyAdmin.deployed();

  const newToken = await deployProxy(TokenV2, [], { deployer });
  await upgradeProxy(tokenProxy.address, newToken.address, { proxyAdmin: tokenProxyAdmin.address });

  console.log(`Token upgraded to version 2 at address: ${tokenProxy.address}`);
};

Dans ce code, nous avons créé une nouvelle version du contrat initial (TokenV2) en appelant deployProxy(TokenV2, [], { deployer }) et stocké l'adresse de la nouvelle version dans la variable newToken.

Ensuite, nous avons appelé upgradeProxy(tokenProxy.address, newToken.address, { proxyAdmin: tokenProxyAdmin.address }) pour mettre à jour le contrat Proxy. La fonction upgradeProxy prend deux arguments : l'adresse du contrat Proxy à mettre à jour et l'adresse de la nouvelle version du contrat initial.

Enfin, nous avons imprimé l'adresse du contrat Proxy mis à jour.

Conclusion

Dans ce tutoriel, nous avons vu comment créer un contrat upgradable Solidity à l'aide d'OpenZeppelin Proxy et ProxyAdmin. Nous avons créé un contrat initial, un contrat Proxy et un contrat ProxyAdmin. Nous avons également vu comment déployer les contrats et comment mettre à jour le contrat initial en utilisant la fonction upgradeProxy.

Les contrats Proxy et ProxyAdmin d'OpenZeppelin sont une solution élégante pour rendre les contrats intelligents upgradables. En utilisant ces contrats, vous pouvez mettre à jour votre contrat sans perdre les données existantes, ce qui est particulièrement utile pour les contrats de grande envergure qui nécessitent des mises à jour régulières.

Aller plus loin

Je peux intervenir sur vos projets grâce à ma nouvelle offre micro-services. Si vous avez besoin d'un microservice qui n'existe pas encore, n'hésitez pas à me le soumettre via le chat de la plateforme en passant par l'un de mes microservices déjà disponibles.

Mots clés