Ошибки логики: округление и потеря точности

Один лишний знак округления «в пользу пользователя» — и протокол медленно истекает.

Потеря точности — искажение результата из-за того, что в EVM нет дробных чисел: всякое деление округляется к нулю, отбрасывая остаток.

Откуда берётся проблема

В Solidity нет float: все вычисления целочисленные, а деление отбрасывает дробную часть. Значит, порядок операций критичен: (a * b) / c и (a / c) * b могут дать разный результат, и второй вариант теряет точность раньше. Финансовая математика (проценты, доли пула, награды) чувствительна к этому: маленькие потери, помноженные на тысячи операций, складываются в реальные деньги.

a, b, c = 7, 100, 3

# делим в конце — точнее
print("(a*b)//c =", (a * b) // c)
# делим раньше — теряем точность
print("(a//c)*b =", (a // c) * b)

Вывод:

(a*b)//c = 233
(a//c)*b = 200

Разница — 33 единицы из ничего, лишь из-за порядка деления. В деньгах такая «утечка» накапливается.

Направление округления — это вопрос безопасности

Главное правило: округляйте всегда в пользу протокола, а не пользователя. Если при выдаче активов округлять вверх, а при приёме — вниз, пользователь систематически получает чуть больше и платит чуть меньше; за много операций это выкачивает контракт. Поэтому при выдаче округляют вниз, при взимании — вверх. Это не педантизм: целые «экономические» атаки строятся на накоплении округления.

// Принцип: округление НЕ в пользу пользователя
// выдаём пользователю -> округляем ВНИЗ
uint payout = (amount * rate) / SCALE;          // floor
// берём с пользователя -> округляем ВВЕРХ
uint owed = (amount * rate + SCALE - 1) / SCALE; // ceil

Как работает под капотом: масштабирование

Поскольку дробей нет, проценты и курсы хранят в «фикс-пойнт» виде — умноженными на масштаб (например, 1e18). Сначала умножают на большой множитель, потом делят: так точность сохраняется до последнего шага. Особое внимание — первому вкладчику пула и пустым пулам: деление на крошечные величины даёт резкие скачки и места для атак инфляции долей.

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

  • Делить раньше, чем умножать. Теряете точность без нужды.
  • Округлять в пользу пользователя. Систематическая утечка средств.
  • Игнорировать крайние случаи. Пустой пул, первый депозит, ноль — частые источники багов.

Итоги

  • В EVM нет дробей: деление отбрасывает остаток, порядок операций важен.
  • Умножайте до деления, чтобы сохранить точность.
  • Округляйте в пользу протокола: выдача — вниз, взимание — вверх.
  • Особо проверяйте крайние случаи (пустой пул, первый вклад, ноль).
Проверьте себя
1. Почему важен порядок операций (a*b)/c vs (a/c)*b в Solidity?
AИз-за газа
BДеление целочисленное и отбрасывает остаток, поэтому раннее деление теряет точность
CИз-за переполнения всегда
DЭто не влияет на результат
2. В чью пользу нужно округлять для безопасности?
AВ пользу пользователя
BВ пользу протокола (выдача вниз, взимание вверх)
CВсегда вверх
DЭто не важно
3. Как сохраняют точность процентов без float?
AИспользуют тип float
BХранят значения в фикс-пойнт (умноженными на масштаб, напр. 1e18), деля в последнюю очередь
CОкругляют случайно
DЗапрещают деление