Smart contract errors

Diagnostic flows for the runtime errors developers hit most often on TRON — OUT_OF_TIME, REVERT, OUT_OF_ENERGY, missing constant_result, TRC-20 Energy variance, and Stake 2.0 permission-denied failures.

📘

Prerequisites

This page is the diagnostic reference for the runtime errors developers see most often when writing or invoking TRON smart contracts. Each section gives the cause, how to confirm it, and the concrete fix. For the full exception-type taxonomy at the protocol layer, see VM exception handling.


OUT_OF_TIME — execution exceeded 80 ms

The contract function ran longer than the per-transaction execution budget. The full fee_limit is consumed when this happens — OUT_OF_TIME is treated as an assert-style failure (see VM exception handling).

The 80 ms budget is chain parameter #13 (API key getMaxCpuTimeOfOneTx); the SR committee can modify it through proposals. Query the live value via wallet/getchainparameters — do not hard-code 80 in your application.

If the same call sometimes triggers OUT_OF_TIME and sometimes does not, the contract is sitting on the boundary — different SR machine performance pushes some calls past the limit and not others.

Fixes:

  • Split the contract into smaller pieces that interact through cross-contract calls. Each cross-contract call resets the 80 ms budget.
  • Replace recursion with iteration where possible — iterative loops are cheaper in both Energy and execution time.
  • Avoid unbounded loops over storage arrays — these are the most common cause. Cache reads, paginate, or move the work to off-chain processing.
  • Re-estimate Energy under realistic conditions. Test with the same input sizes you expect in production, not just minimal cases.
  • Calibrate fee_limit to the contract's actual complexity. Setting fee_limit too high makes the cost of a single OUT_OF_TIME strike unnecessarily large.

For debugging-time bypass of the 80 ms cap (private chains only), see Node operations issues — bypass 80ms execution time limit.


REVERT — diagnostic flow

A REVERT indicates a require-style exception — the contract intentionally aborted. Only the Energy used so far is consumed; the rest of fee_limit is returned to the caller.

Diagnostic flow:

  1. Check the transaction's contractResult field via wallet/gettransactioninfobyid. The hex-encoded reason string follows the 08c379a0 selector (the Error(string) ABI-encoded prefix). Decode it with any hex-to-string tool.

    Example: txid: e5e013e81cb50a4c495a11c8130ad165a4e98d89b9e3fb5b79e6111bf23b31ed returns:

    "contractResult": [
      "08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001e536166654d6174683a207375627472616374696f6e206f766572666c6f770000"
    ]

    Decoding 1e536166654d6174683a207375627472616374696f6e206f766572666c6f77 (skipping the offset/length prefix) yields SafeMath: subtraction overflow — the transaction failed because the sender's balance went negative during a subtraction.

  2. If contractResult is empty, the revert came from a require assertion without a message string. Read the contract source code and identify which require could have failed for the inputs you used. For the full ABI-encoded error format including Solidity 0.8.4+ custom errors, see Errors and debugging.

  3. Look for require() or revert() calls along your function's code path. Confirm each precondition is met for the inputs you used.

  4. Check fund-related preconditions: insufficient TRX balance, insufficient TRC-20 allowance, expired deadlines, paused contracts.

  5. For low-level .call() failures, the call returns false rather than reverting — make sure your code propagates that flag back as a revert.

For Solidity 0.8.4+ custom errors, the selector is the first 4 bytes of keccak256("ErrorName(types)") — use the contract ABI to decode parameters.


OUT_OF_ENERGY — ran out of Energy mid-execution

