К основному содержимому

Введите слово из вопроса или названия статьи.

3 · Под капотом

Нестандартные токены: USDT без return, fee-on-transfer, блэклисты и decimals

Какие сюрпризы скрывают токены ERC-20: USDT без возврата значения, комиссия при переводе, блокировка адресов, разные decimals - и как защищается эскроу.

4 мин чтенияОбновлено:
В этой статье

Стандарт ERC-20 описывает, как токен должен себя вести. На практике многие популярные токены ведут себя иначе, и каждое отличие - потенциальная ошибка в контракте, который с ними работает. Эскроу, принимающий чужие токены, вынужден учитывать всех. Разберём главные «сюрпризы».

1. USDT не возвращает значение

По стандарту transfer и transferFrom возвращают bool: true при успехе. Но USDT в Ethereum был написан раньше стандарта и ничего не возвращает. Если контракт напишет:

solidity
bool ok = IERC20(token).transfer(to, amount);   // для USDT: ошибка декодирования!

вызов упадёт: Solidity ожидает 32 байта ответа, а приходит пустота. Контракт, не учитывающий этого, не сможет работать с самым популярным стейблкоином.

Как решают. Используют обёртку SafeERC20 из OpenZeppelin. Она вызывает функцию «вручную» и считает успехом и пустой ответ, и true. Для автоматических выплат, где нельзя откатывать транзакцию, в нашем контракте похожая логика прописана явно:

solidity
(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, и при выплате последний получатель останется без денег.

Как решают. Проверяют фактическое поступление: замеряют баланс до и после и сравнивают.

solidity
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

Токены хранят суммы как целые числа и «знают», сколько знаков после запятой подразумевается:

ТокенСетьDecimals1 токен в «сырых» единицах
USDT, USDCEthereum, Polygon, Base61 000 000
USDT, USDCBNB Chain181 000 000 000 000 000 000
DAIвезде181 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.

Упоминается в статьях

Готовы попробовать?

Создайте первую сделку за пару минут. Деньги блокируются в смарт-контракте и выплачиваются по правилам, которые видны заранее.