Перейти к основному контенту
Change page

Тестирование смарт-контрактов

Публичные блокчейны, такие как Эфириум, неизменяемы, что затрудняет изменение кода смарт-контракта после развертывания. Существуют шаблоны обновления контрактов для выполнения «виртуальных обновлений», но их сложно реализовать, и они требуют социального консенсуса. Более того, обновление может исправить ошибку только после ее обнаружения — если злоумышленник обнаружит уязвимость первым, ваш смарт-контракт окажется под угрозой эксплойта.

По этим причинам тестирование смарт-контрактов перед развертыванием в Мейннет является минимальным требованием для обеспечения безопасности. Существует множество методов тестирования контрактов и оценки правильности кода; выбор зависит от ваших потребностей. Тем не менее, набор тестов, состоящий из различных инструментов и подходов, идеально подходит для выявления как мелких, так и серьезных недостатков безопасности в коде контракта.

Предварительные требования

На этой странице объясняется, как тестировать смарт-контракты перед развертыванием в сети Эфириум. Предполагается, что вы знакомы со смарт-контрактами.

Что такое тестирование смарт-контрактов?

Тестирование смарт-контрактов — это процесс проверки того, что код смарт-контракта работает так, как ожидается. Тестирование полезно для проверки того, удовлетворяет ли конкретный смарт-контракт требованиям надежности, удобства использования и безопасности.

Хотя подходы различаются, большинство методов тестирования требуют выполнения смарт-контракта с небольшой выборкой данных, которые он должен обрабатывать. Если контракт выдает правильные результаты для выборочных данных, предполагается, что он функционирует должным образом. Большинство инструментов тестирования предоставляют ресурсы для написания и выполнения тестовых сценариев (opens in a new tab), чтобы проверить, соответствует ли выполнение контракта ожидаемым результатам.

Почему важно тестировать смарт-контракты?

Поскольку смарт-контракты часто управляют ценными финансовыми активами, незначительные ошибки программирования могут привести и часто приводят к огромным потерям для пользователей (opens in a new tab). Однако тщательное тестирование может помочь вам на ранней стадии обнаружить дефекты и проблемы в коде смарт-контракта и исправить их до запуска в Мейннет.

Хотя при обнаружении ошибки можно обновить контракт, обновления сложны и могут привести к ошибкам (opens in a new tab) при неправильном обращении. Обновление контракта также сводит на нет принцип неизменяемости и обременяет пользователей дополнительными допущениями о доверии. И наоборот, комплексный план тестирования вашего контракта снижает риски безопасности смарт-контрактов и уменьшает необходимость выполнения сложных обновлений логики после развертывания.

Методы тестирования смарт-контрактов

Методы тестирования смарт-контрактов Эфириума делятся на две широкие категории: автоматизированное тестирование и ручное тестирование. Автоматизированное и ручное тестирование предлагают уникальные преимущества и компромиссы, но вы можете комбинировать их для создания надежного плана анализа ваших контрактов.

Автоматизированное тестирование

Автоматизированное тестирование использует инструменты, которые автоматически проверяют код смарт-контракта на наличие ошибок при выполнении. Преимущество автоматизированного тестирования заключается в использовании скриптов (opens in a new tab) для управления оценкой функциональности контракта. Скриптовые тесты можно запланировать для многократного запуска с минимальным вмешательством человека, что делает автоматизированное тестирование более эффективным, чем ручные подходы к тестированию.

Автоматизированное тестирование особенно полезно, когда тесты повторяются и отнимают много времени; их трудно выполнить вручную; они подвержены человеческим ошибкам; или включают оценку критически важных функций контракта. Но инструменты автоматизированного тестирования могут иметь недостатки — они могут пропускать определенные ошибки и выдавать множество ложных срабатываний (opens in a new tab). Следовательно, идеальным вариантом является сочетание автоматизированного тестирования с ручным тестированием смарт-контрактов.

Ручное тестирование

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

Ручное тестирование может проводиться одним человеком в соответствии с письменным планом тестирования, который охватывает различные сценарии тестирования. Вы также можете привлечь несколько человек или групп для взаимодействия со смарт-контрактом в течение определенного периода в рамках ручного тестирования. Тестировщики будут сравнивать фактическое поведение контракта с ожидаемым поведением, отмечая любые различия как ошибки.

