Solidity: переполнение чисел и приведение типов
После входной валидации разберём числовое представление. В Solidity 0.8.30 обычная арифметика целых проверяет переполнение и недостаток диапазона. Но явное сужение к меньшему unsigned типу имеет собственные правила. Поэтому фраза «версия 0.8 всё проверяет» недостаточна для анализа числового кода.
Используем отдельный ArithmeticPreview без storage и средств. Его pure методы показывают различия checked операции, намеренного wrapping и сужения. Небезопасные варианты обозначены как наблюдение поведения, а не рекомендованные способы учитывать пользовательскую квоту.
Полный числовой пример
// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;
contract ArithmeticPreview {
error TooLarge();
function checkedAdd(uint8 a, uint8 b) external pure returns (uint8) {
return a + b;
}
function wrappingPreview(uint8 a, uint8 b) external pure returns (uint8 result) {
unchecked { result = a + b; }
}
function narrowingPreview(uint256 amount) external pure returns (uint8) {
return uint8(amount);
}
function boundedQuota(uint256 amount) external pure returns (uint8) {
if (amount > type(uint8).max) revert TooLarge();
return uint8(amount);
}
}
Это полный собственный исходник. Он не компилируется и не исполняется в текущей задаче. Все числовые результаты дальше ожидаемые по закреплённому правилу языка. Выбор uint8 сделан для наглядной маленькой границы, а не потому, что любой storage-предел приложения следует хранить именно в восьми битах.
Checked и unchecked
Uint8 представляет значения от 0 до 255. CheckedAdd(255,1) не может вернуть 256 в этом типе и ожидаемо отклоняется. WrappingPreview(255,1) выполняет намеренную модульную арифметику и получает 0. Unchecked меняет поведение операции, но не устанавливает правильность нулевого результата для предметной квоты.
Если программа использует unchecked для уменьшения available-amount, автор должен иметь доказанное условие amount<=available на достижимом пути. Комментарий «переполнение невозможно» не заменяет такое условие. В библиотечном исходнике оно может обеспечиваться предыдущей проверкой, которую нельзя удалить при копировании строки.
Unchecked также не означает, что любая ошибка внутри блока перестанет отклоняться. Например, деление на ноль имеет другое обязательное условие. Внешний revert и неправильный доступ к массиву тоже не становятся успешными только от размещения в этом блоке.
Явное сужение
NarrowingPreview(256) возвращает младшие биты в uint8, поэтому ожидаемое значение 0. Это не checked addition и не автоматический TooLarge. Явное приведение выразило выбранное автором преобразование, которое может потерять старшие биты при допустимом исходном uint256.
BoundedQuota сначала проверяет верхнюю границу, затем приводит значение. При 255 результат допустим, при 256 вызывается TooLarge. Таким образом, правильность предметного сужения обеспечена собственной проверкой до преобразования, а не верой в поведение любого cast.
Рассмотрим вход 257. В небезопасном preview получится 1, а пользователь мог ожидать квоту 257. Если позже такое значение используется как лимит, программа примет совершенно другой договор. Типовые тесты с маленькими числами не обнаружат проблему без граничного сценария.
Signed ответ
Внешний источник может возвращать int256. Для положительной цены или учебного коэффициента сначала проверяют answer>0, затем выполняют допустимое преобразование к unsigned. Прямое использование отрицательного числа в unsigned-смысле может получить очень большое представление вместо ожидаемого отказа.
Нельзя предполагать, что одна проверка answer!=0 устраняет отрицательное значение. Она допускает -1. Условие должно соответствовать предметному диапазону, включая максимальную допустимую величину. Поздние главы о reader добавят конкретную политику ответа и масштаба.
Также некоторые прямые преобразования между типами с разной шириной и знаком ограничены языком. Нужен допустимый последовательный путь и проверка смысла, а не набор cast до исчезновения ошибки компилятора. Принятый синтаксис сам по себе не подтверждает желаемого числа.
Промежуточный диапазон
Пусть итоговое рациональное выражение a×b/d математически помещается в uint256, но произведение a×b превышает диапазон. Обычная checked multiplication отклонится до деления. Перестановка деления раньше может уменьшить точность и изменить предметный результат, поэтому это не универсальное исправление.
Для подходящих рациональных вычислений библиотека Math предоставляет mulDiv с определённым поведением. Но она не выбирает за нас единицы, denominator и направление округления. Следующая глава рассмотрит именно эти условия на собственной учебной квоте.
Переполнение может означать также недоступность разрешённого перехода. Если глобальный счётчик достиг предела и только его дальнейшее увеличение позволяет работать, checked revert сохраняет целостность чисел, но не работоспособность услуги. Нужно иметь осмысленную верхнюю границу и политику жизненного цикла.
Размер типов и хранение
Меньший тип иногда помогает упаковке storage, но не даёт автоматической экономии в каждом месте ABI или arithmetic. Выбор должен учитывать реальный профиль и требования, а не предположение «восемь бит всегда быстрее». Здесь мы не измеряем gas и не запускаем сборку.
Если будущем upgrade расширяет или переставляет поле, возникает ещё и договор layout. Нельзя исправить диапазон на живой прокси простой заменой типа без анализа старых записей. Числовая семантика и место хранения — две разные части совместимости.
Самостоятельные границы
Составьте ожидаемые значения для 0,1,255,256,257 у boundedQuota и narrowingPreview. Затем рассмотрите checkedAdd(254,1), checkedAdd(255,1) и тот же wrapping. Укажите, где будет результат, где отклонение и почему.
После этого сформулируйте проверку для положительного signed answer и предел преобразования в выбранный тип. Не исполняйте эти примеры в рамках текущей задачи. Таблица ожидаемых исходов подготавливает конкретную последующую проверку, но не выдаётся за отчёт о выполненном тестировании.
Преобразование до сравнения
Проверка uint8(amount)<=255 после сужения не защищает первоначальный amount: результат уже находится в этом диапазоне по представлению. Нужно сравнить исходное широкое число до cast, как в boundedQuota. Иначе условие выглядит убедительно, но не исключает ни 256, ни 257.
Этот принцип применим к любому этапу нормализации, который теряет информацию. После усечения старших битов нельзя восстановить исходную величину одной проверкой итогового диапазона. Сохраните исходный вход и проверяйте нужное предметное условие до преобразования, затем используйте новое представление с известной предпосылкой.
Для самостоятельного уточнения: types.html, types.html, Math.sol (5.4.0).
Оглавление курса. Листинги, расчёты и наблюдения здесь разобраны как ожидаемое поведение; компиляция, учебные программы и проверки при написании не запускались.