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

Безопасность Solidity: ответ оракула и масштаб

Контракт не узнаёт внешнее состояние мира автоматически. Он получает данные через выбранный on-chain источник. Перед использованием числа нужно понять его предмет, знак, масштаб и доверенный адрес. Положительный return подходящего типа ещё не означает, что ответ относится к нужной паре или допустим для нашей учебной операции.

Для разбора создадим собственный IQuoteSource. Его read возвращает int256 answer, uint8 decimals и uint256 updatedAt. Это специальный лабораторный protocol, не интерфейс Chainlink и не инструкция подключить произвольный feed этим ABI. Реальные источники имеют свои методы, поля и требования, изучаемые отдельно.

Полный управляемый источник

Сохраните первый исходник как LabQuoteSource.sol для будущей лаборатории. Admin может устанавливать в нём даже некорректные ответы, чтобы исследовать проверяющий reader. Такая свобода намеренна для собственной фикстуры и не является рекомендуемой политикой реального источника.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;
interface IQuoteSource {
    function read() external view returns (int256 answer, uint8 decimals, uint256 updatedAt);
}
contract LabQuoteSource is IQuoteSource {
    error Unauthorized();
    error InvalidSetup();
    address public immutable admin;
    int256 private answer;
    uint8 private scale;
    uint256 private timestamp;
    constructor(address admin_) {
        if (admin_ == address(0)) revert InvalidSetup();
        admin = admin_;
    }
    function set(int256 value, uint8 decimals_, uint256 updatedAt_) external {
        if (msg.sender != admin) revert Unauthorized();
        answer = value;
        scale = decimals_;
        timestamp = updatedAt_;
    }
    function read() external view returns (int256, uint8, uint256) {
        return (answer, scale, timestamp);
    }
}

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

Reader с выбранной областью

Второй полный файл PositiveQuoteReader.sol импортирует именно наш интерфейс. Учебная единица имеет восемь decimals, а максимально допустимый answer равен миллиону отображаемых единиц. Числа выбраны для демонстрации диапазона, не являются рыночной оценкой или общей настройкой любых data feeds.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;
import {IQuoteSource} from "./LabQuoteSource.sol";
contract PositiveQuoteReader {
    error InvalidSetup();
    error InvalidQuote();
    int256 public constant MAX_ANSWER = 1_000_000 * 10 ** 8;
    IQuoteSource public immutable source;
    constructor(address source_) {
        if (source_ == address(0)) revert InvalidSetup();
        source = IQuoteSource(source_);
    }
    function get() external view returns (uint256) {
        (int256 answer, uint8 decimals_,) = source.read();
        if (answer <= 0 || answer > MAX_ANSWER || decimals_ != 8)
            revert InvalidQuote();
        return uint256(answer);
    }
}

В этой главе updatedAt намеренно ещё не проверяется. Это ограниченный этап развития примера: он проверяет знак, величину и масштаб, но не свежесть. Следующий урок добавит точную временную политику в отдельный FreshQuoteReader. Нельзя применять PositiveQuoteReader как будто все поля уже защищены.

Знак до преобразования

Answer является signed. Проверка answer<=0 отклоняет и ноль, и отрицательные числа. Только затем преобразование в uint256 получает нужный положительный смысл. Если сначала привести отрицательный answer, можно получить большое unsigned представление вместо ожидаемого предметного отказа.

Верхняя граница также проверяется в signed области до cast. Тип int256 допускает множество положительных значений, которые не подходят выбранной лабораторной квоте. Компилятор защищает своё представление, а MAX_ANSWER задаёт дополнительную допустимую область продукта.

Decimals не универсальны

Наш reader ожидает ровно восемь decimals и отклоняет другой масштаб. Answer 300×10⁸ означает 300 отображаемых единиц по этому договору. То же сырое число с decimals шесть означало бы другое значение. Сравнение только абсолютного answer не обнаружит такую ошибку смысла.

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

Адрес и предмет источника

Источник выбирается конструктором и остаётся immutable. Это фиксирует адрес, но не доказывает качество его данных. Даже наш LabQuoteSource сохраняет административную возможность set, а другой источник по тому же адресу может иметь свою политику обновления. Reader доверяет оговорённому компоненту.

Также в нашем ABI нет названия пары или типа наблюдения. Если приложение ошибочно выберет источник другого показателя с тем же масштабом, числовые проверки могут пройти. Привязка адреса к предмету находится в конфигурации и документированном договоре источника, а не автоматически внутри uint256.

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

Ожидаемые ответы

Возьмите обычное значение 300×10⁸, отрицательное -1, ноль, MAX_ANSWER+1 и тот же обычный ответ с decimals шесть. Для каждого укажите outcome get. Начальный нулевой источник и источник с исправленным set должны дать разные результаты по нашей явной проверке.

Потом оставьте корректные answer и decimals, но старый updatedAt. PositiveQuoteReader ещё примет такой ответ. Это обнаруженное ограничение текущего этапа, а не скрытое обещание полной безопасности. В следующей главе время станет самостоятельным условием, чтобы успешный вызов не разрешал использовать давно устаревший коэффициент.

Одна согласованная запись

LabQuoteSource.read возвращает три поля одним вызовом. В нашей модели они берутся из одного состояния чтения. Если вместо него клиент делает три независимых RPC-запроса к отдельным getters на latest, между запросами может пройти set. Тогда answer будет старым, а scale или timestamp новым. Для воспроизводимого отчёта выбирают согласованную точку и форму получения данных.

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

Подготовка собственной фикстуры

После создания LabQuoteSource значения нулевые, поэтому первый get ожидаемо отклоняется. Затем его выбранный admin устанавливает положительное значение, scale восемь и конкретную отметку. Reader начинает принимать числовую область, но пока всё ещё не анализирует время. Так последовательность имеет два понятных состояния без необходимости обращаться к настоящему рыночному feed.

Если посторонний X пытается set, источник отклоняет запись Unauthorized. Это защищает управление нашей лабораторной тройкой, но не подтверждает качество ответа admin. Уполномоченный субъект намеренно способен подготовить отрицательное число для упражнения. Поэтому source authorization и consumer validation остаются двумя разными проверками.

В будущем журнале сохраняйте полные source и reader addresses вместе с исходником и параметрами. Два reader с разными source могут вернуть одинаковое число, не являясь одним наблюдением. Сравнение только отображаемых «300» не установит, какой экземпляр данных использовался.

Для самостоятельного уточнения: types.html, selecting-data-feeds, using-data-feeds.

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

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