Solidity – Créer un smart contract évolutif

logo Solidity

Les smart contracts déployés sur la blockchain Ethereum sont par natures immuables, c’est à dire qu’on ne peut en faire évoluer le code source. Dans le contexte du développement d’une application distribuée (DApp) ceci n’est pas concevable.
Le cycle de vie de l’application nécessite obligatoirement de pouvoir faire évoluer le code source d’un smart contract en vue de corriger des bugs ou intégrer de nouvelles évolutions fonctionnelles ou techniques.

Cet article présente une solution basée sur les outils OpenZepplin pour construire et déployer un smart contract puis ensuite le faire évoluer.

Personnellement, j’ai réalisé cette intégration avec Ganache et Truffle mais rien ne vous empêche d’utiliser d’autres outils comme Hardhat par exemple.

Principe général

En tout état de cause il n’est pas possible de modifier un smart contract (SC) déployé sur la blockchain. La solution technique qui permet son évolution est de placer devant le smart contract un proxy. TOUS les appels passent par le proxy qui les redirigent automatiquement et de manière transparente vers le smart contract.

Pour mettre à jour le code du smart contract il faut donc :

  • Déployer une nouvelle version du smart contract
  • Modifier le Proxy pour qu’il renvoie les appels vers la bonne version

L’outillage OpenZepplin prend tout en charge ce qui simplifie grandement l’effort à produire pour atteindre l’objectif.
En fait, 3 smart contracts seront déployés :

  • Un smart contract pour notre implémentation
  • Un smart contract proxy (OpenZepplin)
  • Un smart contract d’administratiuon du proxy (OpenZepplin)

Développement

La suite de l’article montre la mise à jour d’un smart contract déployé localement sur Ganache.

Une première version est déployée, puis une 2ème et enfin une 3ème.

Version 1

La version initiale du smart contract contient simplement une valeur qu’il est possible de mettre à jour ainsi qu’un constructeur :

Smart Contract version 1
// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;
 
/// @author Emmanuel Collin
/// @notice
/// @dev
contract MyContractV1  {
 
    uint private value=0;
 
    constructor(uint v) {
        setValue(v);
 
    }
 
    function getValue() public view returns (uint) {
        return value;
    }
 
    function setValue(uint v) public {
        value = v;
    }
 
}

Déploiement

Le smart contract est déployé avec la valeur initiale de 100.

Déploiement de la version 1
// 2_deploy_contracts.js
const MYCONTRACTV1 = artifacts.require("./MyContractV1.sol");

module.exports = async(deployer) => {

  await deployer.deploy(MYCONTRACTV1, 100);
  const contractInstance = await MYCONTRACTV1.deployed();
  console.log("V1 Smart Contract address : ",contractInstance.address);

  return null;
};

Tests

Utiliser la console Truffle pour tester la version 1 :

truffle(ganache)> const contractV1 = await MyContractV1.deployed(100); 
 (await contractV1.getValue()).toString();undefined 
truffle(ganache)>  (await contractV1.getValue()).toString(); 
'100' 
truffle(ganache)>

Tout est OK, passons à la version 2.

Version 1 en mode évolutif (Upgradable Smart Contract)

Avant de déployer la version 2 il y a quelques étapes à suivre pour rendre notre SC évolutif et compatible avec les outils OpenZepplin.

Installation du plugin OpenZepplin pour Truffle

Il faut installer le plugin pour Truffle qui prendra en charge le travail pour nous :

npm install –save-dev @openzeppelin/truffle-upgrades

Installation plugin pour Truffle
npm install --save-dev @openzeppelin/truffle-upgrades 
npm WARN deprecated uuid@3.3.2: Please upgrade  to version 7 or higher.  Older versions may use Math.random() in certain circumstances, which is known to be problematic.  See https://v8.dev/blog/math-random for details. 

added 441 packages, removed 23 packages, changed 54 packages, and audited 1535 packages in 43s 

139 packages are looking for funding 
  run `npm fund` for details 

35 vulnerabilities (23 moderate, 9 high, 3 critical) 

To address issues that do not require attention, run: 
  npm audit fix 

Some issues need review, and may require choosing 
a different dependency. 

Run `npm audit` for details. 

Modifier le code source

Il y a 2 petites évolutions à faire sur le code de la version 1 pour qu’elle puisse être prise en compte par le plugin :

  • Supprimer le constructeur. Il est remplacer par une fonction (initialize() dans notre cas) qui fait le même travail. Cette fonction sera appelée par le plugin lors du déploiement.
  • Supprimer l’initialisation de la propriété « value »
