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

Безопасность Solidity: граница срока действия

В пятнадцатом уроке ограничили возраст ответа собственного источника котировки: age <= maxAge. Теперь разберём срок действия самого намерения. Даже если пользователь отправил транзакцию до дедлайна, её исполнение может произойти позже. Контракт проверяет состояние в момент выполнения, а не время нажатия кнопки в браузере.

Сохраняем профиль Solidity 0.8.30 и EVM Cancun. Базовый CourseCatalog не меняем; примеры срока создаём в отдельной модели. Никаких транзакций, часов узла или проверок сейчас не запускаем. Времена ниже — условные входы для точного разбора границ.

Два разных условия времени

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

Для FreshQuoteReader раньше приняли равенство на границе возраста. Если ответ обновлён в 1000, а maxAge=60, время 1060 ещё допустимо, 1061 уже нет. Срок намерения тоже определяем явно, чтобы разные части приложения не расходились на последней секунде.

Выберем условие block.timestamp <= deadline. Таким образом, равенство допускается. Это собственная политика примера, а не обязательное правило языка. Другой протокол может использовать строгую границу; важно, чтобы исходник, интерфейс и ожидаемый результат описывали одно решение.

Отправление не устанавливает срок исполнения

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

Принятый RPC-запрос, полученный transaction hash и нахождение в mempool тоже не гарантируют своевременное включение. Интерфейс показывает эти этапы отдельно. Нельзя написать «Успешно» после получения хеша, если контракт ещё не выполнил временные ограничения.

Для сайта это означает понятный маршрут повторения: пользователь получает новое намерение с новым сроком по установленным правилам. Не меняем старые подписанные поля автоматически и не обещаем, что увеличение комиссии всегда успеет спасти прежний дедлайн.

Выделяем проверку в отдельную модель

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

pragma solidity 0.8.30;

contract TimeWindow {
    error InvalidWindow();
    error TooEarly();
    error Expired();

    function requireWindow(uint256 notBefore, uint256 deadline)
        external view
    {
        if (deadline < notBefore) revert InvalidWindow();
        if (block.timestamp < notBefore) revert TooEarly();
        if (block.timestamp > deadline) revert Expired();
    }
}

Если notBefore=1900, deadline=2000, то условные времена 1900 и 2000 проходят. Время 1899 даёт TooEarly, 2001 — Expired. Значение deadline=1890 при прежнем начале окна отвергается независимо от текущего времени. Это ожидаемые результаты чтения листинга, а не выполненные проверки.

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

Не вычитаем время до проверки порядка

В FreshQuoteReader сначала исключали будущий updatedAt, а затем вычисляли возраст. Такой порядок предотвращает underflow при вычитании из меньшего значения. В окне срока аналогично сначала устанавливаем связь конца и начала, если планируем вычислять длительность.

Checked arithmetic Solidity помогает заметить недопустимое вычитание, но произвольный panic не всегда даёт читателю понятную причину. Явная ошибка InvalidWindow выражает предметное условие и сохраняет согласованность интерфейса. При этом обработчик не должен путать техническую ошибку с недоступностью источника.

Номер блока не заменяет секунды. Длительность между блоками и модель сети отличаются, поэтому block.number + 10 нельзя без отдельного договора называть «через десять минут». Если приложение выбирает срок в блоках, это другая единица и отдельная политика.

Время сети и предел точности

Поля блока задаются правилами конкретной сети. Не приписываем timestamp произвольную свободу оператора во всех современных сетях и не обещаем идеальную связь с часами пользователя. Для учебного контракта это значение среды исполнения с выбранной семантикой.

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

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

Меняем условие: операция повторилась

Пусть разрешение успешно использовано в 1995, а тот же payload повторили в 1996. Дедлайн всё ещё действует, поэтому одна временная проверка не остановит повтор. Для однократного действия потребуется nonce или другая запись использования, которую введём позднее.

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

Единицы времени в интерфейсе

Браузерные даты часто представлены миллисекундами, а timestamp контракта выражается секундами. Перед подготовкой payload единицу нужно зафиксировать явно. Передача миллисекунд в поле секунд продлит окно в тысячу раз, оставаясь при этом допустимым целым числом. Тип Solidity не заметит такую предметную ошибку.

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

Разбор результата

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

В следующем уроке посмотрим, почему timestamp и другие публичные поля не обеспечивают обещанную непредсказуемость. Полный маршрут находится в оглавлении курса.

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