Арифметика, оракулы и чеклист аудита
Финальный рубеж: разбираем остаточные риски арифметики и внешних данных и собираем чеклист, по которому проверяют контракт перед мейннетом.
В 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, контроль доступа, аудированные библиотеки) и тщательное тестирование важнее любой «хитрой» оптимизации. Теперь у вас есть и модель мышления, и чеклист, чтобы писать безопасные контракты.