Я демистифицирую три способа обновления смарт-контрактов, и да, их обновление — это не миф!

В сетях блокчейнов, таких как Ethereum, BSC и Polygon, или так называемых блокчейнах на основе EVM, смарт-контракты представляют собой развертываемые и исполняемые фрагменты кода, которые являются неизменными, проверяемыми и автономными. Развернутый смарт-контракт или подтвержденная транзакция не могут быть изменены из-за неизменности блокчейна.

Неизменяемый

Смарт-контракты, развернутые в сетях на основе EVM, таких как Ethereum, в «Теории» неизменны. Загружается байт-код, выполняется функция конструктора, а полученный код затем сохраняется в блокчейне, который не может быть обновлен. достаточно страшно?

Часто инженеры и владельцы бизнеса в сфере блокчейна называют это проклятием смарт-контрактов.

Почему?
Потому что что, если им нужно изменить или обновить бизнес-модель? что тогда будет? так это проклятие! но так ли это на самом деле?

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

Мутация

На практике мы должны знать, что смарт-контракт сам по себе неизменен, но «НЕ» исполнение контракта! это открывает перед нами новые возможности для изменения смарт-контракта.

Смарт-контракты можно видоизменять по-разному:

  • Использование SELFDESTRUCT opcode для удаления контракта и возможной повторной загрузки другого байт-кода по тому же адресу.
  • Использование DELEGATECALLдля вызова кода другого контракта.
  • Использование CREATE2для сохранения кода в состоянии и последующего его выполнения.

Обновление смарт-контрактов

Сейчас обычно есть 3 метода, которые можно использовать для обновления контрактов.

Способ 1 — Прокси:

Когда пользователь взаимодействует со смарт-контрактом, он проходит через прокси-смарт-контракт, который перенаправляет вызов функции в фактическую реализацию, представленную на графике ниже как v1, v2 и v3:

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

Для достижения желаемого результата необходимо использовать специальный операционный код EVM под названием delegatecall. Это позволит нам выполнить смарт-контракт в контексте другого. например, когда мы выполняем вызов V1, если мы изменим любую переменную состояния, это фактически изменит хранилище смарт-контракта прокси, это не изменит хранилище V1, V1 предназначен только для кода.

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

// A piece of Proxy smart contract code that will return the written data size
fallback() external payable {
  assembly {
    let _target := sload(_IMPLEMENTATION_SLOT)
    calldatacopy(0x0, 0x0, returndatasize())
    switch result case 0 {revert(0, 0) default {return (0, returndatasize())}
  }
}

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

Способ 2 — Шаблон адаптера:

В этом методе есть основной контракт, который состоит из большей части бизнес-логики, в то время как некоторые функции основного контракта делегируют свою ответственность другим смарт-контрактам.
Примером этого является оптимизатор доходности от yearn Finance. , это в основном функция для внесения токенов, и после того, как токены инвестируются в соответствии с выбранной инвестиционной стратегией, они реализуются в других смарт-контрактах V1, V2, V3, и поэтому в любое время финансы могут добавить новую инвестиционную стратегию, развернув новый договор на реализацию и обновление адреса в основном договоре. Так что это чем-то похоже на метод Proxy за исключением простоты, потому что в этом случае, когда пользователь вызывает функцию в основном контракте, эта функция фактически существует в контексте основного контракта и не затрагивает резервную функцию основного контракта. договор. Что касается бизнес-логики основного контракта, вы не сможете ее изменить! однако для контрактов реализации V1, V2 и V3 вы можете изменить все, что хотите, если это соответствует определенному интерфейсу.

Интерфейс:

pragma solidity ^0.8.7;

interface IImplementation {
  function getData() external pure returns(uint);
}

Основной контракт:

pragma solidity ^0.8.7;

import './IImplementation.sol';

contract MainContract {
  address public admin;
  IImplementation public implementation;

  constructor() {
    admin = msg.sender;
  }

  function upgrade(address _implementation) external {
    require(msg.sender == admin, 'admin rights required');
    implementation = IImplementation(_implementation);
  }

  function getData() external view returns(uint) {
    return implementation.getData();
  }
}

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

Способ 3 — миграция (самый простой):

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

Мигратор:

pragma solidity ^0.8.7;

import '@openzeppelin/contracts/token/ERC20/IERC20.sol';

contract Migrator {
  mapping(address => bool) public migrations;
  IERC20 public v1;
  IERC20 public v2;

  constructor(address _v1, address _v2) {
    v1 = IERC20(_v1);
    v2 = IERC20(_v2);
  }

  function migrate() external {
    require(migrations[msg.sender] == false, 'migration has been already done');
    migrations[msg.sender] = true; 
    v2.transfer(msg.sender, v1.balanceOf(msg.sender));
  }
}

часть сопоставления предназначена для предотвращения дублирования миграции.

V1:

pragma solidity ^0.8.7;

import '@openzeppelin/contracts/token/ERC20/ERC20.sol';

contract V1 is ERC20 {
  constructor() ERC20('Token V1', 'TV1') {
    _mint(msg.sender, 1000000);
  }
}

V2:

pragma solidity ^0.8.7;

import '@openzeppelin/contracts/token/ERC20/ERC20.sol';

contract V2 is ERC20 {
  constructor() ERC20('Token V2', 'TV2') {
    _mint(msg.sender, 1000000);
  }

  function burn(uint amount) external {
    _burn(msg.sender, amount);
  }
}

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

Я надеюсь, что вы получили некоторое представление об обновляемых смарт-контрактах. Следующим на очереди будет то, как сделать это эффективно, и я буду держать вас в курсе некоторых реализаций в репозитории GitHub. Следите за обновлениями!