Migrating Ethereum contracts to TRON

Step-by-step walkthrough for porting an existing Ethereum (Solidity) contract to TRON: recompile, swap SDKs, switch testnets, and verify.

📘

Prerequisites

If you have an existing Solidity contract that runs on Ethereum (or any EVM-compatible chain), this page is the practical walkthrough for porting it to TRON. It complements the reference page TVM vs EVM — go there for the complete list of opcode and precompile differences. This page focuses on the steps you take rather than enumerating every difference.

For project-level migration of a Truffle/Hardhat repo (directory layout, network config, test framework), see Migrating a Truffle project to TronBox.

At a glance: what changes

Ethereum (EVM)TRON (TVM)
Virtual machineEVM (Solidity, Vyper)TVM (Solidity; largely EVM-compatible — see TVM vs EVM)
Transaction fee modelGas paid in ETH; market-pricedBandwidth + Energy; both can be obtained by staking TRX
Native tokenETH (18 decimals)TRX (6 decimals)
Address formatHex (0x…, 20 bytes)Base58Check (T…, 21 bytes including the 0x41 prefix)
Compilerupstream solcTRON solc
Block explorerEtherscanTRONSCAN
JS SDKethers.js / web3.jsTronWeb
Java SDKWeb3jTrident
Python SDKweb3.pyTronPy
Project frameworkTruffle / Hardhat / FoundryTronBox
Browser IDERemixTronIDE

Step 1 — Recompile with the TRON solc

TRON's Solidity compiler is a fork of upstream solc with TRON-specific extensions (trcToken, transferToken, the trx/sun unit keywords). Most Ethereum contracts compile unchanged, but you must use the TRON distribution.

If you use TronBox (recommended), TRON solc is bundled. Set compilers.solc.version in tronbox-config.js to your contract's pragma version:

compilers: {
  solc: {
    version: '0.8.6'
  }
}

If you build solc yourself or use TronIDE, take the TRON distribution from github.com/tronprotocol/solidity.

🚧

Pin the compiler version

Use a fixed pragma solidity X.Y.Z; (no caret). This avoids subtle behavior changes between minor compiler releases. See Solidity on TRON.

Step 2 — Audit your contract for opcode-level differences

A handful of opcodes behave differently on TVM. Most contracts are not affected — but if your code touches one of these, you need to fix it before migrating:

If your contract uses...Action
block.difficulty (DIFFICULTY opcode)Returns 0 on TVM. Replace with a different randomness source or use it only as a tiebreaker.
block.gaslimit (GASLIMIT)Returns 0 on TVM. Remove the dependency.
tx.gasprice (GASPRICE)Returns energyPrice on TVM (currently 100 sun). Adjust any cost-prediction logic.
block.basefee (BASEFEE)Returns energyPrice. EIP-1559 is not implemented; remove fee-bumping logic.
Ripemd160 precompile (0x03)TVM applies SHA-256 twice, EVM once. If you depend on EVM's behavior, recompute hashes off-chain.
0x09 precompileEVM is Blake2F; TVM is BatchValidateSign. Different functionality entirely — your call will fail. Remove or rewrite.

For the full opcode table, see TVM vs EVM.

Step 3 — Handle CREATE2 carefully

The TVM uses 0x41 as the CREATE2 prefix; the EVM uses 0xff. There are two cases to consider.

Case A: you use Solidity's new {salt: ...} syntax. No code change needed. The TRON solc emulates the EVM behavior internally and your high-level Solidity code stays portable:

pragma solidity 0.8.20;

contract Token1 {
    uint256 public value;
    constructor(uint256 _value) {
        value = _value;
    }
}

contract Factory {
    event ContractCreated(address);

    function createContract(bytes32 _salt, uint256 _x) external {
        Token1 newToken = new Token1{salt: _salt}(_x);
        emit ContractCreated(address(newToken));
    }
}

