Арифметика, оракулы и чеклист аудита

Финальный рубеж: разбираем остаточные риски арифметики и внешних данных и собираем чеклист, по которому проверяют контракт перед мейннетом.
В 2025 году на смарт-контрактах потеряли свыше $900 млн в сотне инцидентов. Почти все они — нарушение пары простых правил, которые мы сейчас соберём в список.

Мы уже закрыли переполнение (0.8.x откатывает его сам, кроме unchecked), reentrancy (CEI + guard) и контроль доступа (msg.sender + роли). Остаются ещё два частых класса проблем: ошибки округления/точности и зависимость от внешних данных (оракулов).

Округление и оракулы

Так как чисел с плавающей точкой нет, деление всегда округляет вниз. Если делить до умножения, легко получить ноль или потерять часть суммы — всегда умножайте перед делением. Оракулы — это источники внешних данных (например, цена ETH/USD). Если брать цену из легко манипулируемого источника (спот-цена одного пула), атакующий через флеш-займ сдвинет её и обманет ваш контракт. Поэтому используют устойчивые оракулы (например, TWAP или агрегаторы вроде Chainlink).

   МАНИПУЛЯЦИЯ ОРАКУЛА (упрощённо)
   ==============================
   1. flash loan огромной суммы
   2. сдвигает цену в спот-пуле  -> oracle.price() врёт
   3. контракт-жертва берёт врущую цену для расчёта
   4. атакующий выгодно «обменивается», возвращает заём
   ЗАЩИТА: усреднённая по времени цена (TWAP) / надёжный агрегатор
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract Pricing {
    // ПЛОХО: деление до умножения теряет точность
    function badShare(uint256 amount, uint256 part, uint256 total)
        external pure returns (uint256)
    {
        return (amount / total) * part; // часто = 0
    }

    // ХОРОШО: умножаем до деления
    function goodShare(uint256 amount, uint256 part, uint256 total)
        external pure returns (uint256)
    {
        return (amount * part) / total;
    }
}

Как работает под капотом (EVM/газ)

Целочисленное деление в EVM отбрасывает остаток. (amount / total) при amount < total даёт 0, и всё произведение обнуляется. Перестановка (amount * part) / total сохраняет точность, но требует следить за переполнением промежуточного произведения (в 0.8.x оно тоже под защитой и откатится при переполнении). Оракул — это просто внешний call к другому контракту; его ответу нельзя слепо доверять, как и любому внешнему вводу.

# Та же логика на Python: порядок операций решает точность
def bad_share(amount, part, total):
    return (amount // total) * part      # деление раньше -> теряем всё

def good_share(amount, part, total):
    return (amount * part) // total       # умножение раньше -> точно

print("bad: ", bad_share(100, 3, 7))     # (100//7)*3 = 14*3 = 42? нет: 14*3=42
print("bad2:", bad_share(5, 3, 7))       # (5//7)*3 = 0*3 = 0  -- потеряли всё
print("good:", good_share(5, 3, 7))      # (5*3)//7 = 15//7 = 2  -- корректно

«Та же логика на Python ▶». При маленьком amount «плохой» порядок обнуляет результат, а правильный (умножить до деления) сохраняет значение.

Частые ошибки и уязвимости

  • Деление до умножения — потеря точности и обнуление сумм.
  • Слепое доверие спот-цене из одного пула — манипуляция через флеш-займ.
  • «Оптимизация» через unchecked там, где переполнение всё-таки возможно.

Best practices: чеклист перед деплоем

  • Все внешние вызовы — после обновления состояния (CEI); критичные функции под nonReentrant.
  • Авторизация только по msg.sender; роли по принципу наименьших привилегий; критичное под мультисиг/таймлок.
  • Умножение до деления; unchecked только с обоснованием; фиксированная версия pragma.
  • Внешние данные — из устойчивых оракулов (TWAP/агрегатор), валидация любого ввода и проверка address(0).
  • Переиспользуйте аудированные библиотеки (OpenZeppelin); покрытие тестами и фаззингом (Foundry); статический анализ (Slither); независимый аудит перед мейннетом.

Итоги курса

Вы прошли путь от устройства EVM и первого контракта до токенов и безопасности. Главный вывод: в Solidity цена ошибки максимальна, поэтому минимализм, проверенные паттерны (CEI, контроль доступа, аудированные библиотеки) и тщательное тестирование важнее любой «хитрой» оптимизации. Теперь у вас есть и модель мышления, и чеклист, чтобы писать безопасные контракты.

Проверьте себя
1. Почему в Solidity нужно умножать до деления?
AТак быстрее компилируется
BЦелочисленное деление отбрасывает остаток, и деление первым может обнулить результат
CУмножение бесплатно
DИначе будет переполнение всегда
2. Чем опасно использование спот-цены из одного пула как оракула?
AОна слишком точная
BЕё можно сдвинуть флеш-займом и обмануть контракт-жертву
CОна не обновляется
DОна требует tx.origin