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 machine | EVM (Solidity, Vyper) | TVM (Solidity; largely EVM-compatible — see TVM vs EVM) |
| Transaction fee model | Gas paid in ETH; market-priced | Bandwidth + Energy; both can be obtained by staking TRX |
| Native token | ETH (18 decimals) | TRX (6 decimals) |
| Address format | Hex (0x…, 20 bytes) | Base58Check (T…, 21 bytes including the 0x41 prefix) |
| Compiler | upstream solc | TRON solc |
| Block explorer | Etherscan | TRONSCAN |
| JS SDK | ethers.js / web3.js | TronWeb |
| Java SDK | Web3j | Trident |
| Python SDK | web3.py | TronPy |
| Project framework | Truffle / Hardhat / Foundry | TronBox |
| Browser IDE | Remix | TronIDE |
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 versionUse 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 precompile | EVM 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
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):
| Endpoint | Faucet | |
|---|---|---|
| Shasta | https://api.shasta.trongrid.io | Shasta faucet |
| Mainnet | https://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:TransferContract(system contract, sent viawallet/createtransaction) — bypasses the contract'sfallback/receivefunction entirely. The TVM is not invoked.TriggerSmartContractwithcallValue— runs the contract's payable function orfallbacklike a normal EVM call.
On Ethereum, any value transfer to a contract triggers its
receive/fallback. If your contract relies onfallbackrunning on every incoming transfer (deposit tracking, event emission, internal accounting), audit the call paths: incoming TRX from a plainTransferContractwill be invisible to your contract code. Patterns that work on Ethereum but break on TRON include deposit detection byfallbackevent, balance-difference-based reentrancy guards infallback, and reflexive ERC-20 contracts that rebase on incoming ETH. -
Address encoding mismatches. TRON addresses are 21 bytes (with the
0x41prefix), 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. -
CHAINIDis not a static integer on TRON. If your contract usesblock.chainidfor EIP-712 domain separators or cross-chain replay protection, the value at runtime is the last 4 bytes of the genesis block hash — not1, not728126428, and not any other constant. Read it from the chain rather than hardcoding. See TVM vs EVM —CHAINIDsemantics. -
SELFDESTRUCTdoes not delete existing contracts. When the self-destruct-restriction gate is on (the current Mainnet state),SELFDESTRUCTon 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 onSELFDESTRUCTas 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_factorrises and per-call Energy cost increases. See FeeLimit & Energy cost.
Related resources
- TVM vs EVM — full reference for opcode and precompile differences
- Solidity on TRON — TRON-specific Solidity language extensions
- Migrating a Truffle project to TronBox — project-level migration walkthrough
- Quickstart — TronBox deploy walkthrough
- Contract verification — verify the migrated contract on TRONSCAN
- FeeLimit & Energy cost — calibrate
fee_limitfor the new cost profile
Updated 15 days ago