Тестирование смарт-контрактов: где ломается код перед релизом
Смарт-контракт ломается не на арифметике, а там, где его поведение расходится с ожиданиями вызывающей стороны. Ошибки в логике контракта почти всегда живут на границе доверия: адрес принят за «свой» и получил управление, хотя за ним может стоять чужой код или контракт, подменённый после развёртывания. Именно эти места тестирование смарт-контрактов проверяет первыми.
Второе узкое место — порядок вызовов. Если состояние меняется после внешнего вызова, участник успевает повторно войти в функцию до финального расчёта баланса. Откат транзакций возвращает состояние контракта, но не отменяет того, что внешний получатель уже выполнил, поэтому перенос всех проверок на «до» дешевле, чем последующее объяснение расхождения в учёте.
Третье — поведение при отказе внешнего источника данных. Пока источник отвечает, логика выглядит корректной; стоит ответу задержаться или устареть, и выясняется, что ветка ошибки просто не написана. Надёжный контракт в этом случае либо останавливает операцию целиком, либо деградирует предсказуемо, но не считает по устаревшему значению.
Отдельно смотрим предел по вычислительной нагрузке: цикл по массиву, который на тестовой сети из десяти записей проходит, на реальном объёме упирается в лимит и роняет операцию. Проверка перед релизом должна закрывать и это — иначе откат транзакций превратится для пользователя в обычный сценарий. Разбираем, как эти места закрываются до релиза.
Что даёт тестирование смарт-контрактов на тестнете
Тестнет — это копия основной сети с реальной экономикой газа и теми же правилами включения транзакций в блок, но без риска потерять средства. Поэтому на нём проверяют то, что локальный стенд не воспроизводит: порядок вызовов между контрактами, лимиты газа на больших массивах данных, права ролей, работу оракулов и внешних сервисов, фактический расход газа по каждой операции.
Первое, что снимает тестнет, — целый класс отказов, который в локальной среде просто не проявляется. Это повторный вход в функцию при внешнем вызове, гонка двух транзакций за одно состояние, нехватка газа на цикле по растущему списку, отсутствие прав на паузу или обновление логики. Каждый такой отказ в основной сети означает остановку протокола и ручной разбор — на тестнете он находится до выката.
Покрытие сценариев строим не вокруг «успешного» пути, а вокруг отказов: недоступный источник данных, частично отклонённая транзакция, повторная отправка, граничные суммы и пустые наборы. Один прогон в Hardhat с откатом к снимку состояния занимает минуты, поэтому правку проверяют за один цикл, а не за день пересборки локального стенда.
Отсюда два измеримых следствия. Время итерации до рабочей версии сокращается с десятков прогонов до единиц, а выкат в основную сеть становится повторением уже отработанной последовательности: те же шаги миграции, тот же порядок развёртывания, те же проверки после запуска.
Сравнение сред проверки: локальный стенд, тестнет, форк сети
Ни одна среда проверки не даёт сразу скорость, реализм и воспроизводимость, поэтому выбор зависит от того, что именно проверяем. Если нужно прогнать сотни вариантов вызова и убедиться, что инварианты держатся на любых входных данных, важнее мгновенная перемотка состояния и полный контроль над временем блока. Когда контракт обменивается данными с чужими протоколами, решающим становится доступ к их реальному состоянию — тогда рабочей средой становится форк основной сети.
На практике это не выбор одной среды, а лестница проверок: логика закрывается на локальном стенде, взаимодействие — на форке, публичный тестнет остаётся для интеграционных сценариев и демонстрации. Ниже — сравнение сред проверки по пригодности и ограничениям окружения.
| Среда проверки | Когда подходит | Ограничения окружения |
|---|---|---|
| Локальный стенд | Прогон логики, граничных значений, расхода газа, покрытия ветвей; отладка по шагам | Нет реальных контрагентов и их ликвидности; поведение под конкуренцией за блок не воспроизводится |
| Публичный тестнет | Интеграционные сценарии, проверка ролей и деплоя, показ заказчику | Состояние нельзя откатить, узлы бывают перегружены, цены и пулы не соответствуют основной сети |
| Форк основной сети | Проверка стыковки с работающими протоколами на текущем блоке, реплей реальных транзакций | Нужен доступ к архивному узлу; снимок устаревает при обновлении сети, внешние сервисы могут быть недоступны |
Итог: локальный стенд даёт скорость и повторяемость, тестнет — публичность, форк — реализм условий. Комбинируя их, мы получаем покрытие, при котором найденный дефект воспроизводится одной командой, а не «иногда на тестнете».
Как мы выстраиваем процесс проверки от аудита до передачи
Смарт-контракт после публикации не откатить: ошибка в правах доступа или в расчёте выплат остаётся в сети навсегда и стоит замороженных средств. Поэтому проверку мы ведём не одним рывком, а последовательностью шагов, где каждый следующий опирается на результаты предыдущего. Ниже — как выглядит эта цепочка от разбора логики до передачи вам готового набора проверок.
-
Аудит логики. Читаем контракт вместе с вашей командой: разбираем роли, права, точки, где меняется состояние и двигаются средства. Фиксируем инварианты — утверждения, которые обязаны быть верны при любом развитии событий. На выходе — карта рисков с приоритетами, а не общий список замечаний.
-
Схема прогонов. Из карты рисков собираем матрицу: какой сценарий запускаем, в каком окружении, что считаем успехом, а что — провалом. Покрываем не проценты строк кода, а все пути, где задействованы средства и доступы.
-
Настройка окружения. Поднимаем локальный узел и слепок главной сети на нужном состоянии: те же балансы, те же внешние источники данных, тот же момент времени. Прогон повторяет реальные условия, а не тепличные.
-
Сценарии отказа. Проверяем не только успешный путь: вызов из-под чужой роли, повторную транзакцию, недоступный внешний источник данных, исчерпание лимита вычислений. Каждый сценарий — отдельный воспроизводимый прогон с заранее описанным ожидаемым результатом.
-
Стабилизация набора. Прогоняем матрицу многократно со случайными входными данными, чтобы поймать редкие сочетания. Найденный дефект закрываем правкой и повторным прогоном всего набора целиком — исправление не должно ломать соседние пути.
-
Передача результатов. Отдаём отчёт: покрытые сценарии, найденные дефекты и их статус, команды запуска, схему развёртывания и чек-лист проверок после публикации. Набор остаётся у вас — его можно запускать перед каждым изменением контракта.
Документация, схемы и регламенты повторных прогонов
После серии прогонов на тестовой сети у заказчика должен остаться не развёрнутый контракт, а комплект, по которому окружение поднимается заново и без автора. Передаём его вместе с результатами тестирования смарт-контрактов.
- Конфигурации окружения — версии узла и компилятора, параметры сети, тестовые аккаунты и их роли: прогон не зависит от того, кто его запускает.
- Карта контрактов и зависимостей — какой контракт кого вызывает, порядок развёртывания, адреса в тестовой сети, задействованные внешние сервисы.
- Политики доступа к ключам — кто подписывает транзакции, какие ключи существуют только для тестовой сети, как хранятся секреты при смене ответственного.
- Регламент прогонов — подготовка состояния, развёртывание, сценарии, проверки после, порядок отката при неудаче.
- Отчёт по дефектам — воспроизводимые сценарии с ожидаемым и фактическим результатом и оценкой влияния на логику контракта.
- Перечень проверок перед основной сетью — что обязано пройти, а что допускает исключение с письменным обоснованием.
Дальше документы работают без нас: новый инженер поднимает окружение по инструкции, повторный прогон даёт тот же результат. Открытые дефекты остаются в отчёте с оценкой влияния — решение о переносе в основную сеть принимается на фактах, а не на памяти участников.
Кейс: как дефекты ловились до выхода в основную сеть
Проект — смарт-контракт распределения вознаграждений между участниками пула: расчёт доли, перенос накопленного баланса между периодами, экстренная остановка выплат.
Проверки до этого покрывали только успешный путь: расчёт на изолированных значениях, без последовательности транзакций и без конкуренции за один и тот же остаток. В основной сети такой набор пропускает ошибки порядка — состояние успевает измениться между проверкой условия и записью результата.
Что вскрылось на тестовой сети
Развернули полный контур на Foundry: виртуальная цепочка с управлением временем блока, подмена балансов и повторные вызовы от разных ролей.
Из 46 сценариев 23 вели себя не так, как ожидалось: расхождение начислений при двух выводах в одном блоке, обход ограничения через паузу, потеря остатка при смене роли. Пять дефектов давали неверное распределение, три блокировали вывод для части участников.
Что изменилось после выката
Доля покрытых ветвей выросла с 61% до 94%, набор прогоняется за 4 минуты вместо 40 — проверка идёт на каждом изменении, а не перед релизом. Каждый найденный дефект получил воспроизводящий сценарий и остался в наборе как постоянная проверка.
За период эксплуатации не зафиксировано ни одного расхождения начислений, откат версии не потребовался. Поведение в основной сети совпало с тестовым: тот же вход даёт тот же результат, потому что прогоняется ровно тот код, который уходит в сеть.
Чем публичный тестнет отличается от локального стенда?
Это не конкурирующие варианты, а две среды под разные вопросы: локальный стенд даёт повторяемость прогона и разбор логики, публичный тестнет — реалистичность условий и поведение внешних зависимостей, которые в изоляции достоверно не смоделировать.
Локальный стенд — это копия основной сети на зафиксированном блоке. Подтверждения приходят мгновенно, состояние снимается слепком и возвращается в ту же точку, а сбои — нехватку газа, откат транзакции, переупорядочивание блоков — мы воспроизводим намеренно. Один и тот же прогон даёт один и тот же результат: падение теста указывает на ошибку в коде, а не на внешнюю случайность.
Публичный тестнет отвечает на вопрос, устоит ли система в реальных условиях. Здесь настоящие оракулы, пулы ликвидности, мосты и чужие контракты, тот же порядок упаковки транзакций и та же конкуренция за газ. Плата за реалистичность — управляемость: состояние меняется между прогонами, внешний протокол может обновиться или стать недоступным, часть тестовых токенов приходится добывать вручную, и вчерашний сценарий уже не воспроизводится буквально.
Поэтому мы разводим среды по задачам, а не выбираем одну. Основная масса прогонов идёт локально: логика, права доступа, граничные суммы, обработка отказов. На тестнет выносим то, что зависит от внешнего мира, — связки с оракулами, ликвидностью, мостами — и держим отдельный набор сценариев приёмки. Тестнет при этом не заменяет аудит: он показывает, что система работает в реальных условиях, а не что она неуязвима.
Частые технические сомнения про тестовые сети и покрытие
Покрытие строк отвечает не на тот вопрос: сплошное прохождение строк в контракте без ветвлений ничего не доказывает. Важнее покрытие ветвей и инварианты — сумма балансов, права ролей, невозможность повторного вывода. Инвариант, закреплённый в тесте, ловит то, что не поймает ни один сценарий обхода кода.
Тестовая сеть не воспроизводит состояние основной: другие адреса зависимостей, недоступные внешние источники, иное поведение при перегрузке. Поэтому финальный прогон делаем на форке актуального состояния — там контракты видят те же зависимости, что и в рабочей сети.
Как понять, что покрытия достаточно?
Смотрим не на процент, а на перечень непокрытых участков и на классы отказов. Если не покрыт путь, где внешний вызов возвращает ошибку или роль отозвана, — это дыра независимо от общей цифры. Мы держим список инвариантов и проверяем их после каждого сценария: изменение одного условия не должно ломать учёт балансов.
Что делать с внешними источниками данных?
В тестовой сети оракул может молчать или отдавать произвольные значения — сценарий получается недостоверным. Мы подставляем управляемый источник и прогоняем отклонения: цена резко упала, ответ завис, пришли устаревшие данные. Так проверяются защитные механизмы, а не только счастливый путь.
Можно ли покрыть тестами уже развёрнутые контракты?
Да. Снимаем форк рабочей сети — состояние, адреса и зависимости на месте, код менять не нужно. Прогоняем сценарии против реальных зависимостей, затем проверяем обновление логики и откат на предыдущую версию: у развёрнутого контракта тоже должен быть проверенный путь назад.
Обсудим вашу задачу и вернёмся с планом проверок
Чтобы вернуться с оценкой и планом проверок, а не с общими словами, хватает нескольких вводных. Чем точнее вы опишете контракты и окружение, тем меньше допущений мы закладываем в этапы и тем быстрее переходим к работе.
Что прислать для старта:
- адреса контрактов в тестовой сети или репозиторий с исходниками и скриптами развёртывания;
- описание ролей доступа: кто владелец, кому разрешено останавливать контракт, менять параметры, выводить средства;
- внешние зависимости — оракулы, мосты, токены-компаньоны и вызовы чужих протоколов, от которых зависит логика;
- сценарии, которые уже ломались или вызывают сомнения: ручные операции перед релизом, редкие ветки логики, миграции состояния;
- текущее состояние проверок: есть ли автотесты, тесты на копии состояния основной сети, статические анализаторы, отчёты прошлых проверок;
- требования к отчётности — нужен формальный вердикт по критичности находок или список правок для вашей команды;
- доступы: тестовая сеть, ключи аккаунта развёртывания, лимиты на обращения к узлу.
Дальше первый шаг за нами. Мы разбираем вводные, уточняем спорные места и возвращаемся с планом: какие проверки делаем, в каком порядке, что именно получите на выходе — набор сценариев, конфигурацию окружения и отчёт с приоритетами. Если чего-то не хватает, скажем сразу, до начала работ.







