Безопасность Solidity: устаревший ответ и недоступность
PositiveQuoteReader проверял применимый диапазон ответа, но намеренно игнорировал updatedAt. Даже правильное положительное число может быть слишком старым для текущей операции. Добавим FreshQuoteReader с явным максимальным возрастом. Он использует собственный IQuoteSource предыдущего урока и не меняет CourseCatalog.
Предметное правило такое: updatedAt ненулевой, не позже текущего block.timestamp, а возраст не больше maxAge. Равенство age==maxAge допускается. Эти границы зафиксированы заранее, чтобы последующие главы о времени продолжали точный договор, а не выбирали новое сравнение по привычке.
Полный reader свежести
// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;
import {IQuoteSource} from "./LabQuoteSource.sol";
contract FreshQuoteReader {
error InvalidSetup();
error InvalidQuote();
error InvalidTime();
error StaleQuote();
int256 public constant MAX_ANSWER = 1_000_000 * 10 ** 8;
IQuoteSource public immutable source;
uint256 public immutable maxAge;
constructor(address source_, uint256 maxAge_) {
if (source_ == address(0) || maxAge_ == 0) revert InvalidSetup();
source = IQuoteSource(source_);
maxAge = maxAge_;
}
function get() external view returns (uint256) {
(int256 answer, uint8 decimals_, uint256 updatedAt) = source.read();
if (answer <= 0 || answer > MAX_ANSWER || decimals_ != 8)
revert InvalidQuote();
if (updatedAt == 0 || updatedAt > block.timestamp) revert InvalidTime();
if (block.timestamp - updatedAt > maxAge) revert StaleQuote();
return uint256(answer);
}
}
Это полный собственный исходник с предыдущим интерфейсом. Источник не устанавливается, узел не запускается и get не выполняется. Примеры дальше являются ожидаемыми числовыми сценариями. MAX_ANSWER и decimals остаются теми же, что на первом этапе, поэтому временная проверка не подменяет уже установленные ограничения.
Порядок проверки времени
Сначала исключаем updatedAt в будущем, затем вычитаем его из timestamp. Так некорректная будущая отметка получает InvalidTime без underflow. Положение строк имеет предметный смысл: arithmetic range и правдоподобный порядок времени проверяются как разные условия.
Нулевой updatedAt обозначает неприемлемый для нас ответ без установленной отметки. Даже если блок сейчас имеет большое время и разность получилась бы старой, специальная ошибка позволяет точнее классифицировать отсутствие первоначальных данных. Формат времени не должен маскировать такое состояние под обычное устаревание.
Граница одного часа
Выберем для мысленного сценария maxAge=3600. При текущем timestamp T ответ T-3599 допустим, T-3600 тоже допустим, T-3601 отклоняется. Ответ T+1 отклоняется как будущий, а не становится искусственно очень свежим после ошибочного unsigned преобразования.
updatedAt=T, age=0 → допускается
updatedAt=T-3600, age=3600 → допускается
updatedAt=T-3601, age=3601 → StaleQuote
updatedAt=T+1 → InvalidTime
updatedAt=0 → InvalidTime
Нужно выбирать T достаточно большим для этих иллюстраций, чтобы сами выражения T-3601 имели смысл. Это ещё одна причина писать конкретные предпосылки вместо механического набора граничных чисел. Таблица не является логом реально проведённого warp или RPC-запроса.
Откуда берётся maxAge
Час — выбранная величина учебного примера, не универсальный безопасный предел. Реальный источник может обновляться при изменении значения, по периодическому условию или иметь другую договорённость. Политика потребителя должна соответствовать источнику и последствиям использования старого числа.
Слишком маленький предел будет часто отклонять обычные ответы. Слишком большой позволит использовать нежелательно старые данные. Положительная maxAge в конструкторе устраняет только нулевую настройку. Она не доказывает правильность огромного либо произвольно выбранного интервала для конкретной услуги.
Также timestamp обновления не всегда совпадает с моментом возникновения наблюдаемого события в мире. Наш reader проверяет заявленную отметку по protocol. Он не доказывает, что источник честно измерил показатель именно тогда. Доверие к содержанию и формальная свежесть остаются отдельными условиями.
Недоступность источника
Если source.read делает revert, наш get тоже отклоняется, поскольку error не перехвачена. Нельзя заменять сбой нулём и считать это корректной бесплатной ценой. Отсутствие ответа означает неизвестные данные, а не известный ответ ноль.
Для будущей локальной фикстуры можно использовать собственный UnavailableQuoteSource. Его полный минимальный файл импортирует тот же интерфейс и всегда отклоняет чтение; никаких внешних сервисов для этого не нужно.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;
import {IQuoteSource} from "./LabQuoteSource.sol";
contract UnavailableQuoteSource is IQuoteSource {
error Unavailable();
function read() external pure returns (int256, uint8, uint256) {
revert Unavailable();
}
}
Если продукт хочет fallback, нужно определить источник, предел его возраста, порядок выбора и разрешённые операции. Автоматическое использование последнего кэша без срока является другой политикой, которой FreshQuoteReader не содержит. На интерфейсе следует показать причину отказа вместо заявления об отрицательном или нулевом числе.
Изменение среды
В другой сети могут быть дополнительные требования доступности и времени. Например, особенности L2 или источника рассматриваются по их официальной документации. Наша проверка age не доказывает работоспособность любого окружения с тем же ABI. Она обеспечивает только явно описанное условие собственного protocol.
При переключении клиента сохраняйте сеть и адрес reader вместе с source и maxAge. Значение одного часа в другом экземпляре может быть совершенно иной конфигурацией. Использование кэша от прежней сети не становится допустимым от того, что числовое answer выглядит привычно.
Передача следующему этапу
После этой главы имеются собственный управляемый LabQuoteSource, reader знака и масштаба и полный FreshQuoteReader. Базовый CourseCatalog не получил цены, средств или зависимости источника. Это самостоятельные модули для последовательного изучения конкретных границ безопасности.
Для задания возьмите положительный answer с decimals восемь и поменяйте только updatedAt по таблице. Затем оставьте свежую отметку и поменяйте знак ответа. Отдельно используйте мысленный недоступный source. Объясните различие InvalidQuote, InvalidTime, StaleQuote и ошибки внешнего чтения. Следующий урок продолжит включённую границу возраста и сравнит её с иными правилами срока, без исполнения примеров в текущей работе.
Проверка не даёт бессрочный результат
Успешный get доказывает нашу временную границу на момент конкретного вызова. Если клиент сохранил возвращённое число на сутки, оно не становится автоматически свежим всё это время. Для нового решения нужно повторить соответствующее чтение либо применять отдельно определённую политику кэша с известной точкой.
В нашем reader maxAge immutable. Его нельзя изменить административной кнопкой без новой модели или нового экземпляра. Если первоначальная настройка выбрана неверно, приложение должно определить явный переход к другому reader и сохранить прежнюю конфигурацию в истории. Обновление одного локального числа на странице не меняет on-chain правило старого адреса.
Повторная запись того же answer с новым updatedAt нашим admin формально освежает отметку. Reader не способен самостоятельно установить, было ли проведено новое наблюдение. Это ожидаемый предел доверия к source, который нельзя скрыть за проверкой age. Поэтому реальная система изучает смысл обновления и обязанности поставщика данных отдельно.
При недоступном RPC клиент также не знает, завершилось ли on-chain чтение корректным ответом. Ошибка транспорта отличается от StaleQuote и от Unavailable источника. Пользовательское сообщение должно сохранять это различие, чтобы неизвестность не превращалась в выдуманную свежую цену.
Для самостоятельного уточнения: units-and-global-variables.html, selecting-data-feeds, using-data-feeds.
Оглавление курса. Листинги, расчёты и наблюдения здесь разобраны как ожидаемое поведение; компиляция, учебные программы и проверки при написании не запускались.