Эффективное ручное тестирование требует значительных ресурсов (навыков, времени, денег и усилий), и из-за человеческого фактора возможно пропустить определенные ошибки при выполнении тестов. Но ручное тестирование также может быть полезным — например, тестировщик-человек (например, аудитор) может использовать интуицию для обнаружения крайних случаев, которые пропустил бы инструмент автоматизированного тестирования.

Автоматизированное тестирование смарт-контрактов

Модульное тестирование

Модульное тестирование оценивает функции контракта по отдельности и проверяет правильность работы каждого компонента. Хорошие модульные тесты должны быть простыми, быстро выполняться и давать четкое представление о том, что пошло не так в случае сбоя тестов.

Модульные тесты полезны для проверки того, что функции возвращают ожидаемые значения и что хранилище контракта обновляется должным образом после выполнения функции. Более того, запуск модульных тестов после внесения изменений в кодовую базу контракта гарантирует, что добавление новой логики не приведет к ошибкам. Ниже приведены некоторые рекомендации по проведению эффективных модульных тестов:

Рекомендации по модульному тестированию смарт-контрактов

1. Поймите бизнес-логику и рабочий процесс вашего контракта

Перед написанием модульных тестов полезно знать, какие функции предлагает смарт-контракт и как пользователи будут получать доступ к этим функциям и использовать их. Это особенно полезно для запуска тестов «счастливого пути» (happy path) (opens in a new tab), которые определяют, возвращают ли функции в контракте правильный результат для допустимых пользовательских вводов. Мы объясним эту концепцию на (сокращенном) примере контракта аукциона (opens in a new tab)

Это простой контракт аукциона, предназначенный для получения ставок в период торгов. Если highestBid увеличивается, предыдущий участник, предложивший наибольшую цену, получает свои деньги; как только период торгов заканчивается, beneficiary вызывает контракт, чтобы получить свои деньги.

Модульные тесты для такого контракта будут охватывать различные функции, которые пользователь может вызвать при взаимодействии с контрактом. Примером может служить модульный тест, который проверяет, может ли пользователь сделать ставку во время проведения аукциона (т. е. вызовы bid() проходят успешно), или тест, который проверяет, может ли пользователь сделать ставку выше текущей highestBid.

Понимание рабочего процесса контракта также помогает в написании модульных тестов, которые проверяют, соответствует ли выполнение требованиям. Например, контракт аукциона указывает, что пользователи не могут делать ставки, когда аукцион завершен (т. е. когда auctionEndTime меньше block.timestamp). Таким образом, разработчик может запустить модульный тест, который проверяет, завершаются ли вызовы функции bid() успешно или с ошибкой, когда аукцион окончен (т. е. когда auctionEndTime > block.timestamp).

2. Оцените все допущения, связанные с выполнением контракта

Важно документировать любые допущения о выполнении контракта и писать модульные тесты для проверки обоснованности этих допущений. Помимо обеспечения защиты от неожиданного выполнения, тестирование утверждений заставляет вас задуматься об операциях, которые могут нарушить модель безопасности смарт-контракта. Полезный совет — выйти за рамки «тестов счастливого пользователя» и написать негативные тесты, которые проверяют, завершается ли функция с ошибкой при неправильных входных данных.

Многие фреймворки для модульного тестирования позволяют создавать утверждения — простые заявления, в которых указывается, что контракт может и чего не может делать, — и запускать тесты, чтобы проверить, выполняются ли эти утверждения при выполнении. Разработчик, работающий над контрактом аукциона, описанным ранее, мог бы сделать следующие утверждения о его поведении перед запуском негативных тестов:

  • Пользователи не могут делать ставки, когда аукцион окончен или еще не начался.

  • Контракт аукциона отменяет транзакцию, если ставка ниже допустимого порога.

  • Пользователям, которые не смогли выиграть торги, возвращаются их средства.

Примечание: Другой способ проверки допущений — написать тесты, которые запускают модификаторы функций (opens in a new tab) в контракте, особенно операторы require, assert и if…else.

