The origin tracer is paused. The library is open and still growing.What’s paused, and when it returns

← All articles

TRC-20 Approvals and Allowances

Key takeaways

  • approve() writes a standing, reusable spend authorization into the token contract's storage; transferFrom() exercises it — two transactions, two signers, possibly far apart in time.
  • An unlimited allowance (2^256 − 1) never decrements: permanent authority over the owner's present and future balance. A bounded allowance is a finite budget that spends itself to zero.
  • On TRON the grant is a TriggerSmartContract call that burns Energy and moves zero tokens, and it persists as readable state until spent down or explicitly re-set to zero.
  • Every lingering non-zero allowance is a live answer to who can move this wallet's tokens — evidence the value-transfer view cannot show.

Token Standards on TRON covers how a TRC-20 transfer works — a contract call that moves value and hides its recipient in call data. This article covers the standard’s other half: approve() and the allowance, the mechanism by which a token owner hands a third party standing power over tokens the owner still holds. The grant itself moves nothing.

That last property is the investigative crux. An approval answers a different question than a transfer log does — who is able to move these tokens, whether or not anyone has. A wallet’s transfer history can be spotless while a counterparty holds live, exercisable authority over its entire balance. That authority is a control fact in its own right — worth reading whether or not the transfer history shows anything at all.

A grant, then a spend

The interface splits authority from movement. approve(spender, value) is the grant: TRON’s developer documentation describes it as one that “authorizes _spender (often a DApp contract) to transfer up to _value tokens from the caller’s account.” The signatures are identical to Ethereum’s ERC-20, and the behavioral fine print comes from that origin standard, EIP-20: the spender may withdraw “multiple times, up to the _value amount,” and calling approve again overwrites the current allowance — the amounts do not add. One signature authorizes many possible spends.

allowance(owner, spender) is the read side — a constant query returning what the spender may still move. EIP-20 phrases it as “the amount which _spender is still allowed to withdraw.” It behaves like a budget the spender draws down, and any observer can query it.

transferFrom(from, to, value) is the exercise. The spender calls it — a separate transaction, signed by a different party, possibly long after the grant — and it moves tokens out of the owner’s balance against the standing allowance. Every successful approve also emits an Approval(owner, spender, value) event with the owner and spender as indexed topics, which is what makes a target’s grants enumerable: filter the token’s event log on the owner address and the wallet’s approval history falls out.

[ ONE AUTHORIZATION · TWO TRANSACTIONS · TWO SIGNERS ] Owner signs approve(spender, value) BURNS ENERGY · MOVES NOTHING Spender signs transferFrom(owner, to, value) SEPARATE TX · OWN SIGNATURE · LATER [ WRITES THE GRANT ] [ DRAWS AGAINST IT ] TOKEN CONTRACT STORAGE allowance[owner][spender] = value emits Approval(owner, spender, value) PERSISTS UNTIL SPENT DOWN OR RE-SET TO 0 KEY [ TOKENS MOVE ] Recipient TOKENS LEAVE THE OWNER'S BALANCE [ THE GRANT WRITES STATE · THE EXERCISE MOVES VALUE ]
The grant and the exercise: one authorization, two transactions, two signers.

EIP-20’s own rationale flags the maintenance burden this design creates: re-approving is not additive-safe, so the standard advises clients to set an allowance to zero before setting it to a new value. Nothing in the protocol expires a grant on the owner’s behalf. Allowances need active hygiene, and stale non-zero grants accumulate in wallets whose owners have long forgotten signing them.

Unlimited and bounded allowances

The convention for “unlimited” is a single approve that sets the allowance to the largest value a 256-bit integer can hold — type(uint256).max in Solidity, 2^256 − 1. The OpenZeppelin ERC-20 reference implementation treats that sentinel specially: when the current allowance equals the maximum, transferFrom skips the decrement entirely. The implementation’s own comment states that it “does not update the allowance value in case of infinite allowance.”

A bounded allowance runs the other branch. Each transferFrom checks the requested amount against what remains, reverts if it exceeds the remainder, and writes back the decremented value.

