Я демистифицирую три способа обновления смарт-контрактов, и да, их обновление — это не миф!
В сетях блокчейнов, таких как 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. Следите за обновлениями!