tip-3009.md
July 23, 2026 ยท View on GitHub
tip: 3009
title: Transfer With Authorization
description: Transfer TRC-20 tokens via a signed authorization
author: yanghang8612@gmail.com
discussions-to: https://github.com/tronprotocol/tips/issues/883
status: Draft
type: Standards Track
category: TRC
created: 2026-05-26
requires: 20, 165, 712
Simple Summary
Let a TRC-20 holder sign a one-time transfer authorization that a relayer can submit and pay resources for.
Abstract
This standard adapts ERC-3009 to extend TRC-20 with transfers authorized by an off-chain TRC-712 signature. A relayer can submit the authorization and pay the transaction's energy and bandwidth costs. Random bytes32 nonces allow independent authorizations to be created and redeemed in parallel or out of order.
The extension defines direct transfers, payee-submitted transfers, cancellation, authorization-state discovery, and optional TRC-165 capability detection. It complements rather than replaces TRC-2612 approvals and TRC-2771 forwarded calls.
Motivation
Stablecoin users on TRON frequently hold TRC-20 balances but no TRX with which to pay transaction fees. Token-specific relayer arrangements do not provide a portable wallet or SDK interface. A transfer-by-signature primitive lets a holder authorize the transfer itself and lets any relayer submit it.
Sequential nonces are a poor fit for exchanges, payment processors, subscriptions, and agentic payments that create many independent transfers. A random nonce avoids head-of-line blocking and coordination among devices or relayers while retaining one-time execution.
Specification
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" are to be interpreted as described in RFC 2119.
Interface
interface ITRC3009 {
event AuthorizationUsed(address indexed authorizer, bytes32 indexed nonce);
event AuthorizationCanceled(address indexed authorizer, bytes32 indexed nonce);
function transferWithAuthorization(
address from,
address to,
uint256 value,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
uint8 v,
bytes32 r,
bytes32 s
) external;
function receiveWithAuthorization(
address from,
address to,
uint256 value,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
uint8 v,
bytes32 r,
bytes32 s
) external;
function cancelAuthorization(
address authorizer,
bytes32 nonce,
uint8 v,
bytes32 r,
bytes32 s
) external;
function authorizationState(address authorizer, bytes32 nonce)
external view returns (bool);
}
Implementations MAY accept a single bytes signature overload in addition to the split v, r, and s functions above. A bytes-signature overload MUST NOT replace the normative interface and does not change the typed-data definitions below.
Authorization State
Each (authorizer, nonce) pair MUST have a single boolean state. false means unused; true means consumed by either successful transfer or cancellation. Implementations MUST mark the nonce used before invoking token-transfer logic and MUST revert the state change if the transfer reverts.
The nonce MUST be an unpredictable or collision-resistant 32-byte value chosen by the signer. Implementations MUST NOT impose sequential ordering. A typical layout is:
mapping(address authorizer => mapping(bytes32 nonce => bool used))
private _authorizationStates;
Validity Windows
For transfer and receive authorizations:
- execution MUST revert unless
block.timestamp > validAfter; - execution MUST revert unless
block.timestamp < validBefore; and - execution MUST revert if the nonce is already used.
Cancellation has no validity window. A cancellation and redemption of the same nonce race for inclusion; the first successful operation consumes the nonce.
Typed Data
The domain MUST follow TRC-712 and bind all of:
EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)
chainId MUST equal block.chainid & 0xffffffff in both the wallet and verifying contract. verifyingContract MUST be the token contract. TRON addresses MUST be encoded as their ABI-level 20-byte value after removing the 0x41 network prefix; Base58Check is display-only.
The primary types are distinct:
TransferWithAuthorization(
address from,address to,uint256 value,
uint256 validAfter,uint256 validBefore,bytes32 nonce)
ReceiveWithAuthorization(
address from,address to,uint256 value,
uint256 validAfter,uint256 validBefore,bytes32 nonce)
CancelAuthorization(address authorizer,bytes32 nonce)
The digest MUST be keccak256("\x19\x01" || domainSeparator || structHash).
The three type hashes are:
bytes32 constant TRANSFER_WITH_AUTHORIZATION_TYPEHASH = keccak256(
"TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)"
);
bytes32 constant RECEIVE_WITH_AUTHORIZATION_TYPEHASH = keccak256(
"ReceiveWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)"
);
bytes32 constant CANCEL_AUTHORIZATION_TYPEHASH = keccak256(
"CancelAuthorization(address authorizer,bytes32 nonce)"
);
Signature Verification
The contract MUST enforce both:
recovered != address(0) && recovered == authorizer
where authorizer is from for transfer/receive and the explicit authorizer for cancellation. Implementations MUST reject malformed signatures, invalid v, and non-canonical high-s signatures. Wallets MUST sign the TRC-712 digest directly and MUST NOT add a TRON personal-message prefix.
This version covers secp256k1 address recovery. Address-less keys, passkeys, and smart-contract signers require an explicit signer-binding model and are outside this interface.
Transfer Semantics
transferWithAuthorization MAY be submitted by any address after validation.
receiveWithAuthorization MUST additionally require to == msg.sender. Its distinct primary type prevents a signature intended for the payee-submitted path from being replayed through the permissionless path.
After validation and nonce consumption, both functions MUST perform the same internal transfer and compliance checks as ordinary TRC-20 transfer/transferFrom logic.
Cancellation
cancelAuthorization MUST validate a CancelAuthorization signature and MUST revert if the nonce was already used. On success it MUST consume the nonce and emit AuthorizationCanceled.
Capability Discovery
Tokens SHOULD implement TRC-165 and return true for interface identifier 0xbff533ba, the XOR of the four split-signature function selectors in ITRC3009. Wallets MUST check support before requesting a signature and MUST provide a clear unsupported-token fallback. Implementations exposing alternate signature overloads SHOULD document those selectors separately.
Events
Successful redemption MUST emit AuthorizationUsed(authorizer, nonce) in addition to the normal TRC-20 Transfer. Successful cancellation MUST emit AuthorizationCanceled(authorizer, nonce).
Rationale
Positioning in the Gasless Stack
- TRC-3009 moves tokens immediately by signature. It is suited to payments, payroll, subscriptions, and parallel agent transactions.
- TRC-2612 creates an allowance by signature. It is suited to swaps, deposits, and later
transferFrompulls. - TRC-2771 forwards arbitrary contract calls. It is suited to voting, claims, mints, and application-specific methods.
These primitives can be composed. TRC-3009 deliberately contains no relayer-fee field; token skims, off-chain payment, and sponsored services are application-layer policy.
Random Nonces
Random nonces let multiple devices or relayers submit independent authorizations without coordination. They also permit long-lived authorizations without blocking newer payments. The storage cost is accepted in exchange for avoiding ordering dependencies.
TRON Resource Model
"Gasless" means the signer does not pay TRX. The submitting account still pays or supplies bandwidth and energy. TRON's staked/regenerating resources may reduce a relayer's marginal cost, but do not change the authorization or reimbursement interface.
Backwards Compatibility
This is an opt-in TRC-20 extension. Existing token functions, balances, and allowances are unchanged. Existing deployed tokens can adopt it only when their upgrade mechanism permits new code and ABI; otherwise adoption requires a new token version or wrapper.
Test Cases
The following deterministic Mainnet vector uses the test-only secp256k1 private key 0x0000000000000000000000000000000000000000000000000000000000000001 (it MUST NOT hold assets):
domain.name = "Example USD"
domain.version = "1"
domain.chainId = 728126428 (0x2b6653dc)
domain.verifyingContract = 0x1111111111111111111111111111111111111111
primaryType = TransferWithAuthorization
from = 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf
to = 0x2222222222222222222222222222222222222222
value = 1000000
validAfter = 1700000000
validBefore = 1900000000
nonce = 0x0000000000000000000000000000000000000000000000000000000000000001
domainSeparator = 0x7f5e759a066ea1edcade46097b2b71b052cbf9705b4f81663f60da0e7574a827
structHash = 0x3d6d9c81e215c2a5e151cee3bf642d3f635a91cabc8706ae9da1a0e9d56d294c
digest = 0x507f51a256c1879a67e4d05d0687a7430419717189c695957fa3082899d3bf08
r = 0xb64a2eb8d94a7821239df122eb6539a4710d0532686eb5df5d551ee8d78757bf
s = 0x5309ef8faa199d400dd88dc0b09be555e9bdf091695c37277e5ce4f6d9b5bef6
v = 28
recovered = 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf
The vector is encoded exactly as TRC-712/EIP-712 typed data; Base58Check conversion is not part of the digest.
Implementations MUST publish and pass an interoperability vector set containing:
- domain fields, encoded domain separator, and the exact truncated TRC-712
chainId; - one complete transfer authorization: typed data, struct hash, digest, signature, and recovered address;
- the equivalent receive and cancel primary types; and
- negative cases for reused nonce, not-yet-valid and expired windows, wrong
verifyingContract, wrong/full-widthchainId,to != msg.sender, high-s, invalidv, and malformed signatures recoveringaddress(0).
The vector producer and verifier MUST use identical ABI-level 20-byte addresses. Mainnet and Nile vectors SHOULD both be provided so wallet and SDK implementations exercise network separation.
An early, unaudited proof of concept is available at simbadMarino/tron-3009. It is informative and not normative.
Security Considerations
- A signed authorization is a live token-transfer instruction, not a login message. Wallets SHOULD display amount, receiver, validity window, nonce status, and whether cancellation is finalized on-chain.
- Relayers and block producers can choose ordering. Dependent authorizations SHOULD be submitted one at a time.
- A signer cannot assume a submitted cancellation wins a race with redemption.
- The
receiveWithAuthorizationcaller check is mandatory; omitting it enables front-running. - Domain separation MUST include both the truncated TRC-712 network id and the token contract. Omitting either enables replay.
- A token's pause, blocklist, and other transfer rules are evaluated by its internal transfer logic at redemption time.
- Relayers MUST treat authorizations as sensitive bearer instructions and protect them from premature disclosure.
Copyright
Copyright and related rights waived via CC0.