Числовые типы и арифметика без переполнения

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

Базовые числовые типы — uint256 (беззнаковое 256-битное) и int256 (знаковое). Можно брать меньшие размеры кратно 8 бит: uint8, uint16, ..., uint256. По умолчанию uint = uint256. Дробных чисел нет вообще: float и double в Solidity отсутствуют, потому что детерминизм важнее удобства.

Диапазоны и переполнение

uint8 хранит 0..255. Что будет, если к 255 прибавить 1? До Solidity 0.8 результат «оборачивался» в 0 — это и есть integer overflow, причина знаменитых взломов. С 0.8.x компилятор по умолчанию вставляет проверку: при переполнении транзакция откатывается с ошибкой Panic.

   uint8: диапазон 0 ............ 255
                                   |
                          255 + 1  v
              ДО 0.8:  -----------> 0   (тихая катастрофа)
              0.8.x:   -----------> REVERT (Panic 0x11)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract Math {
    function add(uint8 a, uint8 b) public pure returns (uint8) {
        return a + b; // при a+b > 255 транзакция откатится
    }

    // если переполнение нужно осознанно (например хэши) — unchecked
    function wrapAdd(uint8 a, uint8 b) public pure returns (uint8) {
        unchecked { return a + b; } // вернётся «обёрнутое» значение
    }
}

Как работает под капотом (EVM/газ)

Встроенная проверка — это не магия, а дополнительные опкоды, которые компилятор добавляет после каждой арифметической операции: сравнить результат и при переполнении вызвать revert. Это стоит немного газа. Блок unchecked убирает эти проверки — его используют, когда вы математически уверены, что переполнения не будет (например, счётчик цикла), чтобы сэкономить газ. Деление на ноль тоже даёт Panic-revert.

# Та же логика на Python: проверка переполнения uint8
MAX_U8 = 255

def checked_add(a, b):
    r = a + b
    if r > MAX_U8:
        raise OverflowError("REVERT: Panic 0x11 (overflow)")
    return r

def unchecked_add(a, b):
    return (a + b) % (MAX_U8 + 1)  # обёртка по модулю, как старая EVM

print("checked 200+50 =", checked_add(200, 50))
try:
    checked_add(200, 100)
except OverflowError as e:
    print(e)
print("unchecked 200+100 =", unchecked_add(200, 100))  # 44

«Та же логика на Python ▶». Питон-версия наглядно показывает разницу: checked откатывается, unchecked оборачивается по модулю — ровно как ведёт себя EVM.

Частые ошибки

  • Применять unchecked «для экономии газа», не убедившись, что переполнение невозможно — это возврат к багам 2017 года.
  • Ожидать дробные числа: 1/3 в Solidity даёт 0, потому что это целочисленное деление.
  • Использовать мелкие типы (uint8) для счётчиков, которые могут вырасти — легко упереться в потолок и поймать revert.

Best practices

  • По умолчанию используйте uint256: он самый дешёвый по газу (нативный размер EVM) и не переполнится на реальных значениях.
  • Для денег с дробями работайте в наименьших единицах (как копейки): у токенов это базовые единицы с учётом decimals.
  • Применяйте unchecked только в проверенных местах и пишите комментарий, почему переполнение невозможно.

Контекст: почему это меняло историю Ethereum

Атаки на переполнение в 2017–2018 годах были массовыми: достаточно вспомнить серию «batchOverflow» в токенах, где функция перевода нескольким адресам перемножала количество получателей на сумму, и произведение тихо переполнялось, позволяя начеканить себе астрономический баланс из ничего. Тогда индустрия спасалась библиотекой SafeMath, которая вручную проверяла каждую операцию и откатывала транзакцию при переполнении. С приходом Solidity 0.8 эти проверки переехали прямо в компилятор, и SafeMath для базовой арифметики стал не нужен. Это хороший пример того, как язык эволюционирует, забирая частые ошибки разработчиков на себя — но понимать механику обязательно, иначе соблазн unchecked вернёт вас в 2017 год.

Итоги

Числа в Solidity — целые, фиксированной разрядности, дробей нет. С версии 0.8 переполнение откатывает транзакцию, а блок unchecked возвращает старое «оборачивающееся» поведение для оптимизаций. Дальше — адреса, булевы и строки.

Проверьте себя
1. Что произойдёт при выполнении uint8 x = 255; x + 1; в Solidity 0.8.x без unchecked?
AВернётся 0
BТранзакция откатится с ошибкой Panic (overflow)
CВернётся 256
DКомпилятор не соберёт код
2. Сколько будет 1/3 в Solidity?
A0.333...
B0
CОшибка компиляции
D1