Skip to content

OpenZeppelin/hardhat-tron

Repository files navigation

@openzeppelin/hardhat-tron

Hardhat plugin for compiling and deploying Solidity contracts to the TRON Virtual Machine.

npm version License: MIT

Write Solidity for TRON the same way you would for any EVM chain. The plugin compiles your contracts with tron-solc (a TVM-targeted fork of solc), runs them against a local TRON node (java-tron, TRE) that it spins up for you in Docker, and exposes the familiar hre.ethers.* surface so unmodified ethers tests work against the TVM.

The public API is documented under src/types.d.ts. Releases follow SemVer; see CHANGELOG.md for what's in each version.

Prerequisites

Before installing, make sure you have:

  1. Node.js 20 or newernode --version
  2. Docker running locally — the plugin spawns a TRON node in a container
  3. A Hardhat projectnpx hardhat init in an empty folder if you don't have one yet

Quick Start

1. Install

npm install --save-dev @openzeppelin/hardhat-tron \
  hardhat \
  @nomicfoundation/hardhat-ethers \
  @nomicfoundation/hardhat-chai-matchers \
  ethers

Peer deps: hardhat ^2.26, @nomicfoundation/hardhat-ethers ^3, @nomicfoundation/hardhat-chai-matchers ^2, ethers ^6.14.

2. Configure Hardhat

Open (or create) hardhat.config.cjs:

require('@nomicfoundation/hardhat-ethers');
require('@nomicfoundation/hardhat-chai-matchers');
require('@openzeppelin/hardhat-tron');

module.exports = {
  solidity: {
    version: '0.8.26',
    settings: {
      optimizer: { enabled: true, runs: 200 },
      evmVersion: 'cancun',
      viaIR: true,
      // Embed source as literal text in metadata so verification
      // services (Sourcify, etc.) can reconstruct it deterministically.
      metadata: { bytecodeHash: 'ipfs', useLiteralContent: true },
    },
  },
  tre: {
    autoStart: true,
    image: 'tronbox/tre:dev',
    compiler: { target: 'tron' },
  },
  defaultNetwork: 'tre',
  networks: {
    tre: {
      url: process.env.TRE_URL || 'http://127.0.0.1:9090/jsonrpc',
      tron: true,
      accounts: [process.env.TRE_PRIVATE_KEY || '0xdd23ca549a97cb330b011aebb674730df8b14acaee42d211ab45692699ab8ba5'],
    },
  },
};

The default key shown above is a well-known TRE dev key — fine for local tests, never use it on a real network.

3. Compile

npx hardhat compile

The first run fetches tron-solc (a few MB), verifies its SHA-256 against the canonical tronprotocol/solc-bin manifest, and writes artifacts to artifacts/. Subsequent runs use the cache.

4. Run your tests

npx hardhat test

The plugin pulls tronbox/tre:dev on first use, starts a TRE container, waits for the node to be ready, runs your Mocha tests, then stops and removes the container.

Writing a test

Your existing ethers-based tests work as-is:

// test/Counter.test.js
const { expect } = require('chai');
const { ethers } = require('hardhat');

describe('Counter', () => {
  it('increments', async () => {
    const counter = await ethers.deployContract('Counter');
    await counter.increment();
    expect(await counter.value()).to.equal(1n);
  });
});

For TRON-specific operations (time-warp, balance manipulation, raw TronWeb access), the plugin exposes hre.tre:

const { tronWeb, address } = hre.tre.makeTronWeb();
const counter = await hre.tre.deployContract('Counter');
await hre.tre.mine(tronWeb); // tre_mine cheatcode
await hre.tre.setBlockTime(tronWeb, 0); // instamine
await hre.tre.setAccountBalance(tronWeb, addr, 10n ** 18n);

Troubleshooting

"Cannot connect to the Docker daemon" — Docker isn't running. Start it and try again.

Tests hang on startup — first-time image pull can take a minute or two. Run docker pull tronbox/tre:dev manually to see progress.

Port 9090 already in use — set tre.port to a free port in your config (and update the network url to match).

Want to manage the container yourself? Set tre.autoStart: false and start TRE manually with docker run --rm -p 9090:9090 tronbox/tre:dev.


