Skip to content

Smart Contracts (TVM)

The TRON Virtual Machine (TVM) provides a high-performance, EVM-compatible environment for deploying Solidity smart contracts. While sharing much of the Ethereum toolchain, this guide highlights critical TVM-specific behaviors, including millisecond timestamp handling and updated self-destruct logic, to ensure safe and optimized deployments.


On Ethereum, block.timestamp returns Unix time in seconds. On TVM, it returns Unix time in milliseconds.

Timestamps.sol
// Task: Compare timestamps in milliseconds as required by TVM.
// This comparison works on Ethereum but behaves incorrectly on TRON:
require(block.timestamp > 1700000000); // expects seconds
// Correct for TVM — compare against milliseconds:
require(block.timestamp > 1700000000000); // milliseconds
// Or: use a relative comparison to avoid the absolute value issue:
uint256 private deployTime = block.timestamp;
require(block.timestamp > deployTime + 30 days * 1000); // 30 days in ms

SELFDESTRUCT Behavior Updated (Proposal 106)

Section titled “SELFDESTRUCT Behavior Updated (Proposal 106)”

Historically, the selfdestruct opcode was disabled on TRON. However, following the approval of TRON Governance Proposal No. 106 (April 2026), the behavior now aligns with Ethereum (EIP-6780) to improve TVM compatibility:

  • A contract will only be deleted if selfdestruct is executed within the same transaction that created the contract.
  • If executed in any subsequent transaction, only the TRX asset transfer occurs, and the contract code remains on-chain.
  • The Energy cost for calling selfdestruct has been adjusted to 5000.

Support for TSTORE, TLOAD (EIP-1153), and MCOPY (EIP-5656) was added to the TVM as part of the GreatVoyage-v4.8.0 (Kant) upgrade (June 2025). This ensures developers can port contracts utilizing Ethereum’s Cancun/Dencun upgrades without modification.

TRON addresses are 21 bytes internally (Ethereum addresses are 20 bytes). The ABI encoding pads to 32 bytes as on EVM, so ABI-encoded contract calls work correctly. However, if you are doing raw byte manipulation of addresses (e.g., in assembly), account for the 21-byte size.

On Ethereum, block.coinbase is the miner/validator address. On TVM, it is the Super Representative address that produced the current block. Do not use block.coinbase as a source of randomness — it is predictable and manipulable by SRs.

The CREATE opcode derives a contract address differently on TRON than on Ethereum. If you are deploying contracts that depend on deterministic CREATE addresses computed off-chain, recalculate them for the TRON address derivation scheme. CREATE2 with an explicit salt works correctly for deterministic addresses.

On Ethereum, address.transfer(amount) has a fixed 2,300 gas stipend. On TVM, this call uses Energy. Reentrancy patterns that relied on the 2,300 gas limit for safety may behave differently. Use the checks-effects-interactions pattern regardless.


TronBox supports Solidity up to 0.8.x. Use pragma ^0.8.0 or pin to a specific version. Solidity 0.8 includes built-in overflow checks, which removes the need for SafeMath.

version.sol
// Task: Specify the Solidity compiler version for TVM.
pragma solidity ^0.8.18;

TRC-20 is functionally identical to ERC-20. The full function signature reference, event definitions, and known mainnet contract addresses are in Token Standards. Below is a complete, deployable minimal TRC-20 with TVM-specific notes inline.

SimpleTRC20.sol
// Task: This code defines a minimal TRC-20 token contract.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
/**
* @title SimpleTRC20
* @dev Minimal deployable TRC-20 token for TRON.
*/
contract SimpleTRC20 {
string public name;
string public symbol;
uint8 public decimals;
uint256 public totalSupply;
mapping(address => uint256) private _balances;
mapping(address => mapping(address => uint256)) private _allowances;
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
constructor(
string memory _name,
string memory _symbol,
uint8 _decimals,
uint256 _initialSupply
) {
name = _name;
symbol = _symbol;
decimals = _decimals;
totalSupply = _initialSupply * 10 ** uint256(_decimals);
_balances[msg.sender] = totalSupply;
emit Transfer(address(0), msg.sender, totalSupply);
}
function balanceOf(address account) external view returns (uint256) {
return _balances[account];
}
function transfer(address to, uint256 amount) external returns (bool) {
_transfer(msg.sender, to, amount);
return true;
}
function allowance(address owner, address spender) external view returns (uint256) {
return _allowances[owner][spender];
}
function approve(address spender, uint256 amount) external returns (bool) {
_allowances[msg.sender][spender] = amount;
// Task: Emit Approval event for standard compliance.
emit Approval(msg.sender, spender, amount);
return true;
}
function transferFrom(address from, address to, uint256 amount) external returns (bool) {
uint256 current = _allowances[from][msg.sender];
require(current >= amount, "TRC20: insufficient allowance");
unchecked { _allowances[from][msg.sender] = current - amount; }
_transfer(from, to, amount);
return true;
}
// ── Internal ──────────────────────────────────────────────────────────
function _transfer(address from, address to, uint256 amount) internal {
require(from != address(0), "TRC20: transfer from zero address");
require(to != address(0), "TRC20: transfer to zero address");
uint256 bal = _balances[from];
require(bal >= amount, "TRC20: insufficient balance");
unchecked {
_balances[from] = bal - amount;
_balances[to] += amount;
}
emit Transfer(from, to, amount);
}
}

