Целочисленная математика и округление в смарт-контрактах
Почему в Solidity нет дробей, как работают базисные пункты, порядок умножения и деления, проверка переполнения, mulDiv и расчёт долей вычитанием.
В этой статье
В обычной программе 0.1 + 0.2 ломает голову, а проценты считаются «как получится». В смарт-контракте за каждую единицу точности отвечают реальные деньги, поэтому все вычисления ведутся целыми числами, а округление - сознательное решение. Эта статья показывает, как не потерять ни копейки. Для контекста о числах в Ethereum - аккаунты и состояние.
Нет дробей: всё в целых
В EVM нет типов float или double. Деньги хранятся в минимальных единицах токена. Если у токена decimals = 6 (как USDC), то 1,5 USDC - это 1500000. Для ETH 1 ETH = 10^18 wei (о токенах).
Проценты задают базисными пунктами (bps): 1 bps = 0,01%, 10 000 bps = 100%. Ставка 1% - это 100 bps, 2,5% - 250 bps. Так видно, как в контракте эскроу:
uint16 public constant BPS = 10_000;
uint16 public constant PLATFORM_FEE_BPS = 100; // 1%Деление округляет вниз
Целочисленное деление отбрасывает остаток: 7 / 2 == 3, 1 / 3 == 0. Расчёт комиссии:
platformFee = (amount * PLATFORM_FEE_BPS) / BPS;Для суммы 99 единиц: 99 * 100 / 10000 = 0 (комиссия 0,99 округляется до 0). Это не ошибка, а следствие дискретности. Для токена с 6 знаками потери в минимальной единице несущественны, но их нужно осознавать.
Умножай сначала, дели потом
Порядок операций критичен. Возьмём сумму 99 999 и ставку 150 bps (1,5%):
// плохо: сначала деление теряет точность
uint256 bad = (amount / BPS) * fee; // (99999 / 10000) = 9; 9 * 150 = 1350
// хорошо: умножение сохраняет точность
uint256 good = (amount * fee) / BPS; // 99999 * 150 = 14999850; / 10000 = 1499Разница - 149 единиц из 1 499, то есть около 10% комиссии потеряно из-за преждевременного деления. Правило: сначала умножение, потом деление.
Переполнение
Начиная с Solidity 0.8 арифметика проверяется: если результат не помещается в тип, транзакция откатывается с ошибкой Panic(0x11). Это защищает от класса уязвимостей, когда число «переходило через ноль» (в версиях до 0.8 так и было). Блок unchecked { ... } отключает проверки ради экономии газа и подходит, только если вы доказали, что переполнение невозможно.
Но есть исключение: явное приведение типов не проверяется.
uint256 big = 2**100;
uint96 small = uint96(big); // тихо обрежется, без ошибкиВ контракте эскроу сумма хранится в uint96 вместе с адресом в одном слове хранилища. Поэтому перед записью сумма явно проверяется, и при превышении вызов отклоняется с ошибкой AmountTooLarge. Без этой проверки продавец мог бы создать «оферту» с обрезанной суммой.
Деление на ноль
Деление на ноль вызывает Panic(0x12). Надо следить за знаменателями, особенно если они берутся из внешних данных (например, из баланса пула).
Масштабируемое умножение: mulDiv
Выражение a * b / c может переполниться на промежуточном шаге a * b, даже если итог мал. Библиотечная функция mulDiv(a, b, c) считает с 512-битным промежуточным значением и избегает этой проблемы. Её используют в DeFi-протоколах для точных расчётов.
Направление округления
Если нужно разделить сумму, деление на части теряет остаток. Куда он пойдёт, решает разработчик, и это вопрос безопасности и справедливости. Общее правило: округляйте в пользу протокола, чтобы нельзя было «выжать» пыль многократными операциями.
В эскроу выбран иной, более надёжный подход - вычитание:
platformFee = (amount * PLATFORM_FEE_BPS) / BPS;
uint256 net = amount - platformFee;
arbiterFee = (net * arbiterFeeBps) / BPS;
uint256 pot = net - arbiterFee;
buyerAmount = (pot * buyerShareBps) / BPS;
// Остаток вычисляется вычитанием: округление вниз не создаст нехватки.
sellerAmount = pot - buyerAmount;Только комиссии и доля покупателя считаются делением. Доля продавца - остаток. Поэтому гарантируется равенство:
platformFee + arbiterFee + buyerAmount + sellerAmount == amountНи одна единица токена не теряется и не создаётся. Если бы обе доли считались по формуле pot * share / BPS, они могли бы в сумме дать на единицу меньше pot, и эта «пыль» застряла бы в контракте. Расчёты на конкретных числах: комиссии на примерах.
Проверка инвариантов фаззингом
Как убедиться, что суммы всегда сходятся? Написать тест-фаззер: подставить тысячи случайных значений и проверить инвариант «части равны целому» (как тестируют контракты, руководство Foundry). Такие тесты находят ошибки в граничных случаях: сумма 1, максимально возможная сумма, доли 0 и 10 000.
Чек-лист для разработчика
- Деньги - только
uint256в минимальных единицах. - Проценты - в bps, умножение до деления.
- Явные приведения типов - только после проверки диапазона (или
SafeCast). - Для сложных формул -
mulDiv. - Остаток - вычитанием.
- Инвариант «сумма частей = целое» - фаззингом.
- Помните про разные
decimalsу токенов (особые токены).
Что почитать дальше
Узнайте, как данные упаковываются в слоты и почему uint96 оказался рядом с адресом: раскладка хранилища. Или как тратить меньше газа: оптимизация газа.
Читайте также
- 2 · Практика
Комиссии в Coin-Trust на примерах: сколько и кому вы заплатите
Разбор всех комиссий сервиса: 1% платформы, ставка гаранта (базовая и в споре) и газ сети. Таблицы с расчётом для каждого исхода сделки на примере 1000 USDC.
3 мин чтения - 3 · Под капотом
Раскладка хранилища и упаковка переменных в Solidity
Как контракт хранит данные в слотах по 32 байта, как упаковываются переменные, где лежат mapping и массивы и как устроена структура сделки Coin-Trust.
4 мин чтения
Упоминается в статьях
- Комиссии в Coin-Trust на примерах: сколько и кому вы заплатите
- Аккаунты и состояние Ethereum: EOA, контракты и мировое состояние
- Обзор языка Solidity: типы, функции, ошибки и события на примерах
- ERC-20 с нуля: пишем токен на Solidity и разбираем каждую строку
- Пишем эскроу на Solidity: конечный автомат и разбор реального контракта
- Раскладка хранилища и упаковка переменных в Solidity
- Каталог уязвимостей смарт-контрактов: 12 классических ошибок и защита от них
- Оракулы и проблема оракулов: как блокчейн узнаёт о внешнем мире
- Оптимизация газа в Solidity: 12 приёмов и когда они уместны
- Foundry для начинающих: компиляция, тесты, фаззинг и локальная сеть
Готовы попробовать?
Создайте первую сделку за пару минут. Деньги блокируются в смарт-контракте и выплачиваются по правилам, которые видны заранее.

