Паттерны обновляемых контрактов
Обновляемость контрактов — одна из самых спорных и важных тем в блокчейн-разработке. В TON контракт может обновить свой код через специальное сообщение, что даёт гибкость для исправления багов, но создаёт риск для пользователей — владелец контракта может изменить логику в любой момент. Правильный баланс между обновляемостью и безопасностью определяет доверие к вашему проекту.
Смарт-контракты в TON могут обновлять свой собственный код. Это критическая возможность: после деплоя контракт живёт на блокчейне, но может содержать баги, требовать новых функций или должен адаптироваться к изменениям протокола. При этом адрес контракта — его “идентичность” в экосистеме — должен сохраняться: NFT-коллекции ссылаются на адрес, DEX-пулы зарегистрированы по адресу, пользователи знают адрес.
В этом уроке мы разберём, как работают обновления на TON, какие паттерны существуют и какие риски несёт обновляемость.
Как работают обновления на TON
Адрес не меняется
Адрес контракта = hash(initial_code + initial_data). Ключевое слово — initial: адрес вычисляется из начального кода и данных при деплое. Когда контракт обновляет свой код через set_code(), адрес остаётся прежним.
set_code и set_data
TON предоставляет два примитива для обновления контракта:
| Операция | Фаза применения | Когда вступает в силу |
|---|---|---|
set_code() | Action phase | После текущей транзакции (отложено) |
set_data() | Compute phase | Немедленно в текущей транзакции |
Критическое различие: set_data() применяется немедленно (в compute phase), а set_code() откладывается до action phase. Если транзакция завершится ошибкой в action phase (например, не хватит газа), данные будут откачены вместе с кодом. Но если в compute phase вы изменили данные под новый формат, а action phase не выполнился — контракт может оказаться в несогласованном состоянии.
Паттерны обновлений
Базовое обновление
Простейший паттерн: администратор отправляет сообщение с новым кодом, контракт проверяет отправителя и применяет обновление.
Преимущества: простота реализации. Недостатки: требует доверия к администратору, нет времени для реакции пользователей.
Отложенное обновление (Timelocked)
Двухэтапный процесс с временной задержкой:
Преимущества: пользователи видят запрос обновления и могут вывести средства, если не согласны. Используется в production-контрактах с пользовательскими средствами.
Горячее обновление (Hot Upgrade)
Для контрактов, которые постоянно получают и обрабатывают сообщения (DEX-пулы, оракулы), простое обновление проблематично: между вызовом set_code() и его применением в action phase контракт всё ещё работает на старом коде.
Решение — горячее обновление через регистр C3:
Горячее обновление:
1. Получить новый код
2. setTvmRegisterC3(новый_код) ← активировать НЕМЕДЛЕННО
3. Вызвать функцию миграции в этой же транзакции
4. set_code(новый_код) ← зафиксировать для будущих транзакций
setTvmRegisterC3() заменяет текущий код контракта прямо в compute phase — следующий вызов функции уже выполнит новый код. Это позволяет запустить функцию миграции из нового кода в той же транзакции.
Горячее обновление — продвинутый паттерн, используемый в высоконагруженных DeFi-контрактах. Для большинства проектов достаточно базового или отложенного обновления.
Миграция хранилища
При обновлении кода контракта часто меняется формат хранилища (persistent data). Старые данные нужно преобразовать в новый формат:
Важные правила миграции:
- Миграция выполняется в той же транзакции, что и обновление кода — нельзя оставить контракт с новым кодом и старыми данными
- Если миграция не удалась (ошибка, нехватка газа) — контракт может стать неработоспособным: новый код не понимает старый формат данных
- Тестирование миграции критически важно — проверяйте на тестнете с реальными данными перед mainnet-обновлением
Безопасность обновлений
Контроль доступа
Кто может обновлять контракт — ключевой вопрос безопасности:
| Уровень | Механизм | Применимость |
|---|---|---|
| Одиночный admin | Проверка sender == admin_address | Прототипы, тестнет |
| Multisig | Требуется N из M подписей | Production |
| DAO/Governance | Голосование держателей токенов | Децентрализованные протоколы |
Рекомендации
- Timelock для production — всегда добавляйте задержку между запросом и применением обновления
- Аудит всех set_code/nativeSetCode вызовов — любой путь к обновлению кода должен быть проверен
В M10-L06 (Audit Checklist) описаны проверки безопасности для обновляемых контрактов: верификация контроля доступа, проверка путей обновления, анализ миграционных функций.
- Газ для action phase — если не хватит газа на выполнение
set_code()в action phase, обновление не применится, хотя compute phase мог изменить данные - Ревизия контракта — после обновления кода необходимо повторно верифицировать контракт в эксплорере
Подробнее о верификации обновлённых контрактов см. в M11-L03 (Contract Verification). После каждого обновления необходимо загрузить новый исходный код для верификации.
Трейты обновлений в Tact
Внимание: трейты обновления в Tact (Upgradable, DelayedUpgradable) явно отмечены как НЕ готовые для production-использования из-за потенциальных несоответствий в хранилище данных. Информация ниже приведена в образовательных целях.
Tact предоставляет экспериментальные трейты для упрощения обновлений:
Upgradable
Прямое обновление с счётчиком версий:
- Администратор отправляет новый код и данные
- Трейт проверяет права доступа (только owner)
- Применяет
nativeSetCode()иnativeSetData() - Увеличивает счётчик версии
DelayedUpgradable
Двухэтапное обновление с таймаутом:
- Этап 1: администратор запрашивает обновление (код сохраняется, запускается таймер)
- Задержка: настраиваемый период ожидания
- Этап 2: администратор подтверждает обновление после истечения таймера
Этот трейт реализует паттерн отложенного обновления (timelocked), описанный выше.
Причина экспериментального статуса: Tact автоматически управляет сериализацией данных контракта. При обновлении кода с изменённой структурой данных автоматическая сериализация может не совпадать с ожиданиями нового кода. Это может привести к потере данных или неработоспособности контракта.
Прокси-паттерны
В Ethereum прокси-контракты — основной способ обновления (ERC-1967, UUPS, Beacon). Причина: EVM не имеет встроенного механизма обновления кода, поэтому используется delegatecall — вызов кода другого контракта в контексте текущего.
На TON прокси-паттерны менее распространены, потому что set_code() нативно поддерживается:
| Аспект | Ethereum Proxy | TON set_code |
|---|---|---|
| Механизм | delegatecall к implementation | Прямая замена кода |
| Сложность | Высокая (storage layout, collision) | Средняя (миграция данных) |
| Gas overhead | Дополнительный вызов | Нет overhead |
| Нативная поддержка | Нет (паттерн поверх EVM) | Да (примитив TVM) |
Тем не менее, прокси-паттерн может использоваться на TON для специфических случаев: например, разделение хранилища и логики для контрактов с очень большим состоянием или для систем с множеством экземпляров одной логики (NFT-коллекции уже используют подобную архитектуру, где item-контракты делегируют часть логики коллекции).
Ключевые выводы
- set_code() откладывается до action phase, а set_data() применяется немедленно — это критическое различие, влияющее на порядок операций при обновлении
- Три паттерна обновления: базовое (простое), отложенное (с timelock для безопасности), горячее (через C3 для немедленной активации)
- Миграция данных обязательна при изменении формата хранилища — если миграция не удалась, контракт может стать неработоспособным
- Контроль доступа — ключевой аспект безопасности: только admin/multisig должен иметь возможность обновлять контракт
- Tact-трейты Upgradable и DelayedUpgradable экспериментальны — не используйте в production из-за потенциальных проблем с сериализацией данных
- Прокси-паттерны менее актуальны на TON, чем в Ethereum, благодаря нативной поддержке set_code
Частые ошибки
- Оставляют функцию обновления кода без ограничений: любой, кто получит доступ к ключу владельца, может заменить логику контракта на вредоносную.
- Не предусматривают миграцию данных при обновлении кода: новый код может ожидать другой формат хранилища, что приведёт к crash контракта.
- Забывают о прокси-паттерне: при обновлении кода через set_code() адрес контракта не меняется, но все ссылки на старый code_hash становятся невалидными.
- Не документируют механизм обновления для пользователей, что подрывает доверие и может привести к обвинениям в rug pull.
Проверка знанийПочему set_code() применяется не сразу, а откладывается до action phase? Какой риск это создаёт при обновлении?
Проверьте понимание
Закончили урок?
Отметьте его как пройденный, чтобы отслеживать свой прогресс
Войдите чтобы оценить урок