В разработке смарт-контрактов бытует заблуждение: мы привыкли считать ERC-20 жестким стандартом. Мы импортируем IERC20.sol, пишем transfer и ожидаем, что любой токен будет вести себя как WETH или USDC.
В реальности ERC-20 — это не жесткий свод правил, а набор рекомендаций. Топовые токены с капитализацией в миллиарды долларов нарушают спецификации, имеют скрытые комиссии или обновляемую логику, которая может остановить работу вашего протокола. Я постоянно вижу эти ошибки в аудитах. Разберем три главных сценария, на которых теряют деньги.
1. Проблема USDT: Отсутствие Return Bool USDT — самый популярный стейблкоин в мире, и по иронии судьбы он не соответствует стандарту ERC-20.
Суть проблемы: По спецификации ERC-20 функции transfer и transferFrom должны возвращать логическое значение bool (true/false), сигнализирующее об успехе. Однако в контракте USDT (на Ethereum Mainnet) эти функции ничего не возвращают (void).
Что происходит: Вы используете стандартный интерфейс IERC20, который ожидает bool. Ваш контракт вызывает transfer у USDT. Стейблкоин выполняет перевод, но не возвращает ничего. Ваш контракт пытается декодировать ответ, видит пустоту там, где должен быть true, считает это ошибкой и делает revert.
Решение: Никогда не используйте стандартный интерфейс для внешних токенов. Всегда используйте библиотеку SafeERC20 от OpenZeppelin и методы safeTransfer / safeTransferFrom. Они умеют обрабатывать нестандартное поведение USDT.
2. Токены с комиссией (Fee-on-transfer) Существует класс токенов (часто это мем-коины или дефляционные токены вроде PAXG), которые взимают комиссию при каждом переводе. Вы отправляете 100 токенов, а получателю приходит 98. Остальные сгорают или уходят в казну проекта.
Сценарий катастрофы: Представьте стейкинг-контракт.
Протокол мгновенно становится неплатежеспособным. Последний пользователь, который попытается забрать свои средства, не сможет этого сделать, так как в контракте физически меньше токенов, чем записано в обязательствах.
Решение: Не верьте входным параметрам. Проверяйте баланс контракта до и после перевода.
В реальности ERC-20 — это не жесткий свод правил, а набор рекомендаций. Топовые токены с капитализацией в миллиарды долларов нарушают спецификации, имеют скрытые комиссии или обновляемую логику, которая может остановить работу вашего протокола. Я постоянно вижу эти ошибки в аудитах. Разберем три главных сценария, на которых теряют деньги.
1. Проблема USDT: Отсутствие Return Bool USDT — самый популярный стейблкоин в мире, и по иронии судьбы он не соответствует стандарту ERC-20.
Суть проблемы: По спецификации ERC-20 функции transfer и transferFrom должны возвращать логическое значение bool (true/false), сигнализирующее об успехе. Однако в контракте USDT (на Ethereum Mainnet) эти функции ничего не возвращают (void).
Что происходит: Вы используете стандартный интерфейс IERC20, который ожидает bool. Ваш контракт вызывает transfer у USDT. Стейблкоин выполняет перевод, но не возвращает ничего. Ваш контракт пытается декодировать ответ, видит пустоту там, где должен быть true, считает это ошибкой и делает revert.
Решение: Никогда не используйте стандартный интерфейс для внешних токенов. Всегда используйте библиотеку SafeERC20 от OpenZeppelin и методы safeTransfer / safeTransferFrom. Они умеют обрабатывать нестандартное поведение USDT.
2. Токены с комиссией (Fee-on-transfer) Существует класс токенов (часто это мем-коины или дефляционные токены вроде PAXG), которые взимают комиссию при каждом переводе. Вы отправляете 100 токенов, а получателю приходит 98. Остальные сгорают или уходят в казну проекта.
Сценарий катастрофы: Представьте стейкинг-контракт.
- Пользователь вызывает stake(100).
- Контракт делает transferFrom на 100 токенов.
- В маппинг баланса пользователя записывается: balances[user] = 100.
- Но фактически на контракт пришло только 98 токенов.
Протокол мгновенно становится неплатежеспособным. Последний пользователь, который попытается забрать свои средства, не сможет этого сделать, так как в контракте физически меньше токенов, чем записано в обязательствах.
Решение: Не верьте входным параметрам. Проверяйте баланс контракта до и после перевода.
Solidity
uint256 balanceBefore = token.balanceOf(address(this));
token.transferFrom(msg.sender, address(this), amount);
uint256 balanceAfter = token.balanceOf(address(this));
uint256 actualAmount = balanceAfter - balanceBefore;
// Записываем actualAmount, а не amount3. Инфляционная атака на Vault (ERC-4626) Это классическая уязвимость пулов ликвидности и хранилищ, основанная на ошибках округления.
Механика: В большинстве хранилищ пользователь получает «доли» (shares) в обмен на активы. Формула обычно такая: Shares = (Assets * TotalShares) / TotalAssets
Атакующий (первый вкладчик) делает следующее:
Удар по жертве: Приходит обычный пользователь и вносит 20 ETH. Формула делит его вклад на раздутый пул. Из-за целочисленного деления в Solidity результат округляется вниз до 0. Пользователь отдает 20 ETH, получает 0 долей. Атакующий (владелец единственной доли) забирает всё.
Решение: Проблема решается двумя способами:
Интеграция сторонних токенов — это всегда работа с недоверенным кодом. Вы не можете контролировать, как написан чужой токен, но вы обязаны защитить свою архитектуру от его странностей. Использование SafeERC20, проверка реальных балансов и защита от округления — это базовый минимум любого DeFi-протокола.
Механика: В большинстве хранилищ пользователь получает «доли» (shares) в обмен на активы. Формула обычно такая: Shares = (Assets * TotalShares) / TotalAssets
Атакующий (первый вкладчик) делает следующее:
- Депозитит 1 wei актива (получает 1 share).
- Напрямую (через transfer, минуя логику депозита) отправляет на контракт огромную сумму, например, 1000 ETH.
- Теперь в пуле: 1000 ETH + 1 wei, но всего 1 share. Цена одной доли становится космической.
Удар по жертве: Приходит обычный пользователь и вносит 20 ETH. Формула делит его вклад на раздутый пул. Из-за целочисленного деления в Solidity результат округляется вниз до 0. Пользователь отдает 20 ETH, получает 0 долей. Атакующий (владелец единственной доли) забирает всё.
Решение: Проблема решается двумя способами:
- Dead Shares: При первом минте отправлять небольшую часть долей (например, 1000 wei) на адрес 0xdead, навсегда блокируя их. Это делает атаку экономически невыгодной.
- Internal Offset: Использовать виртуальные офсеты в формуле подсчета активов (как это реализовано в последних версиях OpenZeppelin ERC4626), чтобы знаменатель никогда не был слишком мал.
Интеграция сторонних токенов — это всегда работа с недоверенным кодом. Вы не можете контролировать, как написан чужой токен, но вы обязаны защитить свою архитектуру от его странностей. Использование SafeERC20, проверка реальных балансов и защита от округления — это базовый минимум любого DeFi-протокола.