This project showcases a minimal transparent upgradeable proxy TRC20 contract setup on the Tron network using TronWeb, TronBox, and OpenZeppelin-inspired upgradeable contracts, alongside an exploration of the UUPS (Universal Upgradeable Proxy Standard) approach. Below is an outline of each file, what it does, and how to use them.
-
ImplementationV1.sol
- First version of our token-like contract.
- Inherits
Initializable
,ERC20Upgradeable
, andOwnableUpgradeable (v5)
. - Defines
initialize(...)
which mints an initial supply and sets the owner via__Ownable_init(initialOwner_)
.
-
ImplementationV2.sol
- Second version of the token contract, preserving V1’s storage layout in the exact same order.
- Introduces
_newValue
and_initializedV2
, plus functionsinitializeV2()
,setNewValue()
, etc. - Does not re-initialize
OwnableUpgradeable
to avoid overwriting the existing owner.
-
ProxyAdmin.sol
- Minimal admin contract for
TransparentUpgradeableProxy
. - Only the
owner()
ofProxyAdmin
can callupgrade(proxy, newImplementation)
. - Also allows the admin to call arbitrary functions on the proxy (e.g.
initializeV2()
) and transfer ownership of theProxyAdmin
itself.
- Minimal admin contract for
-
TransparentUpgradeableProxy.sol
- Stores the implementation and admin addresses in EIP1967-defined slots.
- Forwards all user calls (except those from the admin to
upgradeTo(...)
) to the implementation viadelegatecall
. - Ensures only the admin can perform upgrades, while normal users interact with token functions through the proxy address.
- This contract's address acts as a permanent entry point for users, even if the underlying implementation is upgraded, making it the primary address that end users interact with.
-
1_initial_migration.js
- Standard TronBox migration that deploys the
Migrations
contract.
- Standard TronBox migration that deploys the
-
2_deploy_upgradable_token.js
- Deploys and configures the entire upgradeable system:
- Deploys
ProxyAdmin
. - Deploys
ImplementationV1
. - Deploys
TransparentUpgradeableProxy
pointing toImplementationV1
and controlled byProxyAdmin
. - Calls
ImplementationV1.initialize(...)
via the proxy to set up initial token params (name, symbol, supply, owner). - Deploys
ImplementationV2
. - (Optional) Immediately upgrades the proxy to
ImplementationV2
.
- Deploys
- Deploys and configures the entire upgradeable system:
- Direct Calls (Unlikely) to
ImplementationV1
andImplementationV2
to show they each have their own internal states (not used by proxy). - Interact with the Proxy as V1 to read
name()
,symbol()
,totalSupply()
, transfer tokens, etc. - Upgrade to V2 via
ProxyAdmin.upgrade(...)
. - Initialize V2 by calling
initializeV2()
throughProxyAdmin.callProxy(...)
or directly. - Interact with Proxy as V2 (use
getNewValue()
,setNewValue()
, confirm old V1 data remains). - Transfer Ownership of the proxied contract to a new owner, ensuring only that new owner can call
onlyOwner
functions. - Transfer Ownership of
ProxyAdmin
so the new admin can still upgrade the proxy. - Demonstrate the old owner cannot
setSomeValue()
anymore. - Upgrade back to V1 as the new owner, showing that the storage state is preserved.
Using Docker for Local Development and Testing: Pull the Docker Image (do this only once):
docker pull tronbox/tre
Run the Container:
docker run -it -p 9090:9090 --rm --name tron tronbox/tre
Clone the Repository:
git clone https://github.com/aziz1975/tron-upgradeable.git
cd tron-upgradeable
npm install
cp .env.sample .env
Important Note: Update the .env file with your private key, Tron node host, and deployed contract addresses etc so they are properly used by tronbox.js, 2_deploy_upgradable_token.js, and TestContracts.js.
Contract Setup
Before compiling & deploying, a few updates need to be done:
- Modify your PRIVATE_KEY & FULL_NODE constants depending on your network preferences.
- Modify the 2_deploy_upgradable_token.js script with your own token information (line 56)
- 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, Comment code as needed)
Compile and Deploy:
npx tronbox compile
npx tronbox migrate --network development // Deploy contract in your development network
npx tronbox migrate --network nile // Deploy your contract directly to nile testnet
After deployment, you will see addresses of the deployed contracts:
- ProxyAdmin at some Tron address
- ImplementationV1 at some Tron address
- TransparentUpgradeableProxy at some Tron address
- ImplementationV2 at some Tron address
Copy those addresses and update the .env file accordingly. Test Script: The test script is located in the tests folder.
cd tests
npm install
node TestContracts.js
Important Note: It's recommended not to run the whole test suite at once in the Nile or Shasta environment. Doing so can generate inconsistent outputs and unnecessarily consume resources. Run only the sections of code you actually need to test.
When using upgradeable contracts, storage layout must remain consistent across versions to avoid corrupting stored data. In ImplementationV2, the original state variables from ImplementationV1 appear in the same order at the top.
The ProxyAdmin is a critical role that can change the underlying implementation. Always safeguard your admin keys. For production, consider using multi-signature or a DAO governance mechanism.
- 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.
- The proxy (TransparentUpgradeableProxy) 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.
- ProxyAdmin
- A non-upgradeable admin contract.
- Has an owner (inherited from Ownable).
- Can call upgrade(proxy, newImplementation) on the proxy to change the implementation.
- Can also call other admin functions (e.g., callProxy(...)) if needed.
- TransparentUpgradeableProxy
- A minimal EIP1967 proxy.
- Stores two critical addresses in EIP1967 storage slots: _IMPLEMENTATION_SLOT: Points to the current implementation contract (V1 or V2). _ADMIN_SLOT: Points to the ProxyAdmin contract.
- All user interactions go through the proxy’s fallback() or receive() function, which delegatecalls the logic in the current implementation.
- Only its admin (i.e., the ProxyAdmin) can invoke upgradeTo(...).
- ImplementationV1 / ImplementationV2
- Actual logic (TRC20 + custom code).
- Use OwnableUpgradeable for ownership checks (like onlyOwner).
- Deployed once each as separate contracts, but they do not hold user balances directly in their own storage—rather, the proxy uses their storage layout.
- The “initialize” function(s) (initialize(...), initializeV2()) set up the state behind the proxy.
- ImplementationV2 inherits from the same storage layout as V1, ensuring no collisions.
- Users / External Calls
- When a user calls a function like transfer(...), they actually call it on the proxy address.
- The proxy delegates to the implementation contract code.
- Storage updates happen in the proxy’s storage (EIP1967).