Безопасность Solidity: входные значения и достижимые состояния
Solidity проверяет, что uint256 является целым без знака, а bytes32 имеет определённое представление. Эти типы не знают предметных требований библиотеки. Они допускают нулевой идентификатор, пустой digest и произвольное большое время. Поэтому валидация связывает представление со смыслом конкретного перехода.
В базовом CourseCatalog publish отклоняет id ноль и hash ноль. Конструктор отклоняет нулевого publisher. Deactivate требует active. Каждое условие относится к своей функции и не превращается в глобальную проверку любого будущего setter автоматически.
Отдельное расписание
Для нового примера создадим BoundedEditorialSchedule. Он хранит будущий интервал действия и digest по id. Это отдельная модель, не новое скрытое значение active исходного registry. Наш договор разрешает начало сейчас или позже, положительную длительность и максимум семь суток.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;
contract BoundedEditorialSchedule {
error Unauthorized();
error InvalidPlan();
struct Plan { uint256 start; uint256 end; bytes32 digest; }
address public immutable scheduler;
mapping(uint256 => Plan) public plans;
constructor(address scheduler_) {
if (scheduler_ == address(0)) revert InvalidPlan();
scheduler = scheduler_;
}
function planCourse(uint256 id, bytes32 digest, uint256 start, uint256 end) external {
if (msg.sender != scheduler) revert Unauthorized();
if (id == 0 || digest == bytes32(0) || start < block.timestamp || end <= start)
revert InvalidPlan();
if (end - start > 7 days) revert InvalidPlan();
plans[id] = Plan(start, end, digest);
}
}
Это полный учебный исходник. Сначала проверяется порядок end>start, только затем вычисляется end-start. Благодаря этому неправильный порядок получает InvalidPlan, а не случайную арифметическую ошибку. В checked arithmetic вычитание не исправляет предметный порядок само по себе; оно лишь отклонит выход за диапазон.
Взаимная связь полей
Start и end по отдельности могут быть допустимыми uint256, но составлять невозможный интервал. Аналогично массив ids и amounts по отдельности имеет правильные элементы, но разную длину. Проверка связанных полей нужна отдельно от проверки каждого типа и каждого значения.
У нашего расписания допустимо start==block.timestamp. Условие start<timestamp отклоняет только прошлое начало. Если продукт требует подготовку минимум за час, это другой явный предикат с аккуратным учётом границы. Нельзя приписать такую задержку модели только по слову «будущий» в заголовке.
Максимум семь суток относится к разности, а не к абсолютному Unix timestamp. Сравнение end<=7days означало бы совершенно другой смысл. Похожая ошибка масштаба возникает, когда blocknumber сравнивают с timestamp. Компилятор может принять обе формы, потому что числа имеют подходящий тип.
Повторная запись
PlanCourse заменяет план существующего id. Здесь нет проверки «план уже создан» и нет истории версий. Правильный scheduler может перезаписать значение после предыдущей операции, если новое начало удовлетворяет текущему времени. Такое поведение допускается кодом и должно быть отражено в интерфейсе.
Если нужны только первоначальные назначения, следует хранить признак существования или использовать подходящее условие. Если нужен ожидаемый предыдущий digest, он становится отдельным аргументом проверки. Это предметные ограничения перехода, а не универсальная необходимость запретить любые обновления.
Условия на клиенте и в контракте
Клиентская форма может предотвратить обычную опечатку и показать понятное сообщение. Но другой caller способен сформировать calldata вне этой формы. Поэтому on-chain условие требуется там, где оно защищает достижимое состояние. Один disabled button не устанавливает его для всех участников.
После подготовки формы время может измениться до исполнения. Начало, которое было не прошлым на экране, окажется прошлым в блоке транзакции. Это ожидаемая проверка по фактическому состоянию, а не сбой сети. Пользователь должен знать, почему повторная подготовка интервала может понадобиться.
Адрес scheduler также является входом первоначальной настройки. Ненулевой адрес проходит нашу проверку, но может быть неподходящим контрактом без маршрута planCourse. Валидация формата уменьшает один риск и не подтверждает все свойства выбранного субъекта.
Ошибка, assert и require
Публичные неподходящие параметры в этом примере отклоняются собственными ошибками. Assert обычно используется для внутренних свойств, которые по логике корректной программы не должны нарушаться. Превращение каждого пользовательского ограничения в assert не объясняет его смысл лучше.
Также нельзя удалять проверку только потому, что нормальный сайт не создаёт плохие параметры. Контракт публично доступен, а будущий другой интерфейс может иметь иной bug. Договор состояния защищается на соответствующей границе ввода, а форма помогает пользователю действовать правильно.
Размер и ресурсы
В нашем плане нет массива и внешнего loop. Но число разных mapping ключей может расти при допустимых публикациях scheduler. Максимум длительности одной записи не ограничивает общий объём истории. Граница входа и граница роста storage являются разными свойствами, которые поздняя глава разберёт отдельно.
При добавлении batch нужно определить максимальный размер, одинаковую длину связанных массивов, правила повторений и атомарность. Лимит интерфейса на восемь строк не является контрактным пределом, если функция его не проверяет. Точное требование сохраняется в исходнике и описании будущих сценариев.
Разбор границ
Возьмите timestamp T. Рассмотрите start=T, end=T+7days; start=T-1; end=start; end=start+7days+1; id ноль и digest ноль. Для каждого назовите первую нарушенную проверку и объясните, меняется ли прежний plans[id]. Отклонённый вызов не оставляет частичного плана.
Затем попробуйте повторно назначить существующий id с допустимым новым интервалом и сформулируйте ожидаемую замену. Такая положительная граница так же важна, как отрицательные cases. Сейчас сценарии не выполняются; мы фиксируем достижимые состояния по конкретному коду и готовим анализ арифметических представлений следующего урока.
Связь с манифестом
Ненулевой digest в планах не подтверждает наличие файла по будущему URL. Если редакция требует подготовить материал до назначения интервала, это отдельный процесс проверки точных байтов и ресурсов. Присваивание bytes32 защищает выбранное представление, но не открывает браузер и не проверяет качество текста.
Поэтому состояние «план принят» нельзя подписать как «урок доступен читателю», пока другой компонент не обеспечил нужную публикацию. Разделение результатов не ослабляет контрактную проверку; оно делает область каждого требования точной. В учебном примере путь хранения файла остаётся таким же самостоятельным, как в первой карте границ доверия.
Для самостоятельного уточнения: types.html, control-structures.html, units-and-global-variables.html.
Оглавление курса. Листинги, расчёты и наблюдения здесь разобраны как ожидаемое поведение; компиляция, учебные программы и проверки при написании не запускались.