Нестандартные токены: USDT без return, fee-on-transfer, блэклисты и decimals
Какие сюрпризы скрывают токены ERC-20: USDT без возврата значения, комиссия при переводе, блокировка адресов, разные decimals - и как защищается эскроу.
В этой статье
Стандарт ERC-20 описывает, как токен должен себя вести. На практике многие популярные токены ведут себя иначе, и каждое отличие - потенциальная ошибка в контракте, который с ними работает. Эскроу, принимающий чужие токены, вынужден учитывать всех. Разберём главные «сюрпризы».
1. USDT не возвращает значение
По стандарту transfer и transferFrom возвращают bool: true при успехе. Но USDT в Ethereum был написан раньше стандарта и ничего не возвращает. Если контракт напишет:
bool ok = IERC20(token).transfer(to, amount); // для USDT: ошибка декодирования!вызов упадёт: Solidity ожидает 32 байта ответа, а приходит пустота. Контракт, не учитывающий этого, не сможет работать с самым популярным стейблкоином.
Как решают. Используют обёртку SafeERC20 из OpenZeppelin. Она вызывает функцию «вручную» и считает успехом и пустой ответ, и true. Для автоматических выплат, где нельзя откатывать транзакцию, в нашем контракте похожая логика прописана явно:
(bool ok, bytes memory ret) = token.call(abi.encodeCall(IERC20.transfer, (to, amount)));
if (!ok) return false;
if (ret.length == 0) return true; // так ведут себя USDT и подобные
return ret.length >= 32 && abi.decode(ret, (bool));2. Комиссия при переводе (fee-on-transfer)
Некоторые токены удерживают часть суммы при каждом переводе: вы отправили 100, получатель получил 98. Контракт, который записал в сделку «получено 100», ошибся - на самом деле на балансе 98, и при выплате последний получатель останется без денег.
Как решают. Проверяют фактическое поступление: замеряют баланс до и после и сравнивают.
uint256 balanceBefore = IERC20(token).balanceOf(address(this));
IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
uint256 received = IERC20(token).balanceOf(address(this)) - balanceBefore;
if (received != amount) revert AmountMismatch(amount, received);Токен с комиссией отвергается сразу, а не записывает в сделку неверную сумму. Кроме того, так отсекаются ребейзящие токены, чей баланс сам меняется со временем.
3. Блокировка адресов
У USDT и USDC эмитент может внести адрес в чёрный список - после этого любой перевод на него или с него откатывается. Для эскроу это серьёзно: если продавец в чёрном списке, выплата ему не пройдёт и при наивной реализации заблокирует закрытие сделки.
Как решают. Выплата через «попытку»: если не вышло, сумма записывается в кредиты (pull-платежи), а сделка закрывается. Остальные участники получают свои деньги, а проблемный получатель - запись о долге. Если адрес заблокирован навсегда, забрать деньги он не сможет, но это не повредит остальным.
4. Разные decimals
Токены хранят суммы как целые числа и «знают», сколько знаков после запятой подразумевается:
| Токен | Сеть | Decimals | 1 токен в «сырых» единицах |
|---|---|---|---|
| USDT, USDC | Ethereum, Polygon, Base | 6 | 1 000 000 |
| USDT, USDC | BNB Chain | 18 | 1 000 000 000 000 000 000 |
| DAI | везде | 18 | 1 000 000 000 000 000 000 |
Самая частая ошибка - захардкодить 6 или 18. Тогда в одной сети всё работает, а в другой пользователь вводит «100» и контракт принимает в триллион раз больше или меньше. Правило: decimals всегда берётся из данных конкретной сети и токена, никогда не из константы в коде. Кстати, поэтому интерфейс предупреждает: «18 знаков, не 6» в BNB Chain.
5. Токены с возвратом false вместо отката
Некоторые токены при ошибке не откатывают транзакцию, а возвращают false. Если контракт не проверяет результат, он решит, что перевод удался, хотя деньги не двигались. SafeERC20 проверяет возвращаемое значение и откатывает.
6. Токены с хуками (ERC-777 и подобные)
Токены стандарта ERC-777 вызывают код получателя и отправителя при переводе. Это открывает дверь для реентрантности там, где вы её не ждали: даже «безобидный» перевод токена может вызвать чужой код. Защита та же: порядок «проверки → эффекты → взаимодействия» и nonReentrant.
7. Обновляемые и приостанавливаемые токены
USDC - прокси-контракт: эмитент может обновить его логику, а у USDT есть механизм «deprecate», позволяющий перенаправить токен на новую реализацию. Оба умеют приостанавливать переводы целиком. Это не ошибка, а свойство централизованных стейблкоинов, но его нужно учитывать: «гарантия» контракта эскроу не сильнее, чем надёжность токена, которым он оперирует. Подробнее - в статье про стейблкоины.
8. Гонка разрешений и особенность USDT
Стандартная проблема approve: если сменить разрешение с 100 на 50, у получателя есть окно, чтобы успеть потратить прежние 100 и новые 50. У USDT в Ethereum введена защита другого рода: изменить ненулевое разрешение на ненулевое нельзя - сначала обнулить. Поэтому интерфейс при необходимости просит три подписи вместо двух. Подробнее - в статье про approve.
Белый список токенов
Все эти риски возникают потому, что контракт доверяет «чужому коду». Поэтому в нашем эскроу есть белый список разрешённых токенов (allowedToken). Владелец включает только проверенные: USDT, USDC, DAI и нативную монету. Произвольный токен с экзотическим поведением создать сделку не может. И интерфейс показывает список валют по данным контракта, а не по «справочнику»: если токен не разрешён, он скрыт.
Памятка разработчику
- Всегда оборачивайте вызовы токенов (
SafeERC20). - Проверяйте фактически полученную сумму, а не заявленную.
- Не хардкодьте
decimals. - Выплаты через «попытку» с кредитами, чтобы один получатель не блокировал остальных.
- Ограничьте набор токенов белым списком.
- Тестируйте на «злых» моках: токен без
bool, с комиссией, с блэклистом, с хуками - об этом в статье про тестирование.
Что почитать дальше
Полный обзор типичных ошибок контрактов - в каталоге уязвимостей. Затем: как контракты можно обновлять и чем это рискованно - прокси и обновляемость, и почему адрес эскроу одинаков в четырёх сетях - CREATE2.
Читайте также
- 3 · Под капотом
ERC-20 с нуля: пишем токен на Solidity и разбираем каждую строку
Минимальная реализация стандарта ERC-20 на Solidity: балансы, переводы, allowance, события, типичные ошибки и почему в продакшене нужны проверенные библиотеки.
4 мин чтения - 3 · Под капотом
Реентрантность, checks-effects-interactions и pull-платежи
Как работает атака повторного входа, взломавшая The DAO, и как от неё защищаться: порядок операций, ReentrancyGuard и отложенные платежи на примере эскроу.
5 мин чтения
Упоминается в статьях
- Что такое криптовалюта и откуда берутся монеты
- Что такое смарт-контракт и почему ему можно доверять деньги
- Токены и NFT: что это, чем отличаются и где применяются
- Стейблкоины: USDT, USDC, DAI - чем отличаются и в чём риски
- Жизненный цикл сделки: от оферты до выплаты шаг за шагом
- Как выбрать сеть для сделки: Ethereum, Base, Polygon или BNB Chain
- ERC-20 и approve: зачем «разрешение» и чем опасен безлимит
- Как проверить смарт-контракт перед использованием: пошаговая инструкция
- Обзор языка Solidity: типы, функции, ошибки и события на примерах
- ERC-20 с нуля: пишем токен на Solidity и разбираем каждую строку
- Целочисленная математика и округление в смарт-контрактах
- Реентрантность, checks-effects-interactions и pull-платежи
- Каталог уязвимостей смарт-контрактов: 12 классических ошибок и защита от них
- Foundry для начинающих: компиляция, тесты, фаззинг и локальная сеть
- Тестирование смарт-контрактов: unit, fuzz, инварианты и статический анализ
Готовы попробовать?
Создайте первую сделку за пару минут. Деньги блокируются в смарт-контракте и выплачиваются по правилам, которые видны заранее.

