Переполнение целых: SafeMath и Solidity 0.8
Почему «число + 1» когда-то могло обнулить баланс — и почему теперь нет.
Переполнение (overflow/underflow) — выход результата арифметики за пределы диапазона типа: при превышении максимума значение «заворачивается» к минимуму и наоборот.
Откуда берётся проблема
Целые в EVM имеют фиксированный размер, например uint256 — от 0 до 2²⁵⁶−1. До Solidity 0.8 арифметика была «обёрточной»: если из нуля вычесть единицу (0 - 1), результат не уходил в минус, а становился максимальным числом. В контексте денег это катастрофа: баланс 0, из которого «списали» больше, превращался в гигантскую сумму. Атакующий, подведя баланс под underflow, мог «нарисовать» себе колоссальные средства.
# Иллюстрация обёрточной арифметики на 8 битах (0..255)
MAX = 255
def wrap(x):
return x % (MAX + 1)
print("255 + 1 ->", wrap(255 + 1)) # переполнение вверх
print("0 - 1 ->", wrap(0 - 1)) # переполнение вниз (underflow)Вывод:
255 + 1 -> 0 0 - 1 -> 255
На 256 битах эффект тот же, только числа огромные: «0 − 1» даёт максимальный uint256 — астрономический фейковый баланс.
Историческое решение: SafeMath
До 0.8 разработчики оборачивали все операции в библиотеку SafeMath, которая после каждого сложения/вычитания проверяла, не произошло ли переполнение, и откатывала транзакцию, если да. Это работало, но засоряло код (a.add(b) вместо a + b) и иногда забывалось — а забытая операция оставалась уязвимой.
// До 0.8: ручная защита через SafeMath
using SafeMath for uint256;
balances[to] = balances[to].add(amount); // .add откатит при overflowКак работает под капотом: Solidity 0.8+
Начиная с версии 0.8, компилятор Solidity встроил проверки переполнения по умолчанию: обычные +, -, * сами откатывают транзакцию при выходе за диапазон. SafeMath больше не нужен для базовой защиты. Там, где обёртка действительно желательна (намеренная цикличность счётчиков, оптимизация газа в безопасном месте), её включают явным блоком unchecked { ... } — это сигнал «я сознательно отключил проверку здесь».
// Solidity 0.8+: проверки встроены
balances[to] += amount; // авто-revert при overflow
unchecked {
counter += 1; // намеренно без проверки (осознанно!)
}Частые ошибки
- Бездумный
unchecked. Внутри него возвращается старое опасное поведение; используйте только там, где переполнение математически невозможно. - Старый pragma <0.8 без SafeMath. Сочетание старой версии и «голой» арифметики — уязвимость.
- Приведение типов с обрезанием.
uint256→uint64теряет старшие биты — отдельный источник ошибок.
Итоги
- Целые фиксированного размера «заворачиваются» при переполнении — для денег это критично.
- До Solidity 0.8 защищались библиотекой SafeMath (легко забыть).
- С 0.8 проверки переполнения встроены в компилятор по умолчанию.
uncheckedотключает их — применяйте только осознанно и обоснованно.