Integer overflow — до и после Solidity 0.8
Как работали переполнения чисел в старых версиях Solidity, что изменилось в Solidity 0.8 и когда нужен unchecked.
Что такое integer overflow
Integer overflow — это ситуация, когда число выходит за пределы своего типа и оборачивается к другому значению.
Для uint8 максимум равен 255. В старом Solidity выражение 255 + 1 давало 0. Начиная с Solidity 0.8 такая операция автоматически вызывает revert.
Проверяем поведение в Solidity 0.8
Создайте файл contracts/IntegerOverflowDemo.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract CheckedCounter {
uint8 public value = 255;
function increment() external {
// В Solidity 0.8 это завершится revert:
// uint8 не может хранить число 256.
value += 1;
}
}
contract UncheckedCounter {
uint8 public value = 255;
function increment() external {
unchecked {
// unchecked возвращает старое поведение: 255 + 1 станет 0.
value += 1;
}
}
}
contract SafeTokenVault {
mapping(address => uint256) public balances;
error ZeroAmount();
error NotEnoughBalance();
function deposit(uint256 amount) external {
if (amount == 0) {
revert ZeroAmount();
}
balances[msg.sender] += amount;
}
function withdraw(uint256 amount) external {
if (amount == 0) {
revert ZeroAmount();
}
if (balances[msg.sender] < amount) {
revert NotEnoughBalance();
}
balances[msg.sender] -= amount;
}
}Создайте файл scripts/integer-overflow.ts:
import { network } from "hardhat";
const { ethers } = await network.create();
const checked = await ethers.deployContract("CheckedCounter");
await checked.waitForDeployment();
const uncheckedCounter = await ethers.deployContract("UncheckedCounter");
await uncheckedCounter.waitForDeployment();
console.log("CheckedCounter до:", await checked.value());
try {
await checked.increment();
} catch {
console.log("CheckedCounter: транзакция откатилась из-за overflow");
}
console.log("CheckedCounter после:", await checked.value());
console.log("UncheckedCounter до:", await uncheckedCounter.value());
await uncheckedCounter.increment();
console.log("UncheckedCounter после:", await uncheckedCounter.value());Запуск:
npx hardhat run scripts/integer-overflow.tsCheckedCounter откатит транзакцию. UncheckedCounter покажет 0, потому что блок unchecked отключает автоматическую проверку переполнения.
Как это работало до Solidity 0.8
До Solidity 0.8 арифметика по умолчанию была небезопасной:
uint8 value = 255;
value += 1; // 0 в Solidity 0.7 и нижеПоэтому старые контракты использовали SafeMath: библиотеку, которая вручную проверяла сложение, вычитание и умножение.
В Solidity 0.8 SafeMath обычно не нужен. Компилятор сам вставляет проверки и делает revert при overflow или underflow.
Как это работает
uint8 — беззнаковое число от 0 до 255. Оно удобно для демонстрации, потому что границу легко достигнуть.
value += 1 в CheckedCounter — обычная арифметика Solidity 0.8. Если результат не помещается в тип, транзакция откатывается.
unchecked { ... } — блок без проверок overflow и underflow. Он нужен редко: например, в gas-оптимизациях цикла, где разработчик сам доказал границы.
balances[msg.sender] < amount — явная проверка перед вычитанием. Solidity 0.8 защитит от underflow, но такая ошибка должна возвращать понятную причину.
Когда использовать unchecked
Используйте unchecked только когда инвариант очевиден из кода:
for (uint256 i = 0; i < items.length;) {
// работа с items[i]
unchecked {
i++;
}
}Здесь i++ не может переполниться в реальном массиве: до 2^256 - 1 элементов транзакция никогда не дойдёт из-за gas limit.
Не используйте unchecked вокруг пользовательских сумм, балансов, долей, лимитов и цен. Эти значения приходят извне и напрямую влияют на деньги.
Частые ошибки
Думать, что SafeMath всё ещё обязателен → для Solidity 0.8 обычная арифметика уже проверяется компилятором.
Ставить unchecked ради экономии gas в бизнес-логике → экономия мала, а риск ошибки в расчётах высокий.
Писать код под старую версию компилятора → если контракт использует pragma solidity ^0.7.0, overflow снова становится реальной уязвимостью.
Что дальше
- Mapping в Solidity — как хранить балансы и другие значения по адресу
- Вызов write-функций контракта — как транзакции меняют state
- Reentrancy — атака и защита — другая классическая ошибка в функции вывода средств