Version 1 en mode évolutif
// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;

/// @author Emmanuel Collin
/// @notice
/// @dev
contract MyContractV1  {

    uint private value;

    function initialize(uint v) public {
        setValue(v);
    }

    function getValue() public view returns (uint) {
        return value;
    }

    function setValue(uint v) public {
        value = v;
    }

}

Les détails au sujet de ces 2 modifications sont donnés dans ce chapitre de la documentation du plugin.

Déploiement de la version 1

Le code de déploiement évolue aussi sensiblement pour utiliser le plugin :

Déploiement de la version 2
// migrations/3_deploy_upgradeable_contract.js
const { deployProxy } = require('@openzeppelin/truffle-upgrades');

const MyContractV1 = artifacts.require('MyContractV1');

module.exports = async function (deployer) {
  await deployProxy(MyContractV1, [142], { deployer, initializer: 'initialize' });
};

Cette fois le contrat sera déployé avec la valeur initiale 142.

Résultat du déploiement :

Résultat du déploiement
...
3_deploy_upgradeable_contract.js
================================

   Replacing 'MyContractV1'
   ------------------------
   > transaction hash:    0xd6e08f79b326fa0f2a1a6398418338dcb710f906b191236de0e68187ca6be940
   > Blocks: 0            Seconds: 0
   > contract address:    0x9F4117A5a0cC73eAc3a6c136191FceCe2710bb72
   > block number:        9
   > block timestamp:     1665324320
   > account:             0x6742aF78E1E4Ba1883873bEf8DB4829FA0c28b4a
   > balance:             99.97878906
   > gas used:            136669 (0x215dd)
   > gas price:           20 gwei
   > value sent:          0 ETH
   > total cost:          0.00273338 ETH


   Deploying 'ProxyAdmin'
   ----------------------
   > transaction hash:    0x652c7944ce74a2a09eed25eb459bae6e238dbcbf93809fca2ffe2277b935bf29
   > Blocks: 0            Seconds: 0
   > contract address:    0x96B5e8eB02fF5E0700732a8b6e931ABbCAE09e6c
   > block number:        10
   > block timestamp:     1665324321
   > account:             0x6742aF78E1E4Ba1883873bEf8DB4829FA0c28b4a
   > balance:             99.96909372
   > gas used:            484767 (0x7659f)
   > gas price:           20 gwei
   > value sent:          0 ETH
   > total cost:          0.00969534 ETH


   Deploying 'TransparentUpgradeableProxy'
   ---------------------------------------
   > transaction hash:    0xa085a64260fcf1ef09aaccf347fcaafa1c9fdcaeba4c9dafeef21724008414e4
   > Blocks: 0            Seconds: 0
   > contract address:    0x67aa0b798C50EcD4320eFD8Ef0De03Fbe55D30c1
   > block number:        11
   > block timestamp:     1665324322
   > account:             0x6742aF78E1E4Ba1883873bEf8DB4829FA0c28b4a
   > balance:             99.95696146
   > gas used:            606613 (0x94195)
   > gas price:           20 gwei
   > value sent:          0 ETH
   > total cost:          0.01213226 ETH

...

On constate bien le déploiement des 3 smart contracts.

Tests

Déploiement
truffle(ganache)> const contractV1 = await MyContractV1.deployed();
undefined
truffle(ganache)> contractV1.address
'0x67aa0b798C50EcD4320eFD8Ef0De03Fbe55D30c1'
truffle(ganache)>

L’adresse de contractV1 est bien celle du proxy TransparentUpgradeableProxy !

Invocation du SC via le proxy
Test de la version 1 en mode proxy
truffle(ganache)> (await contractV1.getValue()).toString();
'142'

Version 2

Nous pouvons maintenant passer à la version 2 de notre smart contract. Le code source est modifié pour ajouter la fonction add() qui permet d’ajouter un nombre à la propriété « value ».

Développement

Version 2 du smart contract
// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;

/// @author Emmanuel Collin
/// @notice
/// @dev
contract MyContractV2  {

    uint private value;

    function initialize(uint v) public {
        setValue(v);
    }

    function getValue() public view returns (uint) {
        return value;
    }

    function setValue(uint v) public {
        value = v;
    }

    function add(uint v) public {
        setValue(getValue() + v);
    }

}

Déploiement

Déploiement de la version 2
const { upgradeProxy } = require('@openzeppelin/truffle-upgrades');