3. Измеряйте покрытие кода

Покрытие кода (opens in a new tab) — это метрика тестирования, которая отслеживает количество ветвей, строк и операторов в вашем коде, выполненных во время тестов. Тесты должны иметь хорошее покрытие кода, чтобы минимизировать риск непроверенных уязвимостей. Без достаточного покрытия вы можете ошибочно предположить, что ваш контракт безопасен, потому что все тесты пройдены, в то время как уязвимости все еще существуют в непроверенных путях кода. Однако регистрация высокого покрытия кода дает уверенность в том, что все операторы/функции в смарт-контракте были в достаточной степени проверены на правильность.

4. Используйте хорошо проработанные фреймворки для тестирования

Качество инструментов, используемых при запуске модульных тестов для ваших смарт-контрактов, имеет решающее значение. Идеальный фреймворк для тестирования — это тот, который регулярно поддерживается; предоставляет полезные функции (например, возможности ведения журнала и отчетности); и должен широко использоваться и проверяться другими разработчиками.

Фреймворки для модульного тестирования смарт-контрактов на Solidity доступны на разных языках (в основном JavaScript, Python и Rust). Ознакомьтесь с некоторыми руководствами ниже для получения информации о том, как начать запускать модульные тесты с помощью различных фреймворков для тестирования:

Интеграционное тестирование

В то время как модульное тестирование отлаживает функции контракта изолированно, интеграционные тесты оценивают компоненты смарт-контракта в целом. Интеграционное тестирование может обнаружить проблемы, возникающие из-за межконтрактных вызовов или взаимодействий между различными функциями в одном и том же смарт-контракте. Например, интеграционные тесты могут помочь проверить, правильно ли работают такие вещи, как наследование (opens in a new tab) и внедрение зависимостей.

Интеграционное тестирование полезно, если ваш контракт использует модульную архитектуру или взаимодействует с другими ончейн-контрактами во время выполнения. Один из способов запуска интеграционных тестов — сделать на определенной высоте (используя такой инструмент, как Forge (opens in a new tab) или Hardhat (opens in a new tab)) и смоделировать взаимодействие между вашим контрактом и развернутыми контрактами.

Форк блокчейна будет вести себя аналогично Мейннет и иметь учетные записи со связанными состояниями и балансами. Но он действует только как изолированная локальная среда разработки, что означает, что вам не понадобятся реальные ETH для транзакций, например, и ваши изменения не повлияют на реальный Протокол Эфириума.

Тестирование на основе свойств

Тестирование на основе свойств — это процесс проверки того, что смарт-контракт удовлетвоет некоторому определенному свойству. Свойства утверждают факты о поведении контракта, которые, как ожидается, останутся верными в различных сценариях — примером свойства смарт-контракта может быть «Арифметические операции в контракте никогда не вызывают переполнение или антипереполнение».

Статический анализ и динамический анализ — это два распространенных метода выполнения тестирования на основе свойств, и оба могут проверить, что код программы (в данном случае смарт-контракта) удовлетворяет некоторому предопределенному свойству. Некоторые инструменты тестирования на основе свойств поставляются с предопределенными правилами об ожидаемых свойствах контракта и проверяют код на соответствие этим правилам, в то время как другие позволяют создавать пользовательские свойства для смарт-контракта.

Статический анализ

Статический анализатор принимает на вход исходный код смарт-контракта и выводит результаты, объявляющие, удовлетворяет ли контракт свойству или нет. В отличие от динамического анализа, статический анализ не предполагает выполнения контракта для анализа его правильности. Вместо этого статический анализ рассуждает обо всех возможных путях, по которым смарт-контракт может пойти во время выполнения (т. е. путем изучения структуры исходного кода, чтобы определить, что это будет означать для работы контракта во время выполнения).

Линтинг (opens in a new tab) и статическое тестирование (opens in a new tab) являются распространенными методами проведения статического анализа контрактов. Оба требуют анализа низкоуровневых представлений выполнения контракта, таких как абстрактные синтаксические деревья (opens in a new tab) и графы потока управления (opens in a new tab), выводимые компилятором.

В большинстве случаев статический анализ полезен для обнаружения проблем безопасности, таких как использование небезопасных конструкций, синтаксические ошибки или нарушения стандартов кодирования в коде контракта. Однако известно, что статические анализаторы, как правило, ненадежны при обнаружении более глубоких уязвимостей и могут давать чрезмерное количество ложных срабатываний.

Динамический анализ

Динамический анализ генерирует символические входные данные (например, при символьном выполнении (opens in a new tab)) или конкретные входные данные (например, при фаззинге (opens in a new tab)) для функций смарт-контракта, чтобы проверить, нарушает ли какая-либо трасса выполнения определенные свойства. Эта форма тестирования на основе свойств отличается от модульных тестов тем, что тестовые сценарии охватывают множество сценариев, а программа управляет генерацией тестовых сценариев.

Фаззинг (opens in a new tab) — это пример метода динамического анализа для проверки произвольных свойств в смарт-контрактах. Фаззер вызывает функции в целевом контракте со случайными или искаженными вариациями определенного входного значения. Если смарт-контракт переходит в состояние ошибки (например, когда утверждение не выполняется), проблема помечается, а входные данные, которые направляют выполнение по уязвимому пути, выводятся в отчете.

Фаззинг полезен для оценки механизма проверки входных данных смарт-контракта, поскольку неправильная обработка неожиданных входных данных может привести к непреднамеренному выполнению и вызвать опасные последствия. Эта форма тестирования на основе свойств может быть идеальной по многим причинам:

  1. Написание тестовых сценариев для охвата множества сценариев — сложная задача. Тест свойств требует только определения поведения и диапазона данных для тестирования этого поведения — программа автоматически генерирует тестовые сценарии на основе определенного свойства.

  2. Ваш набор тестов может недостаточно охватывать все возможные пути в программе. Даже при 100% покрытии можно упустить крайние случаи.

  3. Модульные тесты доказывают, что контракт выполняется правильно для выборочных данных, но остается неизвестным, выполняется ли контракт правильно для входных данных вне выборки. Тесты свойств выполняют целевой контракт с множеством вариаций заданного входного значения, чтобы найти трассы выполнения, которые вызывают сбои утверждений. Таким образом, тест свойств предоставляет больше гарантий того, что контракт выполняется правильно для широкого класса входных данных.

Рекомендации по проведению тестирования на основе свойств для смарт-контрактов

Запуск тестирования на основе свойств обычно начинается с определения свойства (например, отсутствия целочисленных переполнений (opens in a new tab)) или набора свойств, которые вы хотите проверить в смарт-контракте. Вам также может потребоваться определить диапазон значений, в пределах которого программа может генерировать данные для входных транзакций при написании тестов свойств.

После правильной настройки инструмент тестирования свойств будет выполнять функции вашего смарт-контракта со случайно сгенерированными входными данными. Если есть какие-либо нарушения утверждений, вы должны получить отчет с конкретными входными данными, которые нарушают оцениваемое свойство. Ознакомьтесь с некоторыми руководствами ниже, чтобы начать тестирование на основе свойств с помощью различных инструментов:

Ручное тестирование смарт-контрактов

Ручное тестирование смарт-контрактов часто проводится на более поздних этапах цикла разработки после запуска автоматизированных тестов. Эта форма тестирования оценивает смарт-контракт как один полностью интегрированный продукт, чтобы проверить, работает ли он так, как указано в технических требованиях.

Тестирование контрактов в локальном блокчейне

Хотя автоматизированное тестирование, выполняемое в локальной среде разработки, может предоставить полезную информацию для отладки, вы захотите узнать, как ваш смарт-контракт ведет себя в производственной среде. Однако развертывание в основной цепи Эфириума влечет за собой комиссию за газ — не говоря уже о том, что вы или ваши пользователи можете потерять реальные деньги, если в вашем смарт-контракте все еще есть ошибки.

Тестирование вашего контракта в локальном блокчейне (также известном как сеть разработки) является рекомендуемой альтернативой тестированию в Мейннет. Локальный блокчейн — это копия блокчейна Эфириума, работающая локально на вашем компьютере, которая имитирует поведение уровня исполнения Эфириума. Таким образом, вы можете программировать транзакции для взаимодействия с контрактом без значительных накладных расходов.

Запуск контрактов в локальном блокчейне может быть полезен как форма ручного интеграционного тестирования. Смарт-контракты обладают высокой компонуемостью, что позволяет вам интегрироваться с существующими протоколами, но вам все равно нужно будет убедиться, что такие сложные ончейн-взаимодействия дают правильные результаты.

Подробнее о сетях разработки.

Тестирование контрактов в тестовых сетях

Тестовая сеть или тестнет работает точно так же, как основная сеть Ethereum, за исключением того, что в ней используется эфир (ETH), не имеющий реальной ценности. Развертывание вашего контракта в тестовой сети означает, что любой может взаимодействовать с ним (например, через фронтенд децентрализованного приложения (dapp)), не подвергая средства риску.

Эта форма ручного тестирования полезна для оценки сквозного потока вашего приложения с точки зрения пользователя. Здесь бета-тестировщики также могут выполнять пробные запуски и сообщать о любых проблемах с бизнес-логикой и общей функциональностью контракта.

Развертывание в тестовой сети после тестирования в локальном блокчейне идеально, поскольку первая ближе к поведению виртуальной машины Эфириума. Поэтому для многих нативных проектов Эфириума характерно развертывание dapp в тестовых сетях для оценки работы смарт-контрактов в реальных условиях.

Подробнее о тестовых сетях Эфириума.

Тестирование и формальная верификация

Хотя тестирование помогает подтвердить, что контракт возвращает ожидаемые результаты для некоторых входных данных, оно не может окончательно доказать то же самое для входных данных, не использованных во время тестов. Следовательно, тестирование смарт-контракта не может гарантировать «функциональную правильность» (т. е. оно не может показать, что программа ведет себя так, как требуется, для всех наборов входных значений).

Формальная верификация — это подход к оценке правильности программного обеспечения путем проверки того, соответствует ли формальная модель программы формальной спецификации. Формальная модель — это абстрактное математическое представление программы, в то время как формальная спецификация определяет свойства программы (т. е. логические утверждения о выполнении программы).

Поскольку свойства записываются в математических терминах, становится возможным проверить, что формальная (математическая) модель системы удовлетворяет спецификации, используя логические правила вывода. Таким образом, говорят, что инструменты формальной верификации предоставляют «математическое доказательство» правильности системы.

В отличие от тестирования, формальная верификация может использоваться для проверки того, что выполнение смарт-контракта удовлетворяет формальной спецификации для всех выполнений (т. е. в нем нет ошибок), без необходимости выполнять его с выборочными данными. Это не только сокращает время, затрачиваемое на запуск десятков модульных тестов, но и более эффективно выявляет скрытые уязвимости. Тем не менее, методы формальной верификации находятся в спектре в зависимости от сложности их реализации и полезности.

Подробнее о формальной верификации смарт-контрактов.

Тестирование, аудиты и программы bug bounty

Как уже упоминалось, тщательное тестирование редко может гарантировать отсутствие ошибок в контракте; подходы формальной верификации могут обеспечить более надежные гарантии правильности, но в настоящее время они сложны в использовании и влекут за собой значительные затраты.

Тем не менее, вы можете еще больше увеличить вероятность обнаружения уязвимостей контракта, проведя независимую проверку кода. Аудиты смарт-контрактов (opens in a new tab) и программы bug bounty (opens in a new tab) — это два способа привлечь других к анализу ваших контрактов.

Аудиты проводятся аудиторами, имеющими опыт поиска случаев недостатков безопасности и плохих практик разработки в смарт-контрактах. Аудит обычно включает тестирование (и, возможно, формальную верификацию), а также ручную проверку всей кодовой базы.

И наоборот, программа bug bounty обычно включает в себя предложение финансового вознаграждения лицу (обычно называемому белым хакером (opens in a new tab), которое обнаруживает уязвимость в смарт-контракте и раскрывает ее разработчикам. Программы bug bounty похожи на аудиты, поскольку они предполагают просьбу к другим помочь найти дефекты в смарт-контрактах.

Основное отличие заключается в том, что программы bug bounty открыты для более широкого сообщества разработчиков/хакеров и привлекают широкий класс этичных хакеров и независимых специалистов по безопасности с уникальными навыками и опытом. Это может быть преимуществом по сравнению с аудитами смарт-контрактов, которые в основном полагаются на команды, которые могут обладать ограниченным или узким опытом.

Инструменты и библиотеки для тестирования

Инструменты для модульного тестирования

  • solidity-coverage (opens in a new tab)Инструмент покрытия кода для смарт-контрактов, написанных на Solidity.

  • Waffle (opens in a new tab)Фреймворк для продвинутой разработки и тестирования смарт-контрактов (на основе Ethers.js).

  • Remix Tests (opens in a new tab)Инструмент для тестирования смарт-контрактов на Solidity. Работает под плагином Remix IDE «Solidity Unit Testing», который используется для написания и запуска тестовых сценариев для контракта.

  • OpenZeppelin Test Helpers (opens in a new tab)Библиотека утверждений для тестирования смарт-контрактов Эфириума. Убедитесь, что ваши контракты ведут себя так, как ожидается!

  • Фреймворк для модульного тестирования Brownie (opens in a new tab)Brownie использует Pytest, многофункциональный фреймворк для тестирования, который позволяет писать небольшие тесты с минимальным количеством кода, хорошо масштабируется для крупных проектов и обладает высокой расширяемостью.

  • Foundry Tests (opens in a new tab)Foundry предлагает Forge, быстрый и гибкий фреймворк для тестирования Эфириума, способный выполнять простые модульные тесты, проверки оптимизации газа и фаззинг контрактов.

  • Hardhat Tests (opens in a new tab)Фреймворк для тестирования смарт-контрактов на основе Ethers.js, Mocha и Chai.

  • ApeWorx (opens in a new tab)Фреймворк для разработки и тестирования смарт-контрактов на базе Python, ориентированный на виртуальную машину Эфириума.

  • Wake (opens in a new tab)Фреймворк на базе Python для модульного тестирования и фаззинга с мощными возможностями отладки и поддержкой кроссчейн-тестирования, использующий pytest и Anvil для лучшего пользовательского опыта и производительности.

Инструменты для тестирования на основе свойств

Инструменты статического анализа

  • Слизер (opens in a new tab)Фреймворк статического анализа Solidity на базе Python для поиска уязвимостей, улучшения понимания кода и написания пользовательских анализов для смарт-контрактов.

  • Ethlint (opens in a new tab)Линтер для обеспечения соблюдения лучших практик стиля и безопасности для языка программирования смарт-контрактов Solidity.

  • Cyfrin Aderyn (opens in a new tab)Статический анализатор на базе Rust, специально разработанный для безопасности и разработки смарт-контрактов Web3.

  • Wake (opens in a new tab)Фреймворк статического анализа на базе Python с детекторами уязвимостей и качества кода, принтерами для извлечения полезной информации из кода и поддержкой написания пользовательских подмодулей.

  • Slippy (opens in a new tab)Простой и мощный линтер для Solidity.

Инструменты динамического анализа

  • Эхидна (opens in a new tab)Быстрый фаззер контрактов для обнаружения уязвимостей в смарт-контрактах с помощью тестирования на основе свойств.

  • Diligence Fuzzing (opens in a new tab)Автоматизированный инструмент фаззинга, полезный для обнаружения нарушений свойств в коде смарт-контракта.

  • Мантикора (opens in a new tab)Фреймворк динамического символьного выполнения для анализа байт-кода EVM.

  • Mythril (opens in a new tab)Инструмент оценки байт-кода EVM для обнаружения уязвимостей контрактов с использованием анализа помеченных данных (taint analysis), конколического анализа и проверки потока управления.

  • Diligence Scribble (opens in a new tab)Scribble — это язык спецификаций и инструмент верификации во время выполнения, который позволяет аннотировать смарт-контракты свойствами, позволяющими автоматически тестировать контракты с помощью таких инструментов, как Diligence Fuzzing или MythX.

Дополнительная литература

Руководства: Тестирование смарт-контрактов в Эфириуме