OUT_OF_ENERGY means the transaction consumed all the Energy allowed by fee_limit (plus the deployer's contribution, if any) before the call finished. Energy already spent is not refunded.

Fixes:

  1. Re-estimate Energy with the production-shape inputs through wallet/triggerconstantcontract or wallet/estimateenergy. See FeeLimit & Energy cost for the full estimation flow.

  2. Increase fee_limit. The current Mainnet maximum is 15,000 TRX (chain parameter #47, API key getMaxFeeLimit — query the live value). If your contract genuinely needs more, redesign — Mainnet caps it for a reason.

  3. Verify the contract's consume_user_resource_percent isn't set so that the caller bears more than they can afford. Re-check the math in FeeLimit & Energy cost.

  4. Check for a Dynamic Energy Model surcharge. Popular contracts get a per-cycle energy_factor multiplier on top of base Energy cost. Query it through wallet/getcontractinfo.

  5. Top up TRX or stake for Energy. A contract call will fail if the initiating address has insufficient TRX to cover the required Energy or Bandwidth costs. For high-volume callers, stake TRX for Energy via Stake 2.0 or rent Energy via the JustLend Energy Rental market.

For the Energy / fee_limit / origin_energy_limit interactions in full detail, see FeeLimit & Energy cost.


Empty constant_result when calling a view or pure method

You receive an empty constant_result array when triggering a view or pure method via wallet/triggerconstantcontract. The call appears to "do nothing" — no return value, no error.

Cause: This is a known issue when a node was downgraded from GreatVoyage-v4.2.2 (Lucretius) or later to GreatVoyage-v4.2.1 (Origen) / v4.2.0 (Plato). The downgraded node's database is missing fields the constant-call path expects.

Fix:

  1. Run the DBRepair.jar database repair tool (shipped with java-tron) on the affected node.
  2. Restart the node with GreatVoyage-v4.2.2.1 (Epictetus) or newer.

See the DBRepair.jar user guide for step-by-step instructions.

If you are not running your own node and seeing empty constant_result, switch to a different RPC provider (TronGrid, QuickNode) — the issue is at the node operator level, not in your contract.


Two transfers of the same TRC-20 token cost different Energy

This is mainly caused by two factors: the storage state of the recipient account, and the Dynamic Energy Model.

Recipient storage state (the SSTORE cost)

In the TVM, the SSTORE instruction is used to write a word to storage. Its Energy consumption depends on the slot's current state:

  • Original value is 0 and new value is greater than 0: SSTORE consumes 20,000 Energy.
  • Original value is not 0: SSTORE consumes 5,000 Energy.

TRC-20 token transfers always execute SSTORE to update the recipient's balance. The first transfer to an address (slot is 0) costs 4× more than subsequent transfers to the same address. The first-time penalty cannot be avoided.

Dynamic Energy Model

For popular TRC-20 token contracts, the Dynamic Energy Model applies a per-cycle penalty multiplier — Energy consumption varies even for the same operation executed at different times. The factor is recalculated every maintenance cycle (~6 hours).

Examples

USDT (high-volume, dynamic-energy-affected):

  • Transfer to an address with USDT balance > 0: ~64,000 Energy. Example
  • Transfer to an address with USDT balance = 0: ~130,000 Energy. Example

The exact numbers fluctuate with USDT's energy_factor; consumption near these magnitudes is normal.

BTT (no dynamic-energy surcharge):

  • Transfer to an address with BTT balance > 0: ~13,253 Energy. Example
  • Transfer to an address with BTT balance = 0: ~28,253 Energy. Example

For cost predictability, estimate Energy immediately before broadcasting (or use the on-chain max_factor for an upper bound). See FeeLimit & Energy cost for the three calibration strategies.


"Permission denied" when using Account Permission Management for Stake 2.0

For accounts activated before Stake 2.0 took effect on Mainnet (committee proposal #84), initiating Stake 2.0 operations through the Account Permission Management multi-sig flow may return Permission denied.

Cause: the multi-sign permission operations field on those legacy accounts does not include the Stake 2.0 operation IDs. Stake 2.0 contracts (FreezeBalanceV2, UnfreezeBalanceV2, DelegateResource, WithdrawExpireUnfreeze, etc.) are protected by Active Permissions, and the legacy template does not authorize them.

Fix: update the account's Active Permission operations bitmap to grant the relevant operation IDs. Source-verified contract type IDs (Tron.proto:374-379):

OperationID
FreezeBalanceV2Contract54
UnfreezeBalanceV2Contract55
WithdrawExpireUnfreezeContract56
DelegateResourceContract57
UnDelegateResourceContract58
CancelAllUnfreezeV2Contract59

The operations field is a 32-byte bitmap; bit n corresponds to operation ID n. Computing the bitmap by hand is error-prone — use a TRON SDK helper (TronWeb, Trident) or the TronLink permission UI.

For the full Account Permission Management flow, see Multi-signature.


Related resources