const MyContractV1 = artifacts.require('MyContractV1');
const MyContractV2 = artifacts.require('MyContractV2');

module.exports = async function (deployer) {
  const existing = await MyContractV1.deployed();
  await upgradeProxy(existing.address, MyContractV2, { deployer });
};

A la ligne 7, on récupère l’adresse du proxy (existing)

A la ligne 8 on modifie le proxy pour déployer la version 2 et pointer dessus.

Résultat du déploiement
3_deploy_upgradeable_contract.js
================================

   Replacing 'MyContractV1'
   ------------------------
   > transaction hash:    0xe0446bf8aa2e00f93952e1f9a27dc0aea46a3f26f3efdf1329764649242f682a
   > Blocks: 0            Seconds: 0
   > contract address:    0xf778A97f5840281122061a52F33585F719E8f4c2
   > block number:        5
   > block timestamp:     1665325731
   > account:             0x0381962bD9B48A87534f6F7C2ceFCAE2b2E87EA0
   > balance:             99.98815564
   > gas used:            136669 (0x215dd)
   > gas price:           20 gwei
   > value sent:          0 ETH
   > total cost:          0.00273338 ETH


   Deploying 'ProxyAdmin'
   ----------------------
   > transaction hash:    0x53f9fb7b22ad68171fb06308793d6c9f1b256b7ec9b96733416ab021ee029d08
   > Blocks: 0            Seconds: 0
   > contract address:    0xCE4Bd3A1f2a37378D30f017db906E57bd6bf1824
   > block number:        6
   > block timestamp:     1665325731
   > account:             0x0381962bD9B48A87534f6F7C2ceFCAE2b2E87EA0
   > balance:             99.9784603
   > gas used:            484767 (0x7659f)
   > gas price:           20 gwei
   > value sent:          0 ETH
   > total cost:          0.00969534 ETH


   Deploying 'TransparentUpgradeableProxy'
   ---------------------------------------
   > transaction hash:    0x6c1a80930a171491d9d45d96a8d61ad18cd9bc9546a88fd9106a3adf26291688
   > Blocks: 0            Seconds: 0
   > contract address:    0x34275c222Cf18e28EbB1ecBc7c4b23aDd4baEbE7
   > block number:        7
   > block timestamp:     1665325732
   > account:             0x0381962bD9B48A87534f6F7C2ceFCAE2b2E87EA0
   > balance:             99.9663278
   > gas used:            606625 (0x941a1)
   > gas price:           20 gwei
   > value sent:          0 ETH
   > total cost:          0.0121325 ETH


   > Saving migration to chain.
   > Saving artifacts
   -------------------------------------
   > Total cost:          0.02456122 ETH


4_deploy_upgradeable_contract_v2.js
===================================

   Replacing 'MyContractV2'
   ------------------------
   > transaction hash:    0xc5231aa4c55e56fa66e4e8caf397f7669fdbd34c768e09a9507ff8c2e0a1986c
   > Blocks: 0            Seconds: 0
   > contract address:    0x44A6b605696cB63ABc6d1670896ae38dF6988C6F
   > block number:        9
   > block timestamp:     1665325734
   > account:             0x0381962bD9B48A87534f6F7C2ceFCAE2b2E87EA0
   > balance:             99.96217872
   > gas used:            179941 (0x2bee5)
   > gas price:           20 gwei
   > value sent:          0 ETH
   > total cost:          0.00359882 ETH

Tests

Déploiement
Tests de la version 2
truffle(ganache)> const contractV1 = await MyContractV1.deployed();
undefined
truffle(ganache)> const contractV2 = await MyContractV2.at(contractV1.address);
undefined
truffle(ganache)> contractV1.address
'0x34275c222Cf18e28EbB1ecBc7c4b23aDd4baEbE7'
truffle(ganache)> contractV2.address
'0x34275c222Cf18e28EbB1ecBc7c4b23aDd4baEbE7'

L’adresse de la version 2 est bien celle du proxy : ‘0x34275c222Cf18e28EbB1ecBc7c4b23aDd4baEbE7’

