11V ethereum OpenZeppelin : Обновление смарт-контрактов

— Что входит в обновление

— Обновление с помощью плагинов обновления

— Как работают обновления

— Инициализация

— Обновление

— Тестирование

— Учебники по OpenZeppelin 教程

— Контакт 联系方式

— Что входит в обновление

Смарт-контракты, развернутые с помощью OpenZeppelin Upgrades Plugins, могут быть обновлены для изменения их кода, сохраняя при этом их адрес, состояние и баланс. Это позволяет вам итеративно добавлять новые функции в ваш проект или исправлять ошибки, которые вы можете обнаружить в производстве.

Смарт-контракты в Ethereum по умолчанию неизменяемы. После их создания их невозможно изменить, и они эффективно действуют как нерушимый контракт между участниками.

Однако для некоторых сценариев желательно иметь возможность изменять их. Вспомните традиционный контракт между двумя сторонами: если они оба согласны изменить его, то они смогут это сделать. В Ethereum они могут захотеть изменить смарт-контракт, чтобы исправить найденную ошибку (которая может даже привести к тому, что хакер украдет их средства!), добавить дополнительные функции или просто изменить правила, применяемые в нем.

— Обновление с помощью плагинов Upgrades

Каждый раз, когда вы развертываете новый контракт с помощью deployProxy в OpenZeppelin Upgrades Plugins, этот экземпляр контракта может быть обновлен позже. По умолчанию, только адрес, который первоначально развернул контракт, имеет права на его обновление.

deployProxy создаст следующие транзакции:
Развертывание контракта реализации (наш контракт Box)
Развертывание контракта ProxyAdmin (администратор для нашего прокси).
Развернуть контракт прокси и запустить любую функцию инициализатора.

Давайте посмотрим, как это работает, развернув обновляемую версию нашего контракта Box, используя ту же настройку, что и при развертывании ранее:

