китаев.tech

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:

solidity
// 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:

typescript
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());

Запуск:

bash
npx hardhat run scripts/integer-overflow.ts

CheckedCounter откатит транзакцию. UncheckedCounter покажет 0, потому что блок unchecked отключает автоматическую проверку переполнения.


Как это работало до Solidity 0.8

До Solidity 0.8 арифметика по умолчанию была небезопасной:

solidity
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 только когда инвариант очевиден из кода:

solidity
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 снова становится реальной уязвимостью.


Что дальше

Материалы китаev.tech публикуются в образовательных целях и не являются инвестиционной рекомендацией. Примеры кода и описания протоколов — для обучения; использование в продакшне на ваш собственный риск. Дисклеймер и политика конфиденциальности.