Compile

Loading the plugin redirects Hardhat's Solidity compile pipeline at tron-solc — a TVM-targeted fork of solc published on tronprotocol/solc-bin. No code changes are required in the consumer; just add the plugin to hardhat.config.cjs:

require('@openzeppelin/hardhat-tron');

module.exports = {
  solidity: {
    version: '0.8.26',
    settings: {
      /* … */
    },
  },
  tre: {
    compiler: {
      // 'tron' (default): always compile with tron-solc.
      // 'tron-when-network-tron': only on tron-typed networks.
      target: 'tron',
      // Optional glob filters. Empty arrays = no restriction.
      include: ['contracts/**/*.sol'],
      exclude: ['contracts/mocks/**'],
      // Route artifacts to artifacts-tron/ instead of artifacts/.
      separateArtifacts: false,
    },
  },
};

The compiler wasm is fetched on demand from the canonical mirror (https://raw.githubusercontent.com/tronprotocol/solc-bin/main), SHA-256-verified against the manifest, and cached under Hardhat's standard compilers-v2/ directory. Override the mirror via tre.compiler.mirror.

Batched compiles

tron-solc@0.8.26 hits a wasm linear-memory ceiling on large source trees (empirically between ~90 and ~230 files, depending on optimizer settings and constructor complexity). For projects above that ceiling, the plugin exposes a tron:compile-batches task that compiles in passes, accumulating artifacts and reusing the compile cache so shared imports compile only once:

npx hardhat tron:compile-batches

Each batch is declared either inline or via a separate file:

tre: {
  compiler: {
    batches: [
      { name: '01-utils',  dirs: ['contracts/utils'] },
      { name: '02-access', dirs: ['contracts/access'] },
      { name: '03-token',  dirs: ['contracts/token'], extraLeaves: ['SafeERC20'] },
    ],
    // Or, equivalently:
    batchesPath: './tron-batches.config.cjs',
  },
}

Each batch entry supports:

Field Type Effect
name string (required) Display label and progress key
include string[] (optional) Raw globs, used as-is — skips the dirs/extraLeaves expansion
dirs string[] (optional) Expanded to ${dir}/**/*.sol. Each contracts/<x> automatically pairs with contracts/exposed/<x> for hardhat-exposed consumers
extraLeaves string[] (optional) Bare basenames, expanded to **/<name>.sol for scattered top-level files

The task wipes paths.artifacts and paths.cache once at the start so the run is reproducible, then mutates tre.compiler.include between passes — the source-resolver subtask re-reads this on every invocation, so each compile sees only the current batch's source set.

Auto-up: TRE lifecycle on hardhat test

When a Hardhat network is declared tron: true and tre.autoStart is left at its default (true), npx hardhat test spawns a TRE container before tests run and tears it down on exit:

module.exports = {
  networks: {
    tre: {
      url: 'http://127.0.0.1:9090/jsonrpc',
      tron: true,
      accounts: [process.env.TRE_PRIVATE_KEY],
    },
  },
  tre: {
    image: 'tronbox/tre:dev', // default
    port: 9090, // default; host side, container exposes 9090
    jarPath: './tre/FullNode.jar', // optional patched-jar bind mount
    autoStart: true, // default — master switch
    autoStartOnTest: true, // default — per-task gate
    keepRunning: false, // default — set true to skip teardown
    readinessTimeoutMs: 60_000, // default — wait-for-ready budget
  },
};

The lifecycle wrapper skips spawning when:

  • tre.autoStart is false,
  • the per-task gate is false (autoStartOnTest / autoStartOnCompile / autoStartOnNode),
  • the selected network does not have tron: true, or
  • something is already responding on the network's URL (manual docker-compose up -d, a teammate's existing container, or a previous run left up by keepRunning).

Teardown is skipped if keepRunning is true OR if a pre-existing container was reused — the plugin never tears down a container it didn't spawn.

Per-task gates

The three Hardhat tasks the plugin can auto-spawn for behave slightly differently:

Task Gate Default Teardown
hardhat test autoStartOnTest true Yes (unless keepRunning)
hardhat compile autoStartOnCompile false Yes (unless keepRunning)
hardhat node autoStartOnNode true Nonode is long-running; user owns cleanup