Those two branches produce two different objects. A bounded grant is a finite budget that extinguishes itself — every spend shrinks it, and it ends at zero. An unlimited grant never shrinks. It stands, undiminished across any number of spends, over the owner’s entire balance — including tokens the wallet hasn’t received yet.

[ TWO GRANTS · TWO DIFFERENT OBJECTS ] Bounded allowance A FINITE BUDGET Each transferFrom checks the amount against what remains An overspend reverts Writes back the decremented value Spends itself down to zero REMAINING AFTER EACH SPEND SHRINKS TO 0 Unlimited allowance allowance = 2^256 − 1 · type(uint256).max transferFrom skips the decrement at the max sentinel Never shrinks, however many times it is spent Covers the owner's present and future balance Ends only when re-approved to zero REMAINING AFTER EACH SPEND STANDS AT MAX · NEVER DECREMENTS KEY [ ONE SPENDS ITSELF TO ZERO · ONE NEVER DOES ]
Two grants that read very differently: one spends itself out of existence, one never does.

How an approval reads on-chain

On TRON, approve() arrives as a TriggerSmartContract transaction — the same envelope as any TVM contract call, covered in Smart Contracts and the TVM. TRON’s documentation on TRC-20 interaction notes that these state-changing calls consume Bandwidth and Energy, and that they carry a fee_limit — “Maximum TRX consumption allowed in this call, in sun.” The full cost mechanics belong to What a Transaction Actually Costs; what matters here is the mismatch. The owner pays Energy to execute code that transfers nothing. The transaction’s entire output is a write: a control action, not a value action.

What that write produces is the allowance itself, stored in the token contract as a nested mapping — owner address to spender address to amount, in OpenZeppelin’s layout mapping(owner => mapping(spender => uint256)). Anyone can read it at the current block. And it stays: a bounded allowance ends when it is spent down, any allowance ends when the owner re-approves it to zero, and nothing else touches it. The approval is stored state, and nothing in the protocol expires it. A grant signed years ago is still a live, queryable fact about who controls a balance today.

The lingering approval as evidence

A non-zero allowance held by a counterparty means that counterparty can move the owner’s tokens right now, by signing a transferFrom of its own. The owner does nothing further. That is a control relationship in the plainest sense, and it is exploited in the wild: security researchers at CYFIRMA documented a QR-code phishing campaign targeting USDT on BNB Smart Chain via Trust Wallet that “covertly triggers an ERC-20 approve() transaction, granting unlimited token allowance to an attacker-controlled contract” — the advisory describes the result as “persistent access, allowing attackers to drain funds from victim wallets without further interaction,” exposing “both current and future USDT balances.” The mechanism is identical wherever a chain runs an ERC-20-style approve/transferFrom pair, TRON’s TRC-20 included, even though this particular documented campaign ran on BNB Smart Chain, not TRON. The same report stresses that the attack requires no wallet vulnerability at all. The victim authorizes the grant; the protocol does exactly what it was asked.

None of this registers at the transfer level. Until the spender acts, no tokens have moved, and an explorer’s transfer tab for the wallet shows an ordinary history. The evidence is the standing grant — the Approval event in the token’s log and the non-zero entry in its allowance mapping. Reading it is a control determination: it identifies who holds signing power over the balance.

The transfer view shows nothing because nothing has moved yet. The allowance is the evidence.

This is why the unlimited-versus-bounded distinction is worth reading closely. An unlimited grant is a categorically stronger control fact than a bounded one: it is unmetered, it survives every spend, and it covers balance the wallet has yet to receive. A bounded grant caps the spender’s reach and destroys itself as it is used. A careful reading of a wallet’s outstanding approvals weighs that standing authority accordingly.

For an investigator, the working habit is to treat approvals as a first-class query alongside the transfer history. Enumerate the target’s Approval events, resolve each spender’s current allowance, and read what remains: every non-zero entry is a party that can move the wallet’s tokens today, and every unlimited entry is a party that can move all of them, indefinitely. The transfer graph tells you where value went; the allowance table tells you whose signature can still send it.

Sources