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

Безопасность Solidity: округление и базовые единицы

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

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

Полное вычисление отношения

// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
contract QuotaMath {
    error InvalidRatio();
    function grantFloor(uint256 budget, uint256 unitsPerBatch, uint256 budgetPerBatch)
        external pure returns (uint256)
    {
        if (unitsPerBatch == 0 || budgetPerBatch == 0) revert InvalidRatio();
        return Math.mulDiv(budget, unitsPerBatch, budgetPerBatch, Math.Rounding.Floor);
    }
    function costCeil(uint256 units, uint256 unitsPerBatch, uint256 budgetPerBatch)
        external pure returns (uint256)
    {
        if (unitsPerBatch == 0 || budgetPerBatch == 0) revert InvalidRatio();
        return Math.mulDiv(units, budgetPerBatch, unitsPerBatch, Math.Rounding.Ceil);
    }
}

Это полный исходник с закреплённым Math OpenZeppelin 5.4.0. Для unsigned положительных значений Floor и Ceil имеют ожидаемую направленность вниз и вверх. Library mulDiv обрабатывает промежуточное произведение подходящим полноточным способом, но итог всё равно должен помещаться в допустимый диапазон.

Проверка ratios исключает нулевой знаменатель и бессмысленное нулевое отношение по нашей модели. Budget или units могут быть нулевыми: тогда результат нулевой. Валидация нулевого количества конкретного пользовательского действия находится выше, если сервис хочет его запрещать. Pure арифметическая функция не обязана устанавливать весь предметный интерфейс.

Разбор бюджета семь

Зададим unitsPerBatch=3 и budgetPerBatch=5. Budget семь даёт математические 21/5=4,2 единицы. GrantFloor возвращает 4. Если сервис обещает только целые единицы, дополнительная дробь остаётся невыданной по выбранному договору.

Для четырёх единиц CostCeil считает 20/3 и требует 7 целых бюджета. Если здесь округлить вниз до 6, пользователь получил бы четыре единицы при меньшем бюджете, чем нужно по отношению. Направление связано с конкретной стороной перехода, не выбирается одним глобальным названием rounding.

budget=7, ratio=3/5 → grantFloor=4
units=4, ratio=3/5 → costCeil=7
units=1, ratio=3/5 → costCeil=2
budget=1, ratio=3/5 → grantFloor=0

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

Порядок операций

Выражение budget/budgetPerBatch×unitsPerBatch для семи даст 1×3=3, а не 4. Преждевременное целое деление потеряло часть точности до умножения. Поэтому перестановка вычислений ради избежания overflow может изменить предметный результат, даже если каждая операция отдельно допустима.

Простое budget×unitsPerBatch/budgetPerBatch сохраняет нужное отношение для небольших чисел, но промежуточное произведение может переполниться раньше деления. Math.mulDiv решает соответствующую техническую часть. Он не исправит перепутанные numerator и denominator или чужой масштаб единиц.

Разделение на несколько действий

Округление зависит от гранулярности операции. Три единицы за один costCeil требуют 5 бюджета. Три независимых вызова для одной единицы потребуют 2+2+2=6. Такой результат не обязательно ошибка: он следует из округления каждого отдельного запроса. Но продукт должен понимать эту разницу.

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

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

Масштаб токена и отображение

Если budget выражен в базовых ERC-20 единицах, единицы ratio также должны быть согласованы. Токен с шестью decimals и строка пользователя «7» требуют преобразования в семь миллионов базовых единиц, если это именно семь отображаемых токенов. Число семь без такого договора означает другое количество.

Нельзя использовать JavaScript floating point как единственный источник больших целых calldata. Десятичная строка разбирается по установленному масштабу, сохраняется подходящим точным представлением и проверяется до ABI-кодирования. Код Solidity не узнаёт, что клиент потерял цифры при подготовке входа.

Наш helper не читает decimals внешнего токена и не делает автоматическую конверсию масштаба. Он работает с согласованными целыми величинами. Добавление metadata вызова создало бы новую внешнюю зависимость и новую политику доверия к returned scale, которой сейчас нет.

Остаток и право на него

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

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

Границы будущей проверки

Для задания рассмотрите ноль, точную кратность, остаток один меньше знаменателя и очень большое промежуточное произведение. Укажите ожидаемое направление Floor и Ceil, необходимость ненулевого denominator и допустимость итогового диапазона. Не называйте работу библиотеки проверенной исполнением в этой задаче.

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

Результаты двух функций сохраняйте с явными названиями единиц. Число четыре от grantFloor является количеством квоты, а число семь от costCeil количеством бюджета. Простое поле value без обозначения может быть повторно использовано в обратном вызове как неверный аргумент. Такая ошибка представления возможна даже при безупречной арифметической библиотеке.

Для самостоятельного уточнения: Math.sol (5.4.0), types.html.

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

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