Рефакторинг смарт-контрактов: где утекает газ и логика
Контракт работает, но стоимость массовой операции растёт с каждым новым адресом: цикл обходит весь массив в хранилище, и на нескольких сотнях записей транзакция перестаёт укладываться в лимит блока. Это не ошибка кода, а следствие решений, принятых на старте — структуры хранения под первую версию логики и ветви, ставшие недостижимыми после обновлений.
Вторая проблема — связанность. Права, лимиты и начисления живут в одном контракте, поэтому правка одной ветви тянет перенос состояния: адреса, балансы, счётчики. Без аккуратной миграции любая переработка кода контракта рискует заблокировать вывод средств.
Мы начинаем с аудита логики контракта и профилирования расхода газа по каждой функции: находим запись в хранилище там, где хватило бы памяти, внешние вызовы внутри цикла, повторяющиеся проверки. Затем — оптимизация расхода газа без изменения поведения: кэширование в памяти, пакетная обработка, разделение горячих и холодных данных. Параллельно закрываем уязвимости контракта — незащищённые точки входа, повторный вызов, доверие к внешним адресам.
Рефакторинг смарт-контрактов идёт с миграцией состояния и прогоном на копии сети: поведение старой версии фиксируем тестами, переносим логику и сравниваем результаты один в один. Стоимость массовой операции падает в разы и перестаёт зависеть от размера реестра, а следующая правка делается без переписывания с нуля.
Что меняется после переработки: скорость вызовов и расход газа
Переработка начинается с замеров на массовых сценариях: пакетное обновление состояний, запись в хранилище в цикле, множественные переводы в одной транзакции. Именно там расход газа растёт нелинейно, а после переписывания логики снижается предсказуемо — на повторных прогонах видно падение на десятки процентов по самым частым операциям.
Отдельный результат — устранение класса отказов, а не отдельной ошибки. Непроверенные внешние вызовы, неограниченные циклы по массивам, переполнения при приведении типов, повторный вход через обратные вызовы — это семейства дефектов, которые воспроизводятся в новых условиях. Закрываем их проверенными шаблонами (например, набором OpenZeppelin) и инвариантными проверками прямо в функциях, чтобы условие соблюдалось при любом порядке вызовов.
Объём кода — тоже измеримая величина. Уходят дублирующие проверки, ветки от прошлых версий, копипаст между контрактами: критичная часть становится короче, поверхность для ошибок — меньше, а правка проходит проверку и повторный аудит быстрее. Время на изменение сокращается с дней до часов.
Скорость исполнения вызовов растёт не только за счёт газа: сокращается число обращений к хранилищу, вычисления выносятся за пределы циклов, ранние проверки отсекают заведомо неуспешные вызовы до тяжёлых операций. Внешне это предсказуемое время подтверждения и меньше отклонённых транзакций, за которые пользователь уже заплатил комиссию.
Сопровождаемость обеспечивается тем, что переработка не ломает интеграции: адреса, интерфейсы и формат событий сохраняются, меняется внутренняя реализация. Состояние переносится скриптом с проверкой на тестовой сети, обновление идёт через схему с отдельным хранилищем — откат на предыдущую версию остаётся доступным.
Подходы к переработке: замена, частичная правка, обёртка
Выбор пути зависит не от того, насколько плохо написан текущий контракт, а от того, что именно требуется сохранить: адрес, накопленное состояние или интерфейс для внешних потребителей. Пока код неизменяем, любая переработка сводится к трём сценариям — и они не взаимозаменяемы по цене и рискам.
| Вариант | Когда подходит | Ограничения |
|---|---|---|
| Полная замена | Логика устарела целиком, состояние компактное и переносится одним проходом | Новый адрес: нужна миграция балансов, разрешений и истории; внешние интеграции обновляют ссылки вручную |
| Частичная правка логики | Контракт уже собран из обновляемых модулей, проблема локализована в одном из них | Недоступна для неизменяемого кода; после правки перепроверяется весь граф зависимостей модуля |
| Контракт-обёртка | Ядро стабильно, меняются правила доступа, лимиты или формат событий | Делегирующий вызов добавляет расход газа на каждом пути; нужен контроль слотов хранения и корректная инициализация |
Чаще выигрывает комбинация: ядро оставляют как есть, а спорную логику выносят в слой-обёртку — тогда перенос состояния не нужен вовсе. Если же меняется сама модель данных, обёртка лишь маскирует проблему: состояние придётся переносить, и лучше это делать до включения новой логики, иначе расхождение между старым и новым контуром придётся разбирать вручную.
Этапы работы: от разбора логики до передачи заказчику
Маршрут рефакторинга строим так, чтобы каждое изменение было проверяемым и обратимым. Начинаем не с правок, а с разбора кода и карты прав: кто вызывает методы, куда контракт обращается сам, где лежит состояние и как им управляют.
- Аудит кода и прав. Снимаем схему взаимодействия контрактов: внешние вызовы, проверки ролей, точки изменения параметров и вывода средств. Опасные места — неограниченные внешние вызовы, отсутствие проверок адресов — попадают в отчёт с оценкой последствий.
- Проектирование схемы. Описываем, как контракты будут связаны после переработки: что выносим в отдельное хранилище, какие роли получают доступ к административным функциям. Схему согласуем до первой правки.
- Переработка кода. Меняем логику по частям, каждое изменение — отдельная правка с описанием. Стандартные участки закрываем проверенными библиотеками (OpenZeppelin), а не самописными решениями.
- Тестирование изменений. Покрываем сценариями каждый переписанный путь: обычные операции, граничные суммы, повторные вызовы, отказ внешнего адреса. На тех же данных новая логика должна давать тот же результат, что старая.
- Проверка прав доступа. Прогоняем каждый метод от имени каждой роли — гость, пользователь, оператор, администратор. Запрещённые действия должен отклонять контракт, а не интерфейс.
- Независимая проверка. Изменённый код смотрит инженер, в переработке не участвовавший, плюс статический анализ на типовые классы уязвимостей.
- Приёмка работ. Передаём код, схему, отчёт о проверках и порядок отката — как вернуть предыдущее состояние, если после релиза что-то пойдёт не так.
Что остаётся у заказчика: код, схемы и регламенты
Рефакторинг заканчивается не правкой кода, а передачей комплекта, по которому команда заказчика развивает и сопровождает контракты без нас. Всё перечисленное остаётся в репозитории и в документации к моменту сдачи — с ним новый инженер входит в проект за дни, а не за недели раскопок.
- Исходный код с комментариями — размечен по функциям: где внешние обращения, где проверки прав, где ограничения по объёму и времени. Комментарии объясняют не «что делает строка», а «почему выбрано именно это решение».
- Схема взаимодействия — кто кого вызывает, в каком порядке меняется состояние, какие события это фиксируют и на каком шаге операция может откатиться целиком.
- Матрица прав доступа — таблица «роль → операция»: выпуск, заморозка, обновление, вывод средств. Отдельно отмечено, что происходит при передаче прав и потере ключа.
- Отчёт о проверке — что нашли, что исправили, что оставили как осознанно принятый риск и почему это допустимо.
- Регламент обновления — порядок выпуска новой версии: кто согласует, какие проверки проходят до публикации, как выполняется откат к предыдущей.
- Документация по контракту — параметры конфигурации, допустимые границы значений и точки, где требуется решение человека.
- Набор воспроизводимых проверок — сценарии, которыми критичные пути прогоняются заново после любой правки.
Комплект рассчитан на инженера, который откроет его через полгода после сдачи. Если хотя бы один пункт нельзя прочитать и повторить без автора — значит, работа ещё не закончена.
Кейс: переработка логики распределения выплат
Клиент — сервис массовых выплат партнёрам: один контракт на Solidity держал реестр получателей и в цикле распределял начисления за расчётный период. Пока адресов было несколько сотен, вызов проходил, но на 1 200 участников перестал укладываться в лимит газа. Транзакция откатывалась целиком — вместе с ней обнулялся весь раунд, потому что атомарность возвращала состояние к исходному. Операторы запускали распределение повторно, дробили списки вручную и сверяли остатки по журналу событий.
Что изменили в логике
Монолитную функцию разбили на три модуля: реестр получателей, расчёт доли и исполнение перевода. Перебор по массиву заменили индексацией по диапазонам и обработкой пакетами — один вызов проводит ограниченное число начислений, а прогресс фиксируется в состоянии, поэтому раунд продолжается с любой точки без повторов. Расчёт доли вынесли из цикла, чтобы одна и та же арифметика не выполнялась для каждого адреса.
Метрики до и после
До: потолок в 90–110 получателей на вызов, раунд — до 12 ручных прогонов, откат любого прогона отменял предыдущие. После: пакет в 250 адресов проходит стабильно, стоимость одной выплаты снизилась на 45%, раунд закрывается одним проходом, доля отказов по лимиту газа — ноль на прогоне в 1 200 участников. Журнал событий даёт полный след по каждому начислению, а модуль реестра теперь меняется отдельно от расчётной логики и не затрагивает выплатной контур.
Чем рефакторинг отличается от написания с нуля
Рефакторинг смарт-контрактов — это переработка уже развёрнутого кода с сохранением адреса и накопленного состояния. Написание с нуля — это новый контракт и перенос в него данных и всех ссылок. Выбор между стратегиями определяется не объёмом правок, а тем, сколько внешних систем и балансов завязано на текущий адрес.
Переработка выигрывает, когда логика в целом рабочая, а проблемы локальны: избыточные записи в хранилище, дублирующиеся проверки, нечитаемые ветки условий. Адрес, права администратора и связи с внешними сервисами остаются прежними — интеграции менять не нужно, состояние никуда не двигается. Плата за это — унаследованная структура: часть решений придётся сохранить, даже если архитектурно они неудобны.
Новый контракт оправдан, когда меняется модель данных или разграничение ролей и правки в старом коде превращаются в переписывание. Тогда нужен перенос: остановка операций, снятие состояния, загрузка в новый контракт, переключение адресов у всех, кто на них ссылается, и предсказуемое поведение для тех, кто не успел переключиться. Здесь выше риск при замене — ошибка в миграции затрагивает пользовательские балансы, а не только внутреннюю логику.
| Вариант | Когда подходит | Ограничения |
|---|---|---|
| Переработка текущего контракта | Логика рабочая, дефекты и издержки локальны, внешние интеграции менять нельзя | Наследует структуру хранилища и принятые решения; часть улучшений недоступна |
| Новый контракт с миграцией | Меняется модель данных, роли или порядок расчётов | Требует переноса состояния и обновления адресов у всех, кто их использует; риск при замене выше |
На сложность поддержки влияет результат, а не исходный объём работ: контракт с явными границами модулей, полным покрытием тестами на перенос состояния и описанными правами дешевле сопровождать дальше. Поэтому перед выбором мы разбираем, что именно держит текущий код — связи, данные или архитектуру, — и только потом называем стратегию и план миграции.
Технические сомнения перед стартом работ
До оценки обычно всплывают одни и те же инженерные вопросы: можно ли ещё менять уже развёрнутый контракт, как обновить логику без переноса состояния и кто отвечает за поведение после правок. Отвечаем коротко — чтобы созвон ушёл на разбор задачи, а не на базовые уточнения.
Контракт уже развёрнут — обновление ещё возможно?
Байт-код неизменяем, но обновление реально через прокси: адрес и состояние остаются прежними, меняется только логика за ними. Если прокси не заложен изначально, скажем об этом сразу и предложим план миграции на новый контракт с переносом ролей и балансов.
Как обновить логику, не перенося состояние?
Хранилище остаётся по старому адресу, новая реализация подключается к нему. Условие — совместимая раскладка хранилища: новые переменные добавляем в конец, порядок существующих не трогаем. Раскладку сверяем автоматически перед каждым обновлением.
Как проверить, что поведение не изменилось?
Гоняем дифференциальные тесты: один набор сценариев на старой и новой версии, сравниваем результаты, события и расход газа. Добавляем инвариантные проверки — например, что сумма балансов не меняется ни при какой последовательности вызовов.
Кто имеет право обновлять контракт?
Права на обновление выдаём отдельной роли: подтверждение несколькими подписями и задержка по времени, чтобы одна скомпрометированная подпись не протолкнула изменение. Роль с доступом к средствам отделяем от административной.
Где заканчивается зона ответственности подрядчика?
Мы отвечаем за аудит, исправления и повторную проверку по согласованной модели угроз, с отчётом по каждому классу найденных уязвимостей. Вне зоны — сценарии, не описанные в этой модели, и внешние сервисы, к которым обращается контракт.
Обсудим задачу: вернёмся с оценкой и планом работ
Оценка объёма работ по рефакторингу смарт-контрактов не считается «на глаз»: цена правки зависит от того, как устроено хранилище, сколько внешних вызовов и что нельзя менять. Поэтому старт — это не встреча с презентацией, а разбор кода и ограничений.
Чтобы вернуться с предметной оценкой, а не вилкой «плюс-минус», достаточно прислать:
- адреса развёрнутых контрактов и сеть, где они работают — основная или тестовая;
- исходники и настройки сборки, если контракты не верифицированы публично;
- текущие тесты, скрипты развёртывания и отчёты прошлых проверок, если они есть;
- перечень внешних протоколов и сервисов, с которыми контракт обменивается вызовами;
- ограничения: какие интерфейсы, роли и структуры хранилища обязаны остаться неизменными;
- известные узкие места — сценарии, где расход газа или время отклика мешают работе.
В ответ вы получаете оценку объёма работ, план этапов с порядком проверок после каждой правки и состав артефактов: правки с обоснованием, обновлённый набор тестов, схему хранилища и политику обновления с возможностью отката.
Опишите задачу и приложите то, что есть, — даже неполный набор входа. Вернёмся с оценкой, порядком этапов и понятным составом работ на выходе.







