Всё правильно, а транзакция не проходит. Часть II

Всё правильно, а транзакция не проходит. Часть II

Переведено с английского

В первой части статьи я рассказывал вам о случае, когда обменник rabbit.io пытался отправить клиенту немного SOL, а транзакция всё время отклонялась, хотя по всем известным нам правилам блокчейна Solana всё вроде бы выглядело корректно.

Также в первой части статьи рассказывалось об обстоятельствах, при которых транзакция, которая составлена полностью правильно, может быть отклонена в сетях XRP Ledger, Stellar и Lightning Network (L2 Биткоина). А ещё я рассказал об особенности реализации чёрных списков в смарт-контрактах USDC, из-за которой транзакции с USDC тоже могут неожиданно отклоняться, даже когда у отправителя всё в порядке.

Если пропустили первую часть статьи, читайте её здесь. А сегодня я расскажу о похожих примерах, связанных с другими сетями и токенами. Итак, вот в каких случаях ещё может быть, что всё сделано правильно, а транзакция не проходит.

5. USDT в DeFi. Транзакция молча развернулась

Этот случай специфичен для разработчиков и операторов DeFi-протоколов. Тем не менее, его последствия ощущают и обычные пользователи.

Представьте себе: вы пытаетесь внести USDC, DAI и USDT в какой-нибудь пул; с USDC или DAI всё работает, а с USDT перевод возвращается. Адрес верный, сеть верная, газ есть. В чём причина?

Дело в том, что USDT на Ethereum не полностью соответствует стандарту ERC-20. Стандарт требует, чтобы функции transfer и transferFrom возвращали булевое значение: true при успехе, false при неудаче. USDT не возвращает ничего.

Если смарт-контракт написан строго по стандарту ERC-20 и ожидает получить bool в ответ на вызов transfer, а USDT возвращает пустоту, то виртуальная машина EVM, работающая на языке Solidity начиная с версии 0.4.22, интерпретирует это как ошибку и возвращает перевод. Технически транзакция могла бы пройти успешно, но она принудительно откатывается. И отправитель не получает ни от сети, ни от смарт-контракта никакой информации о том, что же конкретно пошло не так.

Данный пример не имеет отношения к обменам на сайте rabbit.io. У нас всё организовано максимально просто и надёжно: вы получаете адрес, на который нужно отправить ваши токены, отправляете их туда вручную, а взамен вам приходят от нас токены, которые вам нужны. Смарт-контракты DeFi не задействуются.

Тем не менее, для тех читателей, которые не только совершают обмены на rabbit.io, но и активно взаимодействуют с DeFi, в том числе занимаются разработкой, дам простое решение. Используйте библиотеку SafeERC20 от OpenZeppelin, которая корректно обрабатывает нестандартные токены. Большинство профессионально написанных DeFi-протоколов так и делают. Но старые или любительские контракты до сих пор ломаются на USDT. Эта проблема настолько распространена, что попала в базу известных уязвимостей weird-erc20, в которой вы можете найти и другие токены с этой особенностью.

6. USDT на Ethereum. Нельзя изменить разрешение напрямую

У токена USDT на Ethereum есть и ещё одна особенность, регулярно ломающая транзакции со смарт-контрактами. В стандарте ERC-20 для того чтобы разрешить смарт-контракту тратить ваши токены, вы вызываете функцию approve(spender, amount). Например, если хотите увеличить лимит со 100 до 200 USDC, вы вызываете approve(spender, 200).

Но USDT устроен иначе. В его коде явно прописано: если у адреса-получателя уже есть ненулевое разрешение, нельзя установить новое ненулевое значение напрямую. Транзакция, которая для любых других токенов была бы полностью валидной, с USDT отклоняется. Это сделано как защита от атаки двойного расхода разрешённого лимита: разработчики опасались, что злоумышленник мог бы мгновенно потратить и старый лимит, и новый.

Обязательный паттерн для изменения разрешения USDT:

  • Шаг 1: вызвать approve(spender, 0), то есть обнулить разрешение;
  • Шаг 2: дождаться подтверждения транзакции;
  • Шаг 3: вызвать approve(spender, новое_значение).

Для пользователей децентрализованных приложений это может быть неожиданностью. Ранее пользователь уже давал приложению разрешение на расходование, а теперь хочет установить другое, но подписанная транзакция ничего не изменяет без понятной причины.

Этот пример тоже не имеет отношения к обменам на rabbit.io. У нас таких отклонённых транзакций быть не может, потому что rabbit.io не требует подключения кошелька и не запрашивает разрешения расходовать токены из него.

Но если в каком-то децентрализованном приложении транзакция approve с токенами USDT отклоняется, то решение, скорее всего, простое. Проверьте текущий уровень разрешения, и, если он ненулевой, сначала обнулите его, а затем установите новое значение.

7. Bitcoin. Пылевой лимит

В сети Bitcoin существует понятие «пыль» (dust). Под ним понимается выход транзакции, размер которого меньше суммы, необходимой для его последующей траты. Иными словами, монеты в кошельке есть, но комиссия за их отправку должна быть больше, чем стоят сами монеты.

