Building Upgradable TRON Smart Contracts: Implementing the Transparent Proxy Pattern
In a traditional (non-upgradeable) smart contract, once your contract is deployed, you cannot change its code (logic). If you discover bugs or want to add new features, you must redeploy a new contract. This approach can be problematic if you have a live token or a DeFi project with locked value — migrating user balances and states can be extremely challenging.
Upgradeable contracts solve this by separating the contract’s logic (Implementation) from its storage (Proxy). When you want to upgrade, you only replace the logic contract while retaining the same state and address for the proxy. Users interact with the proxy address, which internally delegates all calls to the chosen implementation contract.
In this article, we’ll demonstrate how to build upgradeable TRON smart contracts inspired by OpenZeppelin’s EIP1967 approach.
Project Structure
Below is a common folder structure (simplified for demonstration) you might see in a TronBox project:
contracts: Holds all Solidity contracts, including:
- ImplementationV1 and ImplementationV2 — Your token implementations (logic).
Note: ImplementationV2 is only applicable when you develop new implementation logic. It will be showcased through the examples for reference, but it is not needed during the first deployment of a proxy contract.
- ProxyAdmin — A contract that manages upgrades.
- TransparentUpgradeableProxy — A minimal proxy that delegates calls to the implementation.
- migrations: Holds JavaScript migration scripts, including 2_deploy_upgradable_token.js which handles deployment.
- tronbox.js: TronBox configuration file.
- .env: Stores sensitive info like private keys and node URLs.
Key Contracts
A) ImplementationV1
ImplementationV1 is our initial token logic. We are using OpenZeppelin Upgradeable contracts (v5 for Ownable). We must ensure the storage layout remains consistent in future versions.
Important parts:
- Inherits from ERC20Upgradeable, OwnableUpgradeable, and Initializable.
- Has an initialize function (instead of a constructor), which will be called once via the proxy.
- Initializes name, symbol, initial supply, and calls __Ownable_init(initialOwner_).
- Mints tokens to the initial owner.
- Provides some extra variable _someValue to illustrate storing and retrieving data.
B) ImplementationV2
ImplementationV2 illustrates a new version of the token logic. It preserves the storage layout from V1 in the exact same order. After that, we add new variables.
Key parts:
- Copies V1’s variables in the same order.
- Adds a _newValue and _initializedV2.
- Contains a initializeV2 function with the reinitializer(2) modifier, so it runs only once.
C) ProxyAdmin
ProxyAdmin is an admin contract that manages upgrades for our TransparentUpgradeableProxy. It can:
- upgrade the proxy to a new implementation.
- call arbitrary functions on the proxy (e.g., if we need to run a new initializer).
- transfer ownership of itself so a new address can control upgrades.
Important: Only the admin can call upgradeTo on the proxy.
D) TransparentUpgradeableProxy
A minimal EIP-1967 style transparent proxy:
- Stores the implementation address and the admin address in specific EIP-1967 slots.
- fallback() and receive() forward all calls to the implementation via delegatecall.
- Only the admin can call upgradeTo().
Deployment Script (2_deploy_upgradable_token.js)
The migration script 2_deploy_upgradable_token.js does the following:
- Creates a TronWeb instance (using the same private key as TronBox).
- Deploys ProxyAdmin.
- Deploys ImplementationV1.
- Deploys TransparentUpgradeableProxy, passing:
- implV1.address as the logic contract
- proxyAdmin.address as admin
- 0x for _data (no direct initialization in constructor)
- Initializes ImplementationV1 by calling initialize(…) through the proxy.
- Optionally performs an immediate upgrade to V2 (commented out by default).
Step by Step Instructions to Develop:
Note: Use the following as a reference. For the actual code implementation, updated README, and the latest updates, please check the github repo.
- Install TronBox
npm install -g tronbox
Make sure you have Node.js installed. TronBox will help compile, migrate, and test your Tron smart contracts.
- Initialize a New Project
mkdir tron-upgradeable
cd tron-upgradeable
tronbox init
cp .env.sample .env
This sets up basic folders (contracts, migrations, etc.). You can also manually create the folders if you prefer.
- Configure your .env File
Update your .env in your project root to store sensitive data (e.g., private key). For example:
PRIVATE_KEY_NILE=”YOUR_NILE_TESTNET_PRIVATE_KEY”
FULL_NODE_NILE=”https://nile.trongrid.io"
Warning!: Never commit .env file to version control!
- Configure tronbox.js
Edit tronbox.js to use your environment variables:
- Add the Contracts
Inside contracts/, create four files:
- ImplementationV1.sol
- ImplementationV2.sol (Applicable for subsequent implementations only)
- ProxyAdmin.sol
- TransparentUpgradeableProxy.sol
- Write the Migration Script (Excerpt)
// Excerpt from 2_deploy_upgradable_token.js:
// …
await deployer.deploy(ImplementationV1);
const implV1 = await ImplementationV1.deployed();
await deployer.deploy(
TransparentUpgradeableProxy,
implV1.address,
proxyAdmin.address,
“0x”
);
const proxy = await TransparentUpgradeableProxy.deployed();
// Initialize ImplementationV1 through the proxy:
const proxiedV1 = await ImplementationV1.at(proxy.address);
await proxiedV1.initialize(“MyToken”, “MTK”, 1000, deployerBase58);
await deployer.deploy(ImplementationV2);
const implV2 = await ImplementationV2.deployed();
// (Optional) upgrade to V2:
// await proxyAdmin.upgrade(proxy.address, implV2.address);
// …
- Configure Upgradeable token and Networks
Assuming you’ve already cloned the GitHub repository, make sure to follow these steps before deploying your contracts:
- Make sure to update your .env variables
- Update Token information as needed in 2_deploy_upgradable_token.js
- Configure your PRIVATE_KEY and FULL_NODE constants as needed in 2_deploy_upgradable_token.js
- * Make sure to run steps A to E form the 2_deploy_upgradable_token.js when deploying your contract for the first time (Comment code as needed)
- * When upgrading implementation contract make sure to run ONLY steps F & G (optionally if you want to upgrade after deploying v2,v3, etc)
- Compile and Deploy
npm install //Install openzeppelin & dotenv deps, etc.
npx tronbox compile
npx tronbox migrate — network development //for development test network
npx tronbox migrate –network nile //to deploy directly in the nile testnet
This uses the compiler version and optimization settings from tronbox.js. Make sure you have sufficient TRX or Energy in your account to deploy to Nile/Shasta.
- Verify Deployment
- Check the console logs for contract addresses.
- Your TRC20 token is at the proxy (TransparentUpgradeableProxy) address.
- Call name(), symbol(), etc., on that address to confirm.
- Interact With the Token
- Use TronWeb to interact with the proxied token.
- For example, proxiedV1.transfer(…), proxiedV1.balanceOf(…), etc.
- Test & Upgrade to a New Implementation
- First, ensure you have the new implementation deployed (e.g., ImplementationV1,V2, VN…).
- Use ProxyAdmin to upgrade the proxy:
// Snippet for upgrading:
await proxyAdmin.upgrade(proxy.address, implV2.address);
console.log(“Upgraded to ImplementationV2!”);
// Now, the proxy delegates calls to ImplementationV2 instead of V1.
- If V2 requires a new initializer, call it through the proxy:
const proxiedV2 = await ImplementationV2.at(proxy.address);
await proxiedV2.initializeV2();
console.log(“V2 initialized:”, await proxiedV2.isV2Initialized());
- Verify new functions (e.g., getNewValue()) are accessible.
How ProxyAdmin Is Connected to TransparentUpgradeableProxy:
- The TransparentUpgradeableProxy has an admin slot. The address in that slot is allowed to invoke the proxy’s upgradeTo(…) function.
- During deployment, you set the admin of the TransparentUpgradeableProxy to be the ProxyAdmin contract’s address.
- Because of that, only the ProxyAdmin contract can successfully call:
proxyAddress.call(abi.encodeWithSignature(“upgradeTo(address)”, newImplementation))
…which changes the implementation reference inside the proxy.
Essentially:
- TransparentUpgradeableProxy stores the admin (which is the ProxyAdmin address).
- ProxyAdmin is the “boss” that can upgrade the proxy to new logic.
How TransparentUpgradeableProxy Is Connected to ImplementationV1/ImplementationV2
- The proxy also has an implementation slot that points to the current logic (e.g., ImplementationV1 or ImplementationV2).
- When non-admin users call the proxy, the call goes into the fallback function, which delegates to the current implementation.
- After an upgrade, that implementation slot is changed to the new implementation address (e.g., from V1 to V2).
In short:
- TransparentUpgradeableProxy holds a reference to “the current Implementation contract.”
- If you upgrade via ProxyAdmin.upgrade(…), the slot is updated from the old logic (V1) to the new logic (V2).
- The same proxy address is used; only the underlying logic changes.
By following these steps, you’ve successfully deployed an Upgradeable TRC20 Token on Tron using TronBox. This architecture allows you to upgrade your token logic without losing the existing state (balances, ownership, etc.). The Transparent Proxy pattern ensures a clean separation of concerns — users continue to interact with the same token address, while you maintain the ability to upgrade the logic in the future (under the control of the ProxyAdmin).
Happy building on Tron!
Key Takeaways:
- Always keep the storage layout consistent between versions.
- Use initializer functions instead of constructors.
- ProxyAdmin is your best friend for safe upgrades.
- Thoroughly test upgrades on a testnet (like Nile) before moving to production.
If you’d like to examine or clone the final code, you can visit our GitHub repository.