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

Foundry prank: смена отправителя вызова

В предыдущем уроке мы прочитали пустую запись. Теперь нужно выполнить публикацию, на которую имеет право только издатель. Переменная с именем publisher сама по себе не назначает отправителя вызова. Разберём, как проверяющая среда задаёт msg.sender для конкретного действия и как легко потерять нужный контекст.

После урока вы сможете подготовить одиночный вызов издателя и короткую последовательность его действий. Для сравнения используем уже созданный CourseCatalog, номер 17 и известные роли фикстуры. Код предназначен для локальных сценариев и при подготовке не исполнялся.

Имя переменной не является полномочием

Когда проверяющий контракт вызывает catalog.publish, реестр получает непосредственного отправителя этого вызова. Само наличие поля address publisher у проверки не влияет на этот факт. В нашем реестре модификатор сравнивает msg.sender с адресом, переданным в конструктор. Для успешной публикации среда должна представить именно этот адрес.

Интерфейс vm позволяет управлять учебным контекстом. vm.prank(publisher) задаёт отправителя следующего подходящего вызова. Это возможность проверяющей среды, а не функция для произвольного посетителя действующей сети. Реальный пользователь по-прежнему должен получить право отправки своим допустимым способом; наличие примера с prank не заменяет ключ или подпись.

Добавим функцию внутрь CourseCatalogTest из третьего урока. В ней digest является собственной фикстурой байтов, а не форматом реального манифеста библиотеки.

function testPublisherCanPublish() public {
    bytes32 digest = keccak256("edition-one");

    vm.prank(publisher);
    catalog.publish(17, digest);

    (bytes32 stored, bool active) = catalog.courses(17);
    assertEq(stored, digest);
    assertTrue(active);
}

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

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

Один вызов означает именно один

Рассмотрим неправильную последовательность. Автор захотел сначала убедиться, что getter возвращает нужного издателя, и поставил чтение между prank и публикацией:

// Иллюстрация ошибки, не дополнительная успешная проверка.
vm.prank(publisher);
address observedPublisher = catalog.publisher();
catalog.publish(17, keccak256("edition-one"));

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

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

Последовательность Отправитель публикации Ожидание
Чтение → prank → публикация Издатель Изменение записи
prank → чтение → публикация Проверяющий контракт Отказ
prank(reader) → публикация Читатель Отказ
Публикация без подготовки роли Проверяющий контракт Отказ в нашей фикстуре

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

Контекст для нескольких действий

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

function testPublisherSequence() public {
    bytes32 first = keccak256("edition-one");
    bytes32 second = keccak256("edition-two");

    vm.startPrank(publisher);
    catalog.publish(17, first);
    catalog.publish(18, second);
    catalog.deactivate(17);
    vm.stopPrank();

    (bytes32 digest17, bool active17) = catalog.courses(17);
    (bytes32 digest18, bool active18) = catalog.courses(18);
    assertEq(digest17, first);
    assertFalse(active17);
    assertEq(digest18, second);
    assertTrue(active18);
}

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

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

Контекст вложенного вызова

Представим, что издатель обращается к отдельному посреднику, а тот вызывает реестр. Тогда непосредственным msg.sender для реестра станет адрес посредника. Наш базовый контракт не содержит разрешения такому адресу, поэтому перенос сценария через промежуточную программу меняет результат. Подмена отправителя прямого вызова не доказывает работоспособность этой цепочки.

По этой причине начинаем с одного ясно указанного адресата. Если архитектура позднее включает smart account или управляющий контракт, для неё потребуется собственная проверка полномочий и маршрута. Поле tx.origin не следует вводить как способ обойти это различие: у реестра намеренно используется непосредственный отправитель. В уроке о ролях разберём расширение отдельно, сохранив исходную лабораторию простой.

Две роли и две причины результата

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

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

Также нельзя считать msg.sender сведением о паспорте или авторстве текста. Контракт проверяет адресное полномочие, установленное при создании. Каким способом редакция выбрала этот адрес и кто контролирует его действия, определяется отдельной моделью. Наши проверки не должны делать выводы, которые не выражены в контракте.

Сейчас сохранены два разрешённых сценария с явными отправителями и объяснена ошибка потери одноразового контекста. Результаты являются ожидаемыми; сетевые операции и Forge не запускались. В следующем уроке проверим отказ читателю и отличим ожидаемую Unauthorized от любого случайного отката.

Назначение отправителя описано в справочнике prank и исходниках Vm выбранного тега. Оглавление курса.

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