Здесь важно понимать принципиальное различие, которое часто путают: правила консенсуса и политику узлов сети. Транзакция, выходы которой меньше пылевого лимита, технически может быть валидна по правилам консенсуса Биткоина, но большинство узлов по умолчанию откажутся её ретранслировать, а большинство майнеров откажутся включать её в блок. Объясняется это тем, что хранение крошечных монет раздувает базу данных без реальной экономической пользы.

Лимит пыли зависит от типа адреса и минимальной ставки комиссии. Исторически лимитом пыли считается 546 сатоши. Транзакции с монетами меньшего объёма будут стоить больше, чем отправляемая сумма, если минимальный размер комиссии - 1 sat/vByte. Но это справедливо только для старых адресов, начинающихся на 1. Более современные адреса создают транзакции, которые занимают меньше места в блокчейне, и пылевой лимит для них может быть ниже. Например, для обычных адресов bc1q… при комиссии 1 sat/vByte пылевой лимит будет равен 294 сатоши, а при том значении минимальной комиссии, которое применяется сейчас (0,1 sat/vByte) пылевой лимит будет ещё в десять раз меньше.

Bitcoin fees, Source: mempool.space

Представьте себе такую ситуацию. У вас есть 100 000 сатоши (0,001 биткоина). Вам нужно отправить кому-то 99 000 сатоши. Вы формируете транзакцию, сетевая комиссия за неё составляет 980 сатоши. 20 сатоши должны вернуться к вам. Но этого не произойдёт: вы не получите свою сдачу, а основная сумма не дойдёт до получателя. Сеть нод-ретрансляторов молча отвергнет такую транзакцию, поскольку один из её выходов меньше пылевого лимита.

Смежная проблема - обратные транзакции. Если при нынешнем уровне комиссий вы получите много транзакций с выходами менее 294 сатоши, а потом комиссии повысятся, то любая транзакция, использующая эти монеты, будет настолько дорогой, что суммы перевода не хватит для оплаты комиссии.

Так что при обмене других криптоактивов на биткоины следите за тем, чтобы создаваемые выходы превышали пылевой лимит. Rabbit.io технически позволяет проводить обмены на суммы, превышающие текущий пылевой лимит в несколько раз. Но при росте комиссий сети такие маленькие суммы будет проблематично отправить куда-то дальше.

8. Ethereum и EVM-сети. Одна старая транзакция блокирует все новые

В сети Ethereum и любых EVM-совместимых блокчейнах (BSC, HyperEVM и других) каждая транзакция, отправленная с конкретного адреса, имеет порядковый номер - nonce. Сеть обрабатывает транзакции строго последовательно: сначала с nonce = 0, затем 1, 2 и так далее. Пропустить номер нельзя.

Если одна из транзакций застряла в мемпуле из-за слишком низкой комиссии - все последующие транзакции с того же адреса с более высокими nonce зависнут тоже, даже если у них нормальная комиссия.

Вот вполне реалистичный сценарий. Несколько недель назад вы отправили транзакцию с низкой платой за газ. Она зависла. Вы об этом забыли. Теперь пытаетесь совершить новую транзакцию. Указываете правильный адрес, выставляете адекватную комиссию, а транзакция не проходит и не проходит. Причина не в текущей, а в той давней зависшей транзакции.

Решение: отправить транзакцию с тем же старым nonce, но с более высокой комиссией. Это либо ускорит оригинальную операцию (Speed Up), либо отменит её (последнее произойдёт в случае, если отправить перевод самому себе с тем же nonce и более высокой платой за газ).

Но обратите внимание, что далеко не все кошельки позволяют контролировать значение nonce. Например, в популярных кошельках Trust Wallet и Exodus этой функции нет. А в MetaMask, который многие считают устаревшим и неудобным, она есть.

9. TRON и USDT TRC-20. Отказ из-за нехватеи энергии

Сеть TRON использует нестандартную модель ресурсов. Вместо единой комиссии транзакции потребляют два типа ресурсов: Bandwidth (пропускная способность) - для любых транзакций, и Energy (энергия) - для взаимодействия со смарт-контрактами, включая переводы токенов TRC-20.

Энергию можно получить, заморозив TRX. Если этого не сделать, сеть автоматически сжигает TRX для покрытия затрат. И здесь возникает неожиданная проблема с ценообразованием:

  • если у получателя уже есть USDT, будет сожжено 6 - 13 TRX;
  • если же у получателя ещё не было USDT, потребуется 13–27 TRX.

Если на балансе отправителя 15 TRX на комиссию, а перевод идёт на новый адрес - транзакция может быть отклонена с ошибкой OUT OF ENERGY. Потраченный TRX при этом не возвращается. То есть всё опять выглядит так же, как и в других примерах из статьи: адрес верный, сеть верная, USDT есть, TRX есть, а транзакция не прошла.

Вывод: для стабильной работы с USDT TRC-20 держите на балансе не менее 27 TRX.

Вместо заключения

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

Каждый непонятный сбой - это не повод для раздражения, а возможность узнать что-то новое. И чем больше я узнаю нового, тем интереснее мне работать с криптовалютами дальше.

Если и вам тоже, то документируйте нестандартные случаи и делитесь своими находками. Криптовалютная отрасль становится надёжнее, когда практический опыт распространяется быстрее, чем успевает обновляться официальная документация.