Энергия на approve в TRC-20 и другие вызовы контрактов TRON
Энергия — это то, чем TRON берёт плату за выполнение кода контрактов, а перевод USDT — один вызов контракта из многих. approve тоже её тратит, как и transferFrom, который позже двигает разрешённые средства. 28 октября 2026 года мы прогнали оба через dry-run на контракте USDT: approve потратил 99,764 единиц энергии при нулевом разрешении и 33,764, когда оно уже было выставлено, а transferFrom — 65,123 при неограниченном разрешении и 89,325 при ограниченном. У вызовов в другие контракты свои цифры, и арендованная энергия оплачивает любой из них так же, как оплачивает перевод.
Делегация поднимает лимит энергии аккаунта, на который приходит, и каждый вызов контракта, который подписывает этот аккаунт, тратит из этого лимита раньше, чем сгорит какой-либо TRX (что такое энергия TRON). Ничто в делегации не называет контракт или функцию, для которых она предназначена. Вызов, отличный от перевода, отличается тем, сколько он тратит, — это зависит от контракта и функции, которые он вызывает, и от состояния, которое он там находит, — а на некоторых контрактах ещё и тем, какую долю из этого платит подписывающий.
Сколько потратили вызовы USDT в dry-run
Dry-run — это constant-вызов: нода исполняет вызов на собственной копии цепочки, сообщает потраченную энергию и отбрасывает результат. Ничего не подписывается и не отправляется в сеть, и это ничего не стоит. Свои мы сделали через публичную полную ноду между 08:29 и 08:34 UTC 28 октября 2026 года. Владельцами были настоящие аккаунты, выбранные из новейших блоков сразу после того, как каждый выдал разрешение спендеру (адресу, которому позволено тратить), а изъятия прогонялись как dry-run от имени этих спендеров. approve с нуля ушёл на адрес, сгенерированный нами для сентябрьского замера, которому ни один владелец разрешения не выдавал. Каждое разрешение и каждый баланс мы читали за мгновения до вызова. Каждая цифра ниже вернулась из вызова, который удался бы. Перевод и оба изъятия двигали одну единицу, миллионную долю USDT, на один и тот же адрес, где USDT есть.
| Вызов USDT, 28 октября 2026 года | Потрачено энергии | Из неё записано как energy_penalty | Сожжено по 100 sun за единицу |
|---|---|---|---|
transfer на адрес, где USDT есть | 64,285 | 49,635 | 6.4285 TRX |
approve, разрешение нулевое | 99,764 | 77,076 | 9.9764 TRX |
approve, разрешение уже выставлено | 33,764 | 26,076 | 3.3764 TRX |
transferFrom от спендера, при неограниченном разрешении | 65,123 | 50,281 | 6.5123 TRX |
transferFrom от другого спендера, при ограниченном разрешении | 89,325 | 68,976 | 8.9325 TRX |
Оба вида approve шли от одного владельца на одну и ту же новую сумму. У одного спендера разрешения ещё не было, у другого оно стояло, и вызов для первого обошёлся на 66,000 единиц дороже. Это тот же разрыв, что между переводом на адрес без USDT и на держателя, который мы замерили в сентябре как 130,285 и 64,285 (две цифры перевода). В обеих парах более дешёвый вызов перезаписывал число, которое там уже было, а более дорогой писал поверх нуля.
Если только создатель контракта не согласился взять часть на себя, как у JustLend DAO (ниже), энергию вызова платит тот, кто его подписывает. В схеме, где клиент выдаёт разрешение контракту мерчанта, а мерчант позже изымает средства, клиент платит за каждый approve, а тот, кто подписывает изымающий вызов, платит за него: за transferFrom из таблицы плюс за собственный код контракта мерчанта, когда изъятие идёт внутри него. Два изъятия, которые нам удалось прогнать через dry-run, различались разрешением. При неограниченном transferFrom спендера обошёлся на 838 единиц дороже обычного перевода той же суммы на тот же адрес, а при ограниченном — ещё на 24,202 сверх этого. Это по одной паре каждого вида, и у пар были и разные владельцы, и разные спендеры.
Чуть больше трёх четвертей каждой цифры сидит в поле, которое нода называет energy_penalty, и доля одинакова для всех пяти вызовов — для approve так же, как для transfer.
Кредитный контракт: два вызова JustLend DAO
Аренда энергии на JustLend DAO сама по себе — пара вызовов контракта: один открывает позицию, другой её возвращает (во сколько обходится такая аренда). Мы сопоставили 30 аренд с их возвратами по публичной записи контракта за 24 часа, закончившихся вечером 2 сентября 2026 года, и прочитали по квитанции каждого вызова, сколько он потратил в сумме.
| Вызов JustLend DAO, 2 сентября 2026 года | Наименьшее | Медиана | Наибольшее |
|---|---|---|---|
Аренда, rentResource | 52,672 | 69,306 | 84,290 |
Возврат, returnResource | 67,195 | 74,135 | 74,135 |
Таблица даёт итоги, потому что подписывающий не всегда платит всё. Контракт настроен брать со своих вызывающих десятую часть энергии, а остальное покрывать из собственной энергии создателя — до 90,000 единиц на вызов, пока этой энергии хватает. Наша собственная аренда тем вечером потратила 84,290 единиц, и подписавший заплатил за 8,429 из них. Менее чем за три часа до этого аренда третьей стороны оплатила все 52,672 сама, потому что энергия создателя кончилась, а её возврат двенадцатью секундами позже оплатил 62,200 из 74,135. Dry-run сообщает, сколько вызов использует, а подписывающий платит что угодно — от десятой части, которую контракт берёт со своих вызывающих, до всего, в зависимости от энергии создателя в эту секунду, поэтому рассчитывайте вызов на всё.
Как рассчитать вызов, который за вас никто не замерял
Ни одна таблица не покрывает все контракты, а цифры выше меняются вместе с состоянием, которое находит вызов. Доверять стоит числу, полученному dry-run именно этого вызова, с аккаунта, который будет его подписывать, незадолго до отправки. Отвечает любая полная нода:
POST /wallet/triggerconstantcontract
{"owner_address": "T… (аккаунт, который будет подписывать)",
"contract_address": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
"function_selector": "approve(address,uint256)",
"parameter": "… (аргументы в кодировке ABI)",
"visible": true}
Проверяйте ответ, прежде чем брать число, потому что вызов, который откатился бы, всё равно сообщает энергию. Две из наших первых попыток transferFrom пришлись на пары, которые уже не могли заплатить: одно разрешение было выбрано за секунды до нашего запроса, а у одного владельца USDT не было вовсе. Они вернулись с 5,947 и 4,845 единицами за работу, заканчивающуюся сбоем. Их result.result всё равно читался как true. Отличало их FAILED в transaction.ret, где у успешного вызова остаётся пустая запись.
Оставляйте запас над цифрой, ведь разрешение можно израсходовать, а получателя опустошить между dry-run и блоком. Запас оставляем и мы: наши стандартные цифры перевода, 65,000 и 131,000, лежат на 715 единиц выше того, что измеряют два перевода, а когда подписанный перевод USDT, переданный нам в mode B, в dry-run выходит выше замера перевода на пустой адрес, мы покупаем замер плюс запас, а доставку оцениваем по самому замеру.
Выставьте fee_limit транзакции так, чтобы он покрывал цифру целиком по цене сети 100 sun за единицу, даже когда энергия арендована. Нода ограничивает вызов величиной fee_limit, делённой на цену энергии, и считает делегированную энергию внутри этого предела, поэтому низкий лимит останавливает вызов, у которого на аккаунте есть каждая нужная единица. Пропускная способность — отдельный счётчик: каждый вызов платит ещё и за свои байты, и энергия их не покрывает (энергия и пропускная способность).
Своп через контракт обменника — та же арифметика с ещё одним контрактом внутри: approve, разрешающий обменнику двигать токены, затем вызов в обменник. Мы такой не прогоняли через dry-run.
Аренда энергии под вызов по единицам
Наши mode A и mode B построены вокруг перевода USDT, а mode B принимает подписанный transfer USDT и больше ничего. В mode A, когда получатель перевода оказывается контрактом, dry-run за вас делаем мы: позиция возвращается как kind: custom, её energy_units — замер, а предупреждение contract_recipient объясняет почему (custom-позиции до подписи чего бы то ни было).
Для любого другого вызова есть mode C. Вы называете кошелёк и объём, а мы делегируем энергию этому кошельку без участия какого-либо получателя (mode C в разборе API). Объём можно задать через energy_units — любое целое число от 65,000 до 50,000,000 на кошелёк, — и позиция возвращается как kind: bulk. Mode C не принимает custom, потому что нет получателя, которого мы могли бы прогнать через dry-run, так что замер на вас. Кошелёк должен быть активирован в сети, а заказ на неактивированный возвращается отклонённым с inactive_wallet.
Пятиминутная аренда принимает объёмы до 131,000. Всё, что больше, обязано идти на часовую аренду, а на более короткой мы отказываем с bulk_needs_longer_lease: большой объём собирается из нескольких источников и приходит частями с разницей в секунды. approve с нуля сам по себе помещается под эту черту, с местом под запас. Добавьте к нему перевод держателю — и пара эту черту переходит, поэтому идёт на час. Наименьший объём, который мы продаём, покрывает approve на стоящем разрешении, и остаётся ещё почти столько же.
POST /v1/estimate с тем же energy_units отвечает, сколько этот объём стоил бы в данный момент, и в поле coverage — есть ли он сейчас на рынке. После доставки позиция остаётся в ready до send_before, а затем читается как expired — здесь это значит, что срок аренды вышел: нормальный конец. Плата берётся за доставку, а энергия, которую кошелёк оставляет неиспользованной, уходит обратно вместе с делегацией.
Чего мы не измеряли: своп, вызов любого контракта, кроме USDT и JustLend DAO, transferFrom на адрес без USDT и то, держатся ли эти цифры неделями. Каждая цифра USDT здесь — один constant-вызов, на который ответила одна нода; мы не отправляли ни одной транзакции, поэтому ни одна из них не проверена по квитанции.
Прогоните вызов через dry-run, затем положите этот объём на кошелёк, который будет его подписывать: mode c и energy_units в POST /v1/orders →