How to create a Transparent Proxy
The proxy pattern is a smart contract design pattern used to make the smart contracts upgradeable. Note that smart contracts by themselves are immutable and some advanced solidity foot work is necessary to make them upgradeable. For a detailed overview of smart contract upgradeability and explanation of the transparent proxy patten please read this earlier article. In this article we will focus on the actually deploying your contract with the transparent proxy pattern.
This Github repository has the code for this article feel free to save it for future use. You can clone it if you want to follow along without setting up from scratch.
Project Set Up
We will be using Hardhat for our Ethereum development workflow. I am using WSL2 in a windows machine for this walkthrough. You can adjust the steps slightly in case you are using a different set up. Let’s get started!
Run the following commands to create a directory and initialize it as a node project.
mkdir hardhat-upgrades && cd hardhat-upgradesnpm init -y
Install hardhat in this directory.
npm i --save-dev hardhat
Initialize a hardhat type script project in the directory. Run the below command and choose “Create a TypeScript project” option. Go with the default parameters for rest of the prompts.
npx hardhat
Install the Open zeppelin hardhat upgrades plugin which we’re going to use to easily deploy our proxy.
npm i --save-dev @openzeppelin/hardhat-upgrades
Also install the upgradeable contracts from Open zeppelin which is the upgradeable counterpart of Open zeppelin contracts.
npm i --save-dev @openzeppelin/contracts-upgradeable
Copy and paste the following into the hardhat.config.ts file. This will do the basic hardhat set up we require for this project.
import { HardhatUserConfig } from "hardhat/config";import "@nomicfoundation/hardhat-toolbox";import "@openzeppelin/hardhat-upgrades";import "@typechain/hardhat";const config: HardhatUserConfig = {solidity: {version: "0.8.9",settings: {optimizer: {enabled: true,runs: 200,},},},defaultNetwork: "localhost",};export default config;
Implementation contract to deploy with Transparent Proxy
Inside contracts folder create a folder for the implementation contract for which we’re going to deploy a transparent proxy.
mkdir contracts/transparent
In the contracts directory create a file named VersionAware.sol and copy paste the following code in it.
// SPDX-License-Identifier: Unlicensepragma solidity ^0.8.0;abstract contract VersionAware {string public versionAwareContractName;function getContractNameWithVersion()externalpurevirtualreturns (string memory);}
Here we create a basic skeleton of our implementation contracts so that we can easily see the version upgrades visibly after deployment and upgrade.
In the contracts/transparent directory create two files named TransparentProxyPatternV1.sol and TransparentProxyPatternV2.sol and copy paste the following code into it.
TransparentProxyPatternV1.sol
// SPDX-License-Identifier: Unlicensepragma solidity ^0.8.0;import {ERC1967UpgradeUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol";import {VersionAware} from "../VersionAware.sol";contract TransparentProxyPatternV1 is ERC1967UpgradeUpgradeable, VersionAware {constructor() {_disableInitializers();}function initialize() external initializer {versionAwareContractName = "Transparent Proxy Pattern: V1";}function getContractNameWithVersion()publicpureoverridereturns (string memory){return "Transparent Proxy Pattern: V1";}}
TransparentProxyPatternV2.sol
// SPDX-License-Identifier: Unlicensepragma solidity ^0.8.0;import {ERC1967UpgradeUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol";import {VersionAware} from "../VersionAware.sol";contract TransparentProxyPatternV2 is ERC1967UpgradeUpgradeable, VersionAware {constructor() {_disableInitializers();}function initialize() external reinitializer(2) {versionAwareContractName = "Transparent Proxy Pattern: V2";}function getContractNameWithVersion()publicpureoverridereturns (string memory){return "Transparent Proxy Pattern: V2";}}
We have two implementation contracts version 1 and 2. Note that the structure of version 1 and 2 need not be same for upgrade to work. Note that both the contracts inherit from ERC1967UpgradeUpgradeable contract from the Open zeppelin upgradeable package. Read the earlier article linked in the top of this article to learn more about ERC1967 standard.
We have two implementation contracts version 1 and 2. Note that the structure of version 1 and 2 need not be same for upgrade to work. Note that both the contracts inherit from ERC1967UpgradeUpgradeable contract from the Open zeppelin upgradeable package. Read the earlier article linked in the top of this article to learn more about ERC1967 standard.
Here I attempt to explain a few advanced details of the the above contracts. Consider skipping it now and coming back later if you have difficulty understanding this on first read. Typical upgradeable contracts should not have a constructor because the constructor of the implementation contract can never be run in the context of the proxy contract. We have added a constructor here which is safe as it does not set to any of the storage variables. Leaving a contract without initializing it can pose a security threat. Calling the _disableInitializers method in the constructor makes the implementation contract not initializable which is much safer than leaving the implementation without a constructor and not initialized. Note that I have used initializer modifier in the initialize method in V1. This modifier makes sure that this initialize method is called only once, like solidity ensures for constructors. Also note that in V2 the modifier of the initialize method is reinitializer(2). Here 2 represents the version of the implementation contract. The reinitializer modifier has to be used instead of initializer because the proxy contract has already been initialized once in V1. There are more things and details to know about Initializer.sol contract which all upgradeable smart contracts should inherit. I will write more detailed article on it in future.
Now, run the following command to compile the smart contracts.
npx hardhat compile
Transparent Proxy Deployment and Upgrade
First we will deploy version 1 with transparent proxy and then upgrade it to version 2. We will use a hardhat script to do this. Create a script named transparent.js in scripts folder and copy paste the following code in it.
const { ethers, upgrades } = require("hardhat");async function main() {const TransparentProxyPatternV1 = await ethers.getContractFactory("TransparentProxyPatternV1");const transparentProxyPatternV1 = await upgrades.deployProxy(TransparentProxyPatternV1, [], {kind: 'transparent', unsafeAllow: ['constructor']});await transparentProxyPatternV1.deployed();console.log(`Transparent Proxy Pattern V1 is deployed to proxy address: ${transparentProxyPatternV1.address}`);let versionAwareContractName = await transparentProxyPatternV1.getContractNameWithVersion();console.log(`Proxy Pattern and Version: ${versionAwareContractName}`);const TransparentProxyPatternV2 = await ethers.getContractFactory("TransparentProxyPatternV2");const upgraded = await upgrades.upgradeProxy(transparentProxyPatternV1.address, TransparentProxyPatternV2, {kind: 'transparent', unsafeAllow: ['constructor'], call: 'initialize'});console.log(`Transparent Proxy Pattern V2 is upgraded in proxy address: ${upgraded.address}`);versionAwareContractName = await upgraded.getContractNameWithVersion();console.log(`Proxy Pattern and Version: ${versionAwareContractName}`);}main().catch((error) => {console.error(error);process.exitCode = 1;});
We first deploy version 1 of the smart contract with proxy and proxy admin. The Open zeppelin upgrades deployProxy method takes care of all these for us. Once version 1 is deployed with proxy we call the getContractNameWithVersion function on the proxy. This method will return a string according to what we made this method return in version 1 of the contract. Then we proceed to upgrade this contract with the upgradeProxy method. After the upgrade is over we call the getContractNameWithVersion function again to see the change in the string returned. Let’s run the script and see the results. Use the following command to run the script.
npx hardhat run scripts/transparent.js
You should see the following printed on your console.
Warning: Potentially unsafe deployment of TransparentProxyPatternV1You are using the `unsafeAllow.constructor` flag.Transparent Proxy Pattern V1 is deployed to proxy address: 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0
Proxy Pattern and Version: Transparent Proxy Pattern: V1
Warning: Potentially unsafe deployment of TransparentProxyPatternV2You are using the `unsafeAllow.constructor` flag.Transparent Proxy Pattern V2 is upgraded in proxy address: 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0
Proxy Pattern and Version: Transparent Proxy Pattern: V2
Yay! the results are as expected. The implementation contract is successfully upgraded to version 2. This is how you deploy and upgrade future versions of your contract with the transparent proxy pattern however complicated your smart contract gets.
Thanks for reading.
Who am I?
I am a full stack blockchain developer, passionate about building a decentralized and potentially more inclusive future. Have a blockchain development need?
Get in touch: 📧 hariharan@alumni.iitm.ac.in
New to trading? Try crypto trading bots or copy trading