hardhat compile is opt-in because tron-solc compiles wasm-locally and the compile pipeline itself doesn't need a running container; spinning a 1–2 GB Java container for a 30-second compile is rarely the right trade. Flip the gate on if your task graph initialises the network connection at compile time (some plugins probe chainId, perform network-typed config validation, etc.).

hardhat node never auto-tears-down — the container has to outlive the node process for the dev RPC to be useful. Clean up with docker rm -f <container-name>; the wrapper logs the name on spawn so it's one copy-paste away.

Notes

The plugin installs a global HTTP keep-alive agent at module load. This eliminates the per-request TCP handshake overhead on the thousands of /wallet/* round-trips a typical test suite makes against the local java-tron container. See src/runtime/http-agent.js for the rationale and tuning notes.

Patched FullNode.jar

The cheatcodes that mutate VM state — setAccountBalance, setAccountCode, setAccountStorageAt, unlockAccounts, snapshot, revert, plus instant time-warps via setNextBlockTimestamp — call tre_* JSON-RPC methods that stock java-tron does not implement. They live on a custom /tre endpoint served by a patched FullNode.jar built from a minor java-tron fork. Each call returns { supported, ... }, so tests degrade cleanly when running against a stock node.

Building the patched jar

The Java sources live under docker/src/ and the build pipeline is a self-contained bash script:

bash docker/build-jar.sh

What it does:

  1. Spins a temporary tronbox/tre:dev container.
  2. Uses its bundled OpenJDK 8 toolchain to compile docker/src/**/*.java against the image's unpatched FullNode.jar (used as the classpath).
  3. Repacks the upstream jar with the patched .class files overlaid (a small Python ZIP step handles top-level classes plus their nested inner classes).
  4. Pulls the resulting jar back to the host as tre/FullNode.jar. The temp container is torn down on exit.

Output is gitignored — it's reproducible from docker/src/ and a 100+ MB jar isn't worth committing.

Files touched by the patch

File Effect
org/tron/core/services/jsonrpc/tre/TreJsonRpc.java New JSON-RPC interface declaring the tre_* methods
org/tron/core/services/jsonrpc/tre/TreJsonRpcImpl.java Implementations — direct AccountStore / CodeStore / ContractStore writes, snapshot/revert via LinkedHashMap, version probe returns v1.0.4-oz-tron
org/tron/core/services/jsonrpc/tre/TreImpersonationRegistry.java Per-process whitelist of base58 addresses that bypass ECRecover in validateSignature
org/tron/core/capsule/TransactionCapsule.java Patched: inline check against TreImpersonationRegistry in validateSignature so txs with a whitelisted owner_address skip ECRecover + permission/weight
org/tron/consensus/dpos/DposSlot.java Patched: one-shot timestamp override hooks so tre_setNextBlockTimestamp can fast-forward chain time without 1:1 wall-clock waits

Wiring the jar into Hardhat

Once built, point the lifecycle wrapper at it with tre.jarPath:

tre: {
  jarPath: './tre/FullNode.jar',
},

The lifecycle wrapper bind-mounts the file over the image's stock jar (/tron/FullNode/FullNode.jar:ro) so the container starts already-patched. The runtime probes (src/runtime/time.js, snapshot stack in src/runtime/ethers-bridge.js) detect the patched jar via tre_version returning the -oz-tron suffix.

Signer rebalancing between test files

Tests draw from a pool of 10 deterministically-derived signers (Hardhat's well-known test test test … junk mnemonic, HD path m/44'/60'/0'/0/i). The deployer is index 0; the remaining nine are funded at first use via tre_setAccountBalance — a direct AccountStore write that sidesteps the witness budget capping the docker-compose pre-funding.

Long test runs spend down those balances. The @openzeppelin/hardhat-tron/signers subpath exposes refundSigners(hre), which resets every cached signer back to its initial balance. Hooking it into a Mocha afterEach in test helpers keeps later files from inheriting depleted state from earlier ones. The call is idempotent (tre_setAccountBalance is a direct write, not a transfer) and parallelized — typical cost is one ~15 ms round-trip per fixture.

License

MIT

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors