Перейти к содержанию

Foundry: выбор целей генератора инвариантных тестов

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

В CourseCatalogInvariantTest заменим setUp, указав два изменяющих метода и издателя. Это промежуточная кампания разрешённых действий, которую позднее заменит handler. Пример не исполнялся, поэтому доля успешных вызовов и качество фактической выборки ещё неизвестны.

Адрес и метод задают разные границы

targetContract сообщает, какой контракт является целью генерируемых обращений. targetSelector ограничивает интересующие селекторы на указанном адресе. У нашего реестра нужны publish(uint256,bytes32) и deactivate(uint256), а getter состояния используется для наблюдения.

Селектор определяется именем и типами аргументов функции. Не набираем его вручную шестнадцатеричной строкой: Solidity может получить величину через CourseCatalog.publish.selector. Это связывает подготовку с объявленным интерфейсом и уменьшает вероятность опечатки в адресуемом методе.

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

Ограничиваем прямую кампанию

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

function setUp() public {
    catalog = new CourseCatalog(publisher);

    bytes4[] memory selectors = new bytes4[](2);
    selectors[0] = CourseCatalog.publish.selector;
    selectors[1] = CourseCatalog.deactivate.selector;

    targetContract(address(catalog));
    targetSelector(FuzzSelector({
        addr: address(catalog),
        selectors: selectors
    }));
    targetSender(publisher);
}

Структура FuzzSelector доступна через StdInvariant выбранного forge-std 1.9.7. Она содержит адрес и массив селекторов. В данном случае адрес совпадает с целью реестра, а каждый элемент массива относится к его ABI. Мы проверили форму API по исходнику библиотеки; выполнение примера остаётся отдельным будущим этапом.

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

Ограничение роли не исправляет входы

Даже правильный отправитель может передать нулевой номер или отпечаток и получить InvalidCourse. Метод deactivate с неизвестным или неактивным номером получает NotActive. Выбор адреса не делает все случайные действия корректными и не должен описываться как гарантия полезной работы.

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

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

Проверяем выбранную поверхность глазами

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

Настройка Включено За пределами этой кампании
Контракт Наш экземпляр catalog Другие развёрнутые реестры
Селекторы Публикация и отключение Чтение как случайное действие
Отправитель Заданный издатель Запрет читателю
Входы Полный ABI-диапазон Связная малая история ещё не обеспечена

Если адрес не совпадает с ожидаемым реестром, выбор правильных селекторов не помогает. Та же сигнатура может существовать в другой программе с другим поведением. Поэтому конфигурация адреса, профиля исходников и ABI рассматривается вместе, особенно в fork-проверках ближе к концу курса.

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

Изменённый пример с read-only методом

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

Ещё более опасный вариант — направить генератор на сам проверяющий контракт. Тогда в поверхность могут попасть вспомогательные функции и настройки, которые не являются прикладными действиями. Явный адрес цели помогает удерживать границу между программой наблюдения и исследуемым объектом.

У будущего handler тоже будут публичные getter и вспомогательные средства от Test. Поэтому мы зададим его целевые методы селекторами, а не предположим, что любая открытая функция подходит генерации. Читатель увидит небольшой список действий, формирующий именно историю библиотеки.

Исключённая область требует объяснения

Изменение имени метода тоже влияет на выбор селектора, даже если типы аргументов сохранены. Поэтому список целей является частью версии лаборатории. При переносе на новый контракт рецензент должен увидеть, какие методы добавлены, удалены или переименованы и зачем это сделано. Случайно оставленный старый список может породить множество отказов вместо исследования новой программы.

Мы не сохраняем селекторы как необъяснимые магические числа. Их выражения рядом с объявлениями методов позволяют проследить связь с ABI и договором. В будущем отчёте эти значения можно записать вместе с адресом цели, но сама запись числа не заменяет описания прикладного действия.

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

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

В следующем уроке создадим посредника для осмысленных действий над номерами 1..20. API целей доступен в StdInvariant 1.9.7, общий механизм описан в руководстве Foundry. Оглавление курса.

Оглавление серии