Case B: you compute the CREATE2 address explicitly (for example, copying OpenZeppelin's Create2.computeAddress or doing the keccak yourself). You must change 0xff to 0x41 in the prefix byte:

function computeAddress(bytes32 salt, bytes32 bytecodeHash, address deployer)
    internal pure returns (address addr)
{
    assembly {
        let ptr := mload(0x40)
        mstore(add(ptr, 0x40), bytecodeHash)
        mstore(add(ptr, 0x20), salt)
        mstore(ptr, deployer)
        let start := add(ptr, 0x0b)
        mstore8(start, 0x41)  // <-- 0x41 on TRON, 0xff on Ethereum
        addr := keccak256(start, 85)
    }
}

If you use OpenZeppelin's library, you'll need a TRON-specific fork or a wrapping helper that overrides this byte.

Step 4 — Adjust native-token decimal precision

The smallest unit of ETH is wei (10⁻¹⁸ ETH); the smallest unit of TRX is sun (10⁻⁶ TRX). If your contract does any arithmetic on native-token base units — for example, comparing balances against hard-coded constants — you'll need to rescale.

Code that uses 1 ether or 1e18 to represent 1 ETH should change to 1 trx or 1e6 to represent 1 TRX. Code that scales prices by 10^18 should scale by 10^6 instead.

The TRON Solidity compiler accepts trx and sun as unit literals — see Solidity on TRON.

Step 5 — Swap SDKs in your off-chain code

In Ethereum codeChange to
web3.js / ethers.jsTronWeb
Web3j (Java)Trident
web3.pyTronPy
Hardhat tests using ethersTronBox tests using TronWeb

The function-call shapes are similar (each SDK has a contract(abi).at(address) style), but the network endpoint, address format, and signing flow differ. See Interacting with contracts for the TronWeb call patterns.

Step 6 — Switch testnets

Replace your Goerli/Sepolia endpoint with Shasta (TRON's primary public testnet):

EndpointFaucet
Shastahttps://api.shasta.trongrid.ioShasta faucet
Mainnethttps://api.trongrid.io(no faucet — buy TRX)

For TronBox specifically, set the shasta network in tronbox-config.js:

networks: {
  shasta: {
    privateKey: process.env.PRIVATE_KEY,
    userFeePercentage: 50,
    feeLimit: 100 * 1e6,                     // 100 TRX
    fullHost: 'https://api.shasta.trongrid.io',
    network_id: '2'
  }
}

See Quickstart for a full example.

Step 7 — Deploy and verify

Deploy with TronBox (tronbox migrate --network shasta) or via TronIDE — see Deploying. Confirm the deployment landed on-chain through wallet/gettransactioninfobyid.

Once deployed on Mainnet, verify the source on TRONSCAN so users can audit the contract. The verification flow on TRONSCAN is similar to Etherscan's — see Contract verification.

Common migration gotchas

  • TRX transfers don't always run fallback. TRX can reach a contract through two distinct protocol-level mechanisms, and they have different semantics for the receiving contract:

    1. TransferContract (system contract, sent via wallet/createtransaction) — bypasses the contract's fallback / receive function entirely. The TVM is not invoked.
    2. TriggerSmartContract with callValue — runs the contract's payable function or fallback like a normal EVM call.

    On Ethereum, any value transfer to a contract triggers its receive / fallback. If your contract relies on fallback running on every incoming transfer (deposit tracking, event emission, internal accounting), audit the call paths: incoming TRX from a plain TransferContract will be invisible to your contract code. Patterns that work on Ethereum but break on TRON include deposit detection by fallback event, balance-difference-based reentrancy guards in fallback, and reflexive ERC-20 contracts that rebase on incoming ETH.

  • Address encoding mismatches. TRON addresses are 21 bytes (with the 0x41 prefix), not 20 — at the chain boundary. Inside the TVM, addresses are 20 bytes, identical to Ethereum (see TVM vs EVM — Address representation). When converting between TRON and Ethereum representations outside the VM (RPC payloads, signed orders bridged between chains, off-chain address derivation), strip or add the prefix byte and Base58Check-encode/decode.

  • CHAINID is not a static integer on TRON. If your contract uses block.chainid for EIP-712 domain separators or cross-chain replay protection, the value at runtime is the last 4 bytes of the genesis block hash — not 1, not 728126428, and not any other constant. Read it from the chain rather than hardcoding. See TVM vs EVM — CHAINID semantics.

  • SELFDESTRUCT does not delete existing contracts. When the self-destruct-restriction gate is on (the current Mainnet state), SELFDESTRUCT on a contract that was not created in the current transaction transfers balance, TRC-10 holdings, and stake-for-self to the obtainer but does not clear code or storage — same shape as Ethereum's EIP-6780. The operation additionally reverts when the contract has active Stake 1.0 in lock period, delegated resources (Stake 1.0 or 2.0), or pending Stake 2.0 unfreezes. Do not rely on SELFDESTRUCT as an upgrade primitive. See TVM vs EVM — SELFDESTRUCT.

  • Energy estimation needs production-shape inputs. A function that costs 50,000 Energy on a small dataset can cost 500,000 Energy at production scale. Estimate with realistic inputs through wallet/triggerconstantcontract.

  • The Dynamic Energy Model affects popular contracts. If your contract is called frequently, its energy_factor rises and per-call Energy cost increases. See FeeLimit & Energy cost.


Related resources