Deploy with TronBox:

2_deploy_token.js
// Task: Use the TronBox deployer to migrate the TRC-20 token.
const SimpleTRC20 = artifacts.require("SimpleTRC20");
module.exports = function (deployer) {
deployer.deploy(
SimpleTRC20,
"My Token", // name
"MTK", // symbol
6, // decimals (6 matches USDT convention on TRON)
1_000_000 // initial supply
);
};

A standard OpenZeppelin ERC-20 also deploys correctly on TRON as a TRC-20. Review only two things before deploying: every block.timestamp comparison (multiply thresholds by 1000) and how you handle selfdestruct (as deletion behavior has been updated).


Every opcode costs Energy. Writing gas-efficient Solidity for Ethereum produces Energy-efficient contracts on TRON as well — the principles are the same. (For Web2 developers: Optimizing gas usage is like optimizing expensive database queries. On traditional servers, bad queries just slow things down; on blockchains, they directly cost your users real money).

Storage reads and writes are the most expensive operations. Minimize SSTORE calls:

Inefficient.sol
// Task: Avoid multiple SSTORE calls to different slots.
// Inefficient — three separate storage writes
function update(uint256 a, uint256 b, uint256 c) external {
storedA = a;
storedB = b;
storedC = c;
}
Efficient.sol
// Task: Pack values into a single struct to save Energy.
struct Config {
uint128 a;
uint64 b;
uint64 c;
}
Config private config;
function update(uint128 a, uint64 b, uint64 c) external {
config = Config(a, b, c); // single SSTORE to one slot
}

Use view and pure functions for reads. View/pure calls cost no Energy when called off-chain (they are free). Only state-changing calls cost Energy.

Emit events instead of writing to storage for historical data. Events are stored in logs (cheap) rather than contract storage (expensive).

Use calldata instead of memory for function parameters when the data is only read and not modified:

MemoryUsage.sol
// Task: Avoid copying large arrays to memory if only reading.
// Less efficient
function process(uint256[] memory data) external { ... }
CalldataUsage.sol
// Task: Use calldata for read-only parameters to save Energy.
// More efficient — data is not copied to memory
function process(uint256[] calldata data) external { ... }

Verifying your contract source code on TRONSCAN allows users and other developers to read your logic, inspect the ABI, and interact directly through the explorer.

Verification steps

  1. Navigate to your contract address on tronscan.org
  2. Click the Contract tab
  3. Click Verify and Publish
  4. Select the compiler version used by TronBox (must match exactly)
  5. Paste your Solidity source code
  6. Submit — TRONSCAN compiles and verifies the bytecode matches on-chain

For multi-file projects, use the Standard JSON Input method, which accepts the full compiler input including all imported files. TronBox’s build/contracts/*.json output contains the compiler input needed.


For a full breakdown of how Energy and Bandwidth work, the Dynamic Energy Model multiplier, and how to implement fee delegation so your DApp subsidises user costs, see Fee Model & Delegation.

Every transaction that calls a contract specifies a fee limit — the maximum TRX the sender is willing to burn if Energy runs out. If the contract execution exceeds the available Energy and the fee limit is reached, the transaction reverts and the fee limit is partially consumed.

In tronweb, set the fee limit per call:

sdk_call.js
// Task: Specify a fee limit to protect against infinite loops/high costs.
await contract.someMethod().send({
feeLimit: 1_000_000_000, // 1,000 TRX maximum
shouldPollResponse: true,
});

Set the fee limit generously during development and tighten it after you know the actual Energy cost from TRONSCAN transaction history.


Timestamp unit mismatch

All time-based logic must use milliseconds, not seconds. Audit every block.timestamp comparison in migrated contracts.

SELFDESTRUCT behavior

Be aware of the new SELFDESTRUCT behavior (Proposal 106): contracts are no longer deleted unless destroyed in their creation transaction. Adjust your upgrade and cleanup logic accordingly.

Off-chain address computation

If your deployment scripts compute contract addresses off-chain (for CREATE factories), recalculate using TRON’s address derivation scheme.

Energy underestimation

Always test Energy consumption on Nile before mainnet. Underestimating fee limits causes reverts at the worst possible moment.