Invocation du smart contract
Version 2 - Lecture de la valeur initiale
truffle(ganache)> (await contractV2.getValue()).toString();
'142'
Version 2 - Appel de la fonction add()
truffle(ganache)> await contractV2.add(10);
{
  tx: '0xaed8cbf957ca9909fce79a097f9fd99d751f30a30eedb86812c6d2354b190f1c',
  receipt: {
    transactionHash: '0xaed8cbf957ca9909fce79a097f9fd99d751f30a30eedb86812c6d2354b190f1c',
    transactionIndex: 0,
    blockHash: '0xfddc15ec6b90e71287d52ff3eae225024343c8b43b346e6fa1f5a23a838b8aa7',
    blockNumber: 12,
    from: '0x0381962bd9b48a87534f6f7c2cefcae2b2e87ea0',
    to: '0x34275c222cf18e28ebb1ecbc7c4b23add4baebe7',
    gasUsed: 30476,
    cumulativeGasUsed: 30476,
    contractAddress: null,
    logs: [],
    status: true,
    logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
    rawLogs: []
  },
  logs: []
}
Version 2 - Vérification de la nouvelle valeur
truffle(ganache)> (await contractV2.getValue()).toString();
'152'

Remarques :

Que se passe-t-il si on appelle la fonction add() depuis contractV1 ?

Appel de la fonction add() depuis la version 1
truffle(ganache)> await contractV1.add(10);
Uncaught TypeError: contractV1.add is not a function
    at evalmachine.<anonymous>:1:20

Une erreur évidement, la fonction add() n’existe pas sur cette version.

Que renvoie la fonction getValue() appelée depuis la version 1 ?

truffle(ganache)> (await contractV1.getValue()).toString();
'152'

Elle renvoie la même chose ce qui confirme que les 2 versions partagent le même contexte, les mêmes données.

Version 3

La version 3 vise à ajouter une nouvelle donnée (propriété) au smart contract (count) ainsi qu’une fonction pour en consulter la valeur (getCount). « count » est un entier qui est incrémenté automatiquement à chaque fois que la valeur de la propriété value change.

Développement

Version 3 du smart contract
// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;

/// @author Emmanuel Collin
/// @notice
/// @dev
contract MyContractV3  {

    uint private value;
    uint private count;

    function initialize(uint v) public {
        setValue(v);
        count = 0;
    }

    function getValue() public view returns (uint) {
        return value;
    }

    function setValue(uint v) public {
        value = v;
        count++;
    }

    function add(uint v) public {
        setValue(getValue() + v);
    }

    function getCount() public view returns (uint) {
        return count;
    }

}

Déploiement

Déploiement de la version 3
const { upgradeProxy } = require('@openzeppelin/truffle-upgrades');

const MyContractV1 = artifacts.require('MyContractV1');
const MyContractV3 = artifacts.require('MyContractV3');

module.exports = async function (deployer) {
  const existing = await MyContractV1.deployed();
  await upgradeProxy(existing.address, MyContractV3, { deployer });
};

Tests

Tests de la version 3
truffle(ganache)> const contractV1 = await MyContractV1.deployed();
undefined
truffle(ganache)> const contractV2 = await MyContractV2.at(contractV1.address);
undefined
truffle(ganache)> const contractV3 = await MyContractV3.at(contractV1.address);
undefined
truffle(ganache)> contractV1.address
contractV2.address
contractV3.address
'0xfF8b6f16Bedb4FF11098Ab06649Ddd0B0c8A9720'
truffle(ganache)> '0xfF8b6f16Bedb4FF11098Ab06649Ddd0B0c8A9720'
truffle(ganache)> '0xfF8b6f16Bedb4FF11098Ab06649Ddd0B0c8A9720'
truffle(ganache)> (await contractV3.getValue()).toString();
'142'
truffle(ganache)> (await contractV3.getCount()).toString();
'0'
truffle(ganache)> await contractV3.add(10);
{
  tx: '0xffef16f8589b23cece9348a6d248c16a663082a74b55b022128548c382da0731',
  receipt: {
    transactionHash: '0xffef16f8589b23cece9348a6d248c16a663082a74b55b022128548c382da0731',
    transactionIndex: 0,
    blockHash: '0x3fc626025cee38042d88556d35fa546b656fcf22487c1a7afcde635bba533176',
    blockNumber: 15,
    from: '0x21a554540e9a005ef517a2bbe7e3a447aa64290f',
    to: '0xff8b6f16bedb4ff11098ab06649ddd0b0c8a9720',
    gasUsed: 51436,
    cumulativeGasUsed: 51436,
    contractAddress: null,
    logs: [],
    status: true,
    logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
    rawLogs: []
  },
  logs: []
}
truffle(ganache)> (await contractV3.getValue()).toString();
'152'
truffle(ganache)> (await contractV3.getCount()).toString();
'1'
truffle(ganache)>

All good !

Références