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

Безопасность Solidity: регрессионный сценарий и отчёт

Оглавление курса «Безопасность: разбор собственного контракта» · Урок 30 из 30.

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

Проверки и программы не запускаются. Foundry, Solidity и зависимости сейчас не устанавливаются и не компилируются. Исходник проверки ниже предназначен для будущего отдельного запуска на профиле Solidity 0.8.30/Cancun, Foundry 1.3.1 и OpenZeppelin Contracts 5.4.0. Статус результата в отчёте остаётся ожидаемым, а не успешно измеренным.

Выберем одну причину

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

Предусловия ограничивают вывод. Нужен именно такой путь создания, доступный initialize и отсутствие уже применённого ограничения первой настройки. Базовый CourseCatalog с конструктором и immutable publisher не относится к этому сценарию. Подменить его адресом прокси и объявить общую уязвимость было бы неверно.

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

Карточка находки

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

finding: PW-LAB-INIT-01
model: UpgradeableCourseCatalogV1
cause: proxy_created_without_atomic_initializer
preconditions:
  - proxy_storage_is_not_initialized
  - first_initialize_call_is_publicly_reachable
affected_property: expected_publisher_is_assigned_at_creation
impact: unauthorized_choice_of_publisher_in_this_model
fix: ERC1967Proxy_constructor_receives_encoded_initialize
implementation_lock: constructor_calls_disableInitializers
execution: not_run
result: expected_from_source_reasoning

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

Свойство исправленной модели

После атомарного создания publisher должен сразу совпадать с переданным ожидаемым адресом. Поздний initialize не меняет это значение. Прямое назначение через реализацию закрыто её конструктором. Эти свойства независимы: защита реализации не заменяет настройку прокси, а однократность initialize не выбирает правильного первого субъекта.

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

Регрессионный исходник

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

pragma solidity 0.8.30;
import {Test} from "forge-std/Test.sol";
import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import {TeachingCatalogFactory} from "../src/TeachingCatalogFactory.sol";
import {UpgradeableCourseCatalogV1} from "../src/UpgradeableCourseCatalogV1.sol";

contract InitializationRegressionSource is Test {
    function test_atomicCreationKeepsExpectedPublisher() public {
        address expectedPublisher = makeAddr("expected-publisher");
        address other = makeAddr("other-reader");
        TeachingCatalogFactory factory = new TeachingCatalogFactory();
        address proxy = factory.create(expectedPublisher);
        UpgradeableCourseCatalogV1 catalog = UpgradeableCourseCatalogV1(proxy);

        assertEq(catalog.publisher(), expectedPublisher);
        vm.expectRevert(Initializable.InvalidInitialization.selector);
        vm.prank(other);
        catalog.initialize(other);
        assertEq(catalog.publisher(), expectedPublisher);
    }
}

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

Отдельный исходник должен проверять, что initialize реализации отклоняется после _disableInitializers. Ещё одна группа проверяет совместимость V1→V2: publisher, courses(17), active и releaseCount сохраняются, а retentionEpoch получает нужную метку. Нельзя объединить все свойства в один assertTrue(success), поскольку потеряется смысл причины отказа.

Отчёт об объёме анализа

В документе перечисляем рассмотренные пути: публикация, деактивация, подписи и nonce в соответствующих отдельных моделях, вывод кредита, обработка очереди, proxy-инициализация и делегирование. Каждая модель имеет свой исходник и собственные предусловия. Не выдаём совокупность учебных контрактов за один огромный production-контракт со всеми функциями одновременно.

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

Остаточные ограничения

Publisher учебного UUPS-реестра имеет право выбрать новый код. Совместимое размещение не гарантирует добросовестное управление. Пауза отдельной incident-модели не отзывает прежние подписи автоматически и не стирает публичную историю. Ограниченная очередь продолжает требовать исполнителя, а очистка обработанной области не делает mapping перечислимым.

Материал по contentHash проверяется SHA-256 точных байтов манифеста. Это не подменяется CID или digest EIP-712. Пользовательский доступ и фактическая сохранность ключей находятся за пределами одного Solidity-листинга. Сайт также имеет собственные источники RPC, серверные сессии и политики хранения.

Как завершить документ

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

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

Результат, который не подтвердился

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

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

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

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