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

Безопасность Solidity: пауза, восстановление и сообщение об инциденте

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

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

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

Сначала определим область нарушения

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

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

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

Отдельная пауза записи

Введём новый IncidentCourseCatalog. Он не заменяет автоматически исходный CourseCatalog и не добавляет паузу к уже развёрнутому immutable-контракту. Publisher управляет материалом, guardian выбирает режим записи. У обеих ролей есть собственная ответственность.

pragma solidity 0.8.30;

contract IncidentCourseCatalog {
    struct Course { bytes32 contentHash; bool active; }
    address public immutable publisher;
    address public immutable guardian;
    bool public writesPaused;
    mapping(uint256 => Course) public courses;
    error Unauthorized();
    error Paused();
    error InvalidCourse();
    error NotActive();
    event Published(uint256 indexed courseId, bytes32 contentHash);
    event Deactivated(uint256 indexed courseId);
    event WriteModeChanged(bool paused);

    constructor(address publisher_, address guardian_) {
        require(publisher_ != address(0) && guardian_ != address(0), "roles required");
        publisher = publisher_;
        guardian = guardian_;
    }

    modifier permittedWrite() {
        if (msg.sender != publisher) revert Unauthorized();
        if (writesPaused) revert Paused();
        _;
    }

    function setWritesPaused(bool paused) external {
        if (msg.sender != guardian) revert Unauthorized();
        writesPaused = paused;
        emit WriteModeChanged(paused);
    }

    function publish(uint256 id, bytes32 hash) external permittedWrite {
        if (id == 0 || hash == bytes32(0)) revert InvalidCourse();
        courses[id] = Course(hash, true);
        emit Published(id, hash);
    }

    function deactivate(uint256 id) external permittedWrite {
        if (!courses[id].active) revert NotActive();
        courses[id].active = false;
        emit Deactivated(id);
    }
}

Когда writesPaused равен true, обе предметные записи отклоняются. Getter courses остаётся доступным. Административный выбор режима является специально разрешённым исключением, иначе снять паузу было бы невозможно. Ввод новых маршрутов подписи, batch или обновления потребовал бы отдельно проверить, какие из них должны учитывать этот режим.

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

Пауза не является отменой разрешений

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

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

Сохранение свидетельств

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

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

Сообщение о пределах знания

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

Условный текст для проекта может говорить: «Новые публикации временно остановлены. Чтение ранее доступных материалов продолжается. Проверяем источник изменения реестра; история сохранена». Такая формулировка не утверждает ни кражу средств, ни полное восстановление. Реальное сообщение составляется по фактам конкретного случая и согласованной области.

Возврат к работе

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

Манифест новой редакции получает SHA-256 точных байтов согласно общему договору курса. Старый CID или подпись EIP-712 не заменяют этот хеш. Редакционный выпуск и административное восстановление связаны, но каждый имеет свой идентификатор и проверяемое состояние.

Деактивация курса не отзывает файл, который читатель уже сохранил. Blockchain storage и публичные копии истории тоже не исчезают от паузы. Если продукт хочет ограничить будущую выдачу защищённого ресурса, это задача сервера и собственной политики доступа; нельзя обещать удаление всех уже распространённых копий.

Ожидаемые результаты

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

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

Область временного ограничения

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

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

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