Security Ru

Интеграционный ад ERC-20: USDT, Fee-on-transfer и другие

В разработке смарт-контрактов бытует заблуждение: мы привыкли считать 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. Остальные сгорают или уходят в казну проекта.

Сценарий катастрофы: Представьте стейкинг-контракт.

  1. Пользователь вызывает stake(100).
  2. Контракт делает transferFrom на 100 токенов.
  3. В маппинг баланса пользователя записывается: balances[user] = 100.
  4. Но фактически на контракт пришло только 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, а не amount
3. Инфляционная атака на Vault (ERC-4626) Это классическая уязвимость пулов ликвидности и хранилищ, основанная на ошибках округления.

Механика: В большинстве хранилищ пользователь получает «доли» (shares) в обмен на активы. Формула обычно такая: Shares = (Assets * TotalShares) / TotalAssets

Атакующий (первый вкладчик) делает следующее:

  1. Депозитит 1 wei актива (получает 1 share).
  2. Напрямую (через transfer, минуя логику депозита) отправляет на контракт огромную сумму, например, 1000 ETH.
  3. Теперь в пуле: 1000 ETH + 1 wei, но всего 1 share. Цена одной доли становится космической.

Удар по жертве: Приходит обычный пользователь и вносит 20 ETH. Формула делит его вклад на раздутый пул. Из-за целочисленного деления в Solidity результат округляется вниз до 0. Пользователь отдает 20 ETH, получает 0 долей. Атакующий (владелец единственной доли) забирает всё.

Решение: Проблема решается двумя способами:

  1. Dead Shares: При первом минте отправлять небольшую часть долей (например, 1000 wei) на адрес 0xdead, навсегда блокируя их. Это делает атаку экономически невыгодной.
  2. Internal Offset: Использовать виртуальные офсеты в формуле подсчета активов (как это реализовано в последних версиях OpenZeppelin ERC4626), чтобы знаменатель никогда не был слишком мал.

Интеграция сторонних токенов — это всегда работа с недоверенным кодом. Вы не можете контролировать, как написан чужой токен, но вы обязаны защитить свою архитектуру от его странностей. Использование SafeERC20, проверка реальных балансов и защита от округления — это базовый минимум любого DeFi-протокола.