Многим из вас, наверное, известно, что в теории, смарт-контракты в EVM-подобных системах, являются неизменяемыми (immutable), но на практике это уже давно не так. И речь даже не о таких свойствах как Pausable, то есть каких-то переменных состояния контракта, которые могут влиять на его работоспособность, а о более серьезных возможностях изменения бизнес-логики контракта. В этой статье я опишу основные приемы и остановлюсь подробнее на одном из них, на BeaconProxy.
Ключом к пониманию механизмов обновления контрактов являются следующие утверждения:
-
В вашем Solidity-контракте под капотом одна точка входа. Да-да! Вы описываете много функций, и думаете, что они вызываются, и они вызываются, но только вот точка входа в контракт одна и в ней находится роутер, считывающий из входной транзакции хэш названия функции, и делающий JUMPI на соответствующую позицию в скомпилированном коде. (ссылка на источник).
-
Если переданный хэш не соответствует ни одной известной роутеру функции, исполняется метод fallback (если он есть).
-
Метод delegatecall позволяет вызывать точку входа другого контракта, при этом используя слоты хранилища от текущего контракта. Другими словами, сами инструкции виртуальной машины EVM выглядят так: прочитай из хранилища слот 3, запиши в слот 8 хранилища число 12, итп. По умолчанию, при обычном вызове контракта, в качестве хранилища используется хранилище, ассоциированное с самим контрактом. Само по себе хранилище ничего не знает о контракте, это просто key/value интерфейс, где key — это номер слота. Вся работа с ним осуществляется из самого контракта.
Вышеизложенного, на мой взгляд, достаточно чтобы понять как построена система обновлений контрактов. Для взаимодействия с пользователем деплоится так называемый Proxy-контракт. Он хранит в одном из слотов (с высоким номером) адрес контракта код которого надо выполнить. При вызове прокси, срабарывает fallback, а оттуда вызывается delegatecall. При обновлении контракта деплоится новая логика, отдельно, в новый неизменяемый контракт, а затем адрес нового контракта сохраняется в указатель внутри Proxy-контракта (источник).
На этом теория закончилась, давайте работать руками! Подготовим окружение:
# Подготовка к работе $ curl -s https://deb.nodesource.com/setup_16.x | sudo bash $ sudo apt-get install -y nodejs $ mkdir habr-proxy && cd habr-proxy $ npm init -y $ npm install --save-dev @openzeppelin/contracts-upgradable \ @openzeppelin/hardhat-upgrades \ @nomiclabs/hardhat-ethers ethers $ npx hardhat # тут я выбираю basic project $ nano hardhat.config.js # добавить require('@openzeppelin/hardhat-upgrades');
И попробуем сделать что-то такое:

Я подготовил вот такие простые контракты:
// // contracts/Version1.sol // pragma solidity ^0.8.0; import "hardhat/console.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; contract Version1 is Initializable { uint32 public counter; function __Version1_init() internal onlyInitializing { __Version1_init_unchained(); } function __Version1_init_unchained() internal onlyInitializing { counter = 100; } function initialize() initializer public { __Version1_init(); } function setCounter(uint32 counter_) public { counter = counter_; } function getCounter() view public returns(uint32) { return counter; } /** * @dev This empty reserved space is put in place to allow future versions to add new * variables without shifting down storage in the inheritance chain. * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps */ uint256[45] private __gap; } // // contracts/Version2.sol // pragma solidity ^0.8.0; import "hardhat/console.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; contract Version2 is Initializable { uint32 public counter; function __Version2_init() internal onlyInitializing { __Version2_init_unchained(); } function __Version2_init_unchained() internal onlyInitializing { counter = 1000; } function initialize() initializer public { __Version2_init(); } function setCounter(uint32 counter_) public { counter = counter_+500; } function getCounter() view public returns(uint32) { return counter+5; } /** * @dev This empty reserved space is put in place to allow future versions to add new * variables without shifting down storage in the inheritance chain. * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps */ uint256[45] private __gap; }
Контракты совсем простые + следуют рекомендациям OpenZeppelin о подготовке Upgradable контрактов. Теперь напишем тест, чтобы проверить что наше понимание документации соответствует действительности.
// test/main-test.js const { expect } = require("chai"); const { ethers } = require("hardhat"); describe("BeaconProxy", function () { it("Should do what we need when deployed from Hardhat", async function () { const Version1 = await ethers.getContractFactory("Version1"); const Version2 = await ethers.getContractFactory("Version2"); // Развертывание контракта, который хранит указатель на актуальную // версию контракта, (Implementation Pointer на диаграмме) const beacon = await upgrades.deployBeacon(Version1); await beacon.deployed(); console.log("Beacon deployed to:", beacon.address); // Развертывание проксей (и, соответственно, стораджей) const proxy1 = await upgrades.deployBeaconProxy(beacon, Version1, []); await proxy1.deployed(); console.log("Proxy1 deployed to:", proxy1.address); const proxy2 = await upgrades.deployBeaconProxy(beacon, Version1, []); await proxy2.deployed(); console.log("Proxy2 deployed to:", proxy2.address); // Переменные для отправки запросов через с прокси. const proxy1_accessor = Version1.attach(proxy1.address) const proxy2_accessor = Version1.attach(proxy2.address) // И вот начались наши тесты. { const setValueTx = await proxy1_accessor.setCounter(105) await setValueTx.wait() } { const value = await proxy1_accessor.getCounter() expect(value.toString()).to.equal('105') } { const value = await proxy2_accessor.getCounter() expect(value.toString()).to.equal('100') } // Как мы видим, данные хранятся и правда внутри прокси-контрактов, // поэтому в одном переменная равна 105, а в другом 100. // Производим обновление указателя на версию контракта. await upgrades.upgradeBeacon(beacon, Version2); { const setValueTx = await proxy1_accessor.setCounter(105) await setValueTx.wait() } { const value = await proxy1_accessor.getCounter() expect(value.toString()).to.equal('610') // 105 + 500 + 5 } { const value = await proxy2_accessor.getCounter() expect(value.toString()).to.equal('105') } // Видно, что на обоих проксях новое поведение. }); });
На сегодня все, а уже скоро разберем как можно деплоить прокси-контракты из другого контракта!
ссылка на оригинал статьи https://habr.com/ru/post/672204/
Добавить комментарий