// contracts/Box.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Box {
    uint256 private _value;

    // Emitted when the stored value changes
    event ValueChanged(uint256 value);

    // Stores a new value in the contract
    function store(uint256 value) public {
        _value = value;
        emit ValueChanged(value);
    }

    // Reads the last stored value
    function retrieve() public view returns (uint256) {
        return _value;
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Установите плагин Hardhat Upgrades.

npm install --save-dev @openzeppelin/hardhat-upgrades
Войти в полноэкранный режим Выйдите из полноэкранного режима

Затем нам нужно настроить Hardhat на использование нашего плагина @openzeppelin/hardhat-upgrades. Для этого добавьте плагин в файл hardhat.config.js следующим образом.

// hardhat.config.js
...
require('@nomiclabs/hardhat-ethers');
require('@openzeppelin/hardhat-upgrades');
...
module.exports = {
...
};
Вход в полноэкранный режим Выйти из полноэкранного режима

Для того чтобы обновить контракт типа Box, нам нужно сначала развернуть его как обновляемый контракт, что представляет собой другую процедуру развертывания, чем мы видели до сих пор. Мы инициализируем наш контракт Box вызовом store со значением 42.

В настоящее время в Hardhat нет собственной системы развертывания, вместо этого мы используем скрипты для развертывания контрактов.

Мы создадим скрипт для развертывания нашего обновляемого контракта Box с помощью deployProxy. Мы сохраним этот файл как scripts/deploy_upgradeable_box.js.

// scripts/deploy_upgradeable_box.js
const { ethers, upgrades } = require('hardhat');

async function main () {
  const Box = await ethers.getContractFactory('Box');
  console.log('Deploying Box...');
  const box = await upgrades.deployProxy(Box, [42], { initializer: 'store' });
  await box.deployed();
  console.log('Box deployed to:', box.address);
}

main();
Вход в полноэкранный режим Выход из полноэкранного режима

После этого мы можем развернуть наш обновляемый контракт.

Используя команду run, мы можем развернуть контракт Box в сети разработки.

npx hardhat run --network localhost scripts/deploy_upgradeable_box.js
Deploying Box...
Box deployed to: 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0
Войти в полноэкранный режим Выйти из полноэкранного режима

Затем мы можем взаимодействовать с нашим контрактом Box, чтобы получить значение, которое мы сохранили при инициализации.

Мы будем использовать консоль Hardhat для взаимодействия с нашим обновленным контрактом Box.

Нам нужно указать адрес нашего прокси-контракта, полученный при развертывании нашего Box-контракта.

npx hardhat console --network localhost
Welcome to Node.js v12.22.1.
Type ".help" for more information.
> const Box = await ethers.getContractFactory('Box');
undefined
> const box = await Box.attach('0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0');
undefined
> (await box.retrieve()).toString();
'42'
Вход в полноэкранный режим Выход из полноэкранного режима

После создания файла Solidity мы можем обновить экземпляр, который мы развернули ранее, с помощью функции upgradeProxy.

upgradeProxy создаст следующие транзакции:
Развернуть контракт реализации (наш контракт BoxV2).
Вызов ProxyAdmin для обновления контракта прокси для использования новой реализации.

Мы создадим сценарий для обновления нашего контракта Box для использования BoxV2 с помощью upgradeProxy. Мы сохраним этот файл как scripts/upgrade_box.js. Нам нужно указать адрес нашего прокси-контракта, полученный при развертывании нашего Box-контракта.

// scripts/upgrade_box.js
const { ethers, upgrades } = require('hardhat');

async function main () {
  const BoxV2 = await ethers.getContractFactory('BoxV2');
  console.log('Upgrading Box...');
  await upgrades.upgradeProxy('0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0', BoxV2);
  console.log('Box upgraded');
}

main();
Вход в полноэкранный режим Выход из полноэкранного режима

Затем мы можем развернуть наш обновляемый контракт.

Используя команду run, мы можем обновить контракт Box в сети разработки.

$ npx hardhat run --network localhost scripts/upgrade_box.js
Compiling 1 file with 0.8.4
Compilation finished successfully
Upgrading Box...
Box upgraded
Войти в полноэкранный режим Выйти из полноэкранного режима

Готово! Наш экземпляр Box был обновлен до последней версии кода, сохранив при этом свое состояние и тот же адрес, что и раньше. Нам не потребовалось ни развертывать новый экземпляр по новому адресу, ни вручную копировать значение из старого Box в новый.

Давайте попробуем это, вызвав новую функцию инкремента и проверив значение после этого:

Нам нужно указать адрес нашего прокси-контракта, по которому мы развернули наш Box-контракт.

npx hardhat console --network localhost
Welcome to Node.js v12.22.1.
Type ".help" for more information.
> const BoxV2 = await ethers.getContractFactory('BoxV2');
undefined
> const box = await BoxV2.attach('0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0');
undefined
> await box.increment();
...
> (await box.retrieve()).toString();
'43'
Вход в полноэкранный режим Выйти из полноэкранного режима

Вот и все! Обратите внимание, что значение Box сохранилось на протяжении всего обновления, как и его адрес. И этот процесс одинаков независимо от того, работаете ли вы на локальном блокчейне, в тестовой сети или в основной сети.

Давайте посмотрим, как плагины OpenZeppelin Upgrades выполняют эту задачу.

— Как работают обновления

Этот раздел будет более теоретически насыщенным, чем другие: не стесняйтесь пропустить его и вернуться к нему позже, если вам интересно.

Когда вы создаете новый экземпляр контракта с возможностью обновления, OpenZeppelin Upgrades Plugins фактически развертывает три контракта:
Написанный вами контракт, который известен как контракт реализации, содержащий логику.
ProxyAdmin, который будет администратором прокси.
Прокси для контракта реализации, который является контрактом, с которым вы фактически взаимодействуете.

Здесь прокси — это простой контракт, который просто делегирует все вызовы контракту реализации. Вызов делегата похож на обычный вызов, за исключением того, что весь код выполняется в контексте вызывающей стороны, а не получателя. Поэтому передача данных в коде контракта реализации будет фактически передавать баланс прокси, а любые чтения или записи в хранилище контракта будут считываться или записываться из собственного хранилища прокси.

Это позволяет нам разделить состояние и код контракта: прокси хранит состояние, а контракт реализации предоставляет код. Это также позволяет нам изменять код, просто делегируя прокси другому контракту реализации.

Вы можете иметь несколько прокси, использующих один и тот же контракт реализации, поэтому вы можете сэкономить газ, используя этот паттерн, если планируете развернуть несколько копий одного и того же контракта.
Любой пользователь смарт-контракта всегда взаимодействует с прокси, который никогда не меняет свой адрес. Это позволяет вам выпустить обновление или исправить ошибку, не требуя от пользователей ничего менять на своей стороне — они просто продолжают взаимодействовать с тем же адресом, что и всегда.

Если вы хотите узнать больше о том, как работают прокси OpenZeppelin, ознакомьтесь с разделом Прокси.

— Инициализация

Обновляемые контракты не могут иметь конструктора. Чтобы помочь вам запустить код инициализации, OpenZeppelin Contracts предоставляет базовый контракт Initializable, который позволяет пометить метод как инициализатор, гарантируя, что он может быть запущен только один раз.

В качестве примера, давайте напишем новую версию контракта Box с инициализатором, хранящим адрес администратора, который будет единственным, кому разрешено изменять его содержимое.

// contracts/AdminBox.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

contract AdminBox is Initializable {
    uint256 private _value;
    address private _admin;

    // Emitted when the stored value changes
    event ValueChanged(uint256 value);

    function initialize(address admin) public initializer {
        _admin = admin;
    }

    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() initializer {}

    // Stores a new value in the contract
    function store(uint256 value) public {
        require(msg.sender == _admin, "AdminBox: not admin");
        _value = value;
        emit ValueChanged(value);
    }

    // Reads the last stored value
    function retrieve() public view returns (uint256) {
        return _value;
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

При развертывании этого контракта нам нужно будет указать имя функции инициализатора (только если имя не является по умолчанию initialize) и указать адрес администратора, который мы хотим использовать.

// scripts/deploy_upgradeable_adminbox.js
const { ethers, upgrades } = require('hardhat');

async function main () {
  const AdminBox = await ethers.getContractFactory('AdminBox');
  console.log('Deploying AdminBox...');
  const adminBox = await upgrades.deployProxy(AdminBox, ['0xACa94ef8bD5ffEE41947b4585a84BdA5a3d3DA6E'], { initializer: 'initialize' });
  await adminBox.deployed();
  console.log('AdminBox deployed to:', adminBox.address);
}

main();
Вход в полноэкранный режим Выйти из полноэкранного режима

Для всех практических целей инициализатор действует как конструктор. Однако имейте в виду, что поскольку это обычная функция, вам придется вручную вызвать инициализаторы всех базовых контрактов (если таковые имеются).

Вы могли заметить, что мы включили конструктор, а также инициализатор. Этот конструктор служит для того, чтобы оставить контракт реализации в инициализированном состоянии, что является средством защиты от некоторых потенциальных атак.

Чтобы узнать больше об этом и других предостережениях при написании обновляемых контрактов, ознакомьтесь с нашим руководством по написанию обновляемых контрактов.

— Модернизация

В силу технических ограничений при обновлении контракта до новой версии вы не можете изменить схему хранения этого контракта.

Это означает, что если вы уже объявили в контракте переменную состояния, вы не можете удалить ее, изменить ее тип или объявить перед ней другую переменную. В нашем примере с Box это означает, что мы можем добавлять новые переменные состояния только после значения.

// contracts/Box.sol
contract Box {
    uint256 private _value;

    // We can safely add a new variable after the ones we had declared
    address private _owner;

    // ...
}
Вход в полноэкранный режим Выход из полноэкранного режима

К счастью, это ограничение касается только переменных состояния. Вы можете изменять функции и события контракта по своему усмотрению.

Если вы случайно испортите расположение хранилища вашего контракта, плагины обновления предупредят вас при попытке обновления.

— Тестирование

Для тестирования обновляемых контрактов мы должны создать модульные тесты для реализации контракта, а также тесты более высокого уровня для тестирования взаимодействия через прокси. Мы можем использовать deployProxy в наших тестах так же, как и при развертывании.

Когда мы хотим обновить контракт, мы должны создать модульные тесты для нового контракта реализации, а также создать тесты более высокого уровня для тестирования взаимодействия через прокси после обновления с помощью upgradeProxy, проверяя, что состояние сохраняется при всех обновлениях.

— OpenZeppelin Tutorials 教程

CN 中文 Github OpenZeppelin 教程 : github.com/565ee/OpenZeppelin_CN

CN 中文 CSDN OpenZeppelin 教程 : blog.csdn.net/wx468116118

EN 英文 Github OpenZeppelin Tutorials : github.com/565ee/OpenZeppelin_EN

— Контакт 联系方式

Домашняя страница : 565.ee

GitHub : github.com/565ee

Электронная почта : 565.eee@gmail.com

Facebook : facebook.com/565.ee

Twitter : twitter.com/565_eee

Telegram : t.me/ee_565

Оцените статью
devanswers.ru
Добавить комментарий