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

Смарт-контракты изменили наше представление о децентрализованных приложениях, введя самоисполняющиеся программы, устраняющие необходимость в посредниках. Они прозрачно выполняются в децентрализованных блокчейн-сетях, предлагая такие преимущества, как отсутствие необходимости доверия и повышенная безопасность. Однако их неизменяемость, которая когда-то считалась достоинством, создаёт серьёзную проблему: после развёртывания смарт-контракт нельзя изменить. Эта жёсткость затрудняет внедрение необходимых обновлений или исправление ошибок.
В этой статье мы рассмотрим концепцию обновляемых смарт-контрактов, объясним, почему они необходимы, и разберём принцип их работы, уделив особое внимание их реализации на Linea zkEVM.
Обновляемые смарт-контракты решают ограничения традиционных смарт-контрактов, позволяя вносить изменения и улучшения после развёртывания. В то время как традиционные смарт-контракты известны своей неизменяемостью, обновляемые контракты сочетают безопасность неизменяемости с гибкостью адаптации. Это позволяет разработчикам внедрять обновления, не нарушая работу пользователей и не требуя от них миграции на новый контракт.
В традиционных контрактах ошибки или уязвимости в системе безопасности становятся постоянными после развёртывания. Обновляемые контракты решают эту проблему, позволяя вносить изменения после развёртывания и устраняя ключевые проблемы, такие как:
1. Исправление ошибок и уязвимостей безопасности: разработчики могут устранять уязвимости после развёртывания, защищая активы пользователей.
2. Адаптивность: обновляемые контракты обеспечивают бесшовную интеграцию с новыми протоколами, поддерживая актуальность приложений в соответствии с технологическим прогрессом.
3. Улучшение безопасности: с учётом развивающихся киберугроз эти контракты упрощают внедрение новых мер безопасности без повторного развёртывания всего контракта.
4. Гибкость управления: децентрализованные автономные организации (DAO) могут использовать голосование сообщества для управления процессом обновления, обеспечивая прозрачность и участие заинтересованных сторон.
5. Упрощённые обновления: в отличие от традиционных контрактов, требующих сложного повторного развёртывания и миграции данных, обновляемые контракты упрощают процесс обновления, делая его более эффективным и удобным для пользователей.
Для создания обновляемого смарт-контракта наиболее распространённым методом является паттерн прокси. Этот подход разделяет логику контракта и его данные, позволяя разработчикам обновлять логику, сохраняя при этом то же хранилище данных.
Вот как это работает:
Прокси-контракт: этот контракт хранит состояние (переменные состояния) и выступает в роли интерфейса для пользователей.
Контракт реализации: этот контракт содержит исполняемый код (бизнес-логику).
Делегирование: прокси-контракт делегирует вызовы функций контракту реализации с помощью delegatecall Ethereum. Это позволяет прокси выполнять код реализации, сохраняя при этом то же хранилище.
Когда требуется обновление, прокси-контракт перенаправляется на новый контракт реализации, что позволяет обновлять логику без изменения хранилища контракта или нарушения работы пользователей. Вот пример с использованием прокси-контракта DAO для иллюстрации этого процесса:
Важно отметить два момента: слот хранилища для адреса контракта реализации и слот хранилища для адреса администратора. Это намеренно выбранные уникальные идентификаторы, используемые для вычисления слота хранилища адреса реализации внутри прокси-контракта.
При использовании паттерна прокси с delegatecall (мы рассмотрим эту функцию подробнее чуть позже) контракт реализации работает в контексте хранилища прокси. Это означает:
Общее пространство хранилища: оба контракта используют одну и ту же структуру хранилища.
Риск перезаписи: без тщательного управления переменные в прокси-контракте могут быть перезаписаны контрактом реализации.
Использование хешированного уникального идентификатора обеспечивает:
Изоляцию: гарантируется, что слоты хранилища, используемые прокси, вряд ли будут конфликтовать с какими-либо переменными в контракте реализации.
Согласованность: один и тот же слот хранилища последовательно используется во всём контракте везде, где требуется адрес реализации.
Ещё один важный аспект — конструктор прокси-контракта. Он включает параметр _implementation, что означает: контракт реализации должен быть развёрнут первым, а его адрес передаётся в качестве аргумента при развёртывании прокси-контракта.
Теперь рассмотрим функции fallback и receive — это функции Solidity, которые играют ключевую роль в перенаправлении вызовов и переводов Ether в контракт реализации. Они обрабатывают ситуации, когда контракт получает вызовы или средства, не соответствующие ни одной из явно определённых функций. Вот подробное объяснение того, что делают эти функции:
Механизм fallback: функция fallback в Solidity — это специальная функция, которая выполняется при двух основных условиях:- когда вызывается функция, не существующая в контракте;
когда данные отправляются в контракт без соответствия какой-либо сигнатуре функции.
Делегирование: в контексте контракта DAOProxy функция fallback делегирует все такие вызовы текущему контракту реализации.
Как это работает в DAOProxy:
Обработка вызовов функций:- когда пользователь вызывает функцию, не определённую в прокси-контракте, срабатывает функция fallback.
Это распространённая ситуация, поскольку прокси-контракт в основном содержит административные функции, такие как upgradeTo и changeAdmin, а не функции бизнес-логики контракта реализации.
Делегирование в реализацию:- функция fallback вызывает _delegate(implementation()), перенаправляя вызов в контракт реализации.
_delegate — это внутренняя функция, которая использует delegatecall для выполнения кода реализации в контексте хранилища прокси.
Контекст выполнения:- все изменения состояния происходят в хранилище прокси.
msg.sender и msg.value остаются неизменными, сохраняя контекст исходного вызывающего.
Получение Ether: функция receive, введённая в Solidity 0.6.0, выполняется, когда контракт получает Ether без каких-либо сопутствующих данных (то есть когда msg.data пуст).
Делегирование переводов Ether: в контракте DAOProxy обычные переводы Ether делегируются контракту реализации.
Как это работает в DAOProxy:
Обработка переводов Ether:- когда кто-то отправляет Ether на адрес прокси-контракта без данных, срабатывает функция receive.
Это происходит через транзакцию, которая указывает только адрес получателя (to) и сумму (value в Ether), без вызова какой-либо конкретной функции.
Делегирование в реализацию:- как и функция fallback, receive вызывает _delegate(implementation()), перенаправляя перевод Ether в контракт реализации.
Затем контракт реализации определяет, как обрабатывать полученный Ether (например, обновление балансов, генерация событий).
Теперь рассмотрим функцию _delegate, которая позволяет прокси перенаправлять вызовы функций в контракт реализации, сохраняя контекст исходного вызова. Это позволяет обновлять логику контракта со временем без изменения его адреса или нарушения его состояния.
Остальные функции — это геттеры и сеттеры, которые помогают управлять переменными состояния нашего прокси-контракта. Для этого мы используем ключевое слово assembly, которое позволяет разработчикам включать низкоуровневый ассемблерный код непосредственно в свои контракты. Эта возможность обеспечивает прямой доступ к инструкциям Ethereum Virtual Machine (EVM), позволяя более точно контролировать поведение контракта, оптимизировать потребление газа и выполнять операции, которые либо недоступны напрямую, либо менее эффективны в высокоуровневом коде Solidity.
Теперь главный вопрос: как нам взаимодействовать с нашим смарт-контрактом реализации с помощью этого прокси-контракта?
Для этого нам нужен ABI смарт-контракта реализации. Поскольку прокси делегирует вызовы контракту реализации, вам нужен его ABI, чтобы знать, какие функции можно вызывать. Кроме того, при взаимодействии с блокчейном мы будем использовать адрес прокси-контракта, а не контракта реализации. Прокси хранит состояние и делегирует вызовы реализации, поэтому все взаимодействия должны проходить через прокси.
Представим, что у нас есть JavaScript-проект, и мы используем Ethers.js для взаимодействия с блокчейном. Вот как это будет выглядеть:
Таким образом, мы вызываем воображаемую функцию setValue в нашем контракте реализации через наш прокси-контракт.
Раз уж мы говорим об обновляемых контрактах, возникает ещё один важный вопрос: как обновить свои контракты?
Во-первых, только администратор (лицо, развернувшее прокси-контракт) может обновить контракт реализации. Для этого администратор должен сначала развернуть новую версию контракта реализации в блокчейне. После этого необходимо вызвать функцию upgradeTo в прокси-контракте, передав адрес нового контракта в качестве параметра. Это будет выглядеть следующим образом:
await proxyContract.connect(adminSigner).upgradeTo(newImplementationAddress);Обновляемые смарт-контракты играют важную роль в разработке блокчейна, поскольку позволяют вносить изменения и улучшения после развёртывания. Используя паттерн прокси, разработчики могут бесшовно внедрять новые функции, исправлять ошибки и реагировать на изменяющиеся требования, не нарушая пользовательский опыт и не мигрируя активы.
Однако с большой силой приходит большая ответственность. Крайне важно подходить к обновляемым контрактам с глубоким пониманием их механики, последствий для безопасности и лучших практик. Обеспечение согласованности структуры хранилища, внедрение надёжного контроля доступа и тщательное тестирование обновлений — это необходимые шаги для защиты от потенциальных рисков.
Приступая к созданию обновляемых контрактов, рассмотрите возможность использования проверенных инструментов, таких как OpenZeppelin's Upgrades Plugin, для упрощения процесса и повышения безопасности. Удачного кодинга!
Переведено ИИ. Может содержать ошибки. Пожалуйста, всегда проверяйте информацию.