Transparent Upgradable Smart Contracts: A Guide with Code Explanation
Tags: smartcontract, javascript, blockchain, web3
Published: 2023-03-23 19:37:36

Smart contracts have revolutionized the way transactions occur on a blockchain. They are self-executing contracts with the terms of the agreement between buyer and seller being directly written into lines of code. Once the terms are fulfilled, the contract automatically enforces the transaction without the need for intermediaries.

But what happens when you need to upgrade the contract after deployment? Unfortunately, traditional smart contracts are immutable, meaning that any updates or changes are not possible after deployment. That’s where upgradable smart contracts come in.

Upgradable smart contracts enable updates and changes without losing the data or the contract address. The contract code remains in the same address, but the implementation is upgraded behind the scenes, and the contract remains functional with its stored data.

In this article, we will walk you through creating a Transparent Upgradable Smart Contract with a code explanation. We will use Hardhat as our development environment and OpenZeppelin’s transparent proxy as our upgradable contract mechanism.


## Setting up the Development Environment
Create a project folder called “Transparent Upgradable Smart Contract” in your preferred code editor. Open your editor’s command line and paste this command to install all the necessary dependencies:

```bash
yarn add --dev @nomiclabs/hardhat-ethers@npm:hardhat-deploy-ethers ethers @nomiclabs/hardhat-etherscan @nomiclabs/hardhat-waffle chai ethereum-waffle hardhat hardhat-contract-sizer hardhat-deploy hardhat-gas-reporter prettier prettier-plugin-solidity solhint solidity-coverage dotenv
```
After the installation is complete, create two contracts: `Box.sol` and `BoxV2.sol` in the contracts folder.

In `Box.sol` write the smart contract below:

```solidity
// contracts/Box.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.8;
 
contract Box {
    uint256 internal value;
 
    // Emitted when the stored value changes
    event ValueChanged(uint256 newValue);
 
    // Stores a new value in the contract
    function store(uint256 newValue) public {
        value = newValue;
        emit ValueChanged(newValue);
    }
 
    // Reads the last stored value
    function retrieve() public view returns (uint256) {
        return value;
    }

    // returns the current version of the contract
    function version() public pure returns (uint256) {
        return 1;
    }
}
```
The contract `Box` is a simple contract that stores a single value. It has three functions: `store`, `retrieve`, and `version`.

1. The `store` function stores a new value in the contract and emits an event ValueChanged with the new value as a parameter.
2. The `retrieve` function returns the last stored value.
3. The `version` function returns the current version of the contract, which is 1 in this case

In `BoxV2.sol` write the smart contract below:

```solidity
// contracts/BoxV2.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.8;

contract BoxV2 {
    uint256 internal value;

    // Emitted when the stored value changes
    event ValueChanged(uint256 newValue);

    // Stores a new value in the contract
    function store(uint256 newValue) public {
        value = newValue;
        emit ValueChanged(newValue);
    }

    // Reads the last stored value
    function retrieve() public view returns (uint256) {
        return value;
    }

    // Increments the stored value by 1
    function increment() public {
        value = value + 1;
        emit ValueChanged(value);
    }

    // returns the current version of the contract
    function version() public pure returns (uint256) {
        return 2;
    }
}

```
The contract `BoxV2` stores a single uint256 value. It has three functions:

1. `store` : This function takes a uint256 value as input and stores it in the contract. It also emits an event `ValueChanged` with the new value as a parameter.

2. `retrieve` : This function is a view function that returns the last stored value.

3. `increment` : This function increments the stored value by 1 and emits an event `ValueChanged` with the new value as a parameter.

Additionally, the contract has a function `version` that returns the current version of the contract as a uint256. In this case, the version is 2. This function is marked as pure, which means it doesn’t modify the state of the contract.

To deploy the smart contract, we need to create a deploy folder that will contain two files,` 01-deploy-box.js`, and `02-deploy-boxV2.js` .

In `01-deploy-box.js` write the following code:

```javascript
// deploy/01-deploy-box.js
const { developmentChains, VERIFICATION_BLOCK_CONFIRMATIONS } = require("../helper-hardhat-config")

const { network } = require("hardhat")
const { verify } = require("../helper-functions")

module.exports = async ({ getNamedAccounts, deployments }) => {
    const { deploy, log } = deployments
    const { deployer } = await getNamedAccounts()

    const waitBlockConfirmations = developmentChains.includes(network.name)
        ? 1
        : VERIFICATION_BLOCK_CONFIRMATIONS

    log("----------------------------------------------------")

    const box = await deploy("Box", {
        from: deployer,
        args: [],
        log: true,
        waitConfirmations: waitBlockConfirmations,
        proxy: {
            proxyContract: "OpenZeppelinTransparentProxy",
            viaAdminContract: {
                name: "BoxProxyAdmin",
                artifact: "BoxProxyAdmin",
            },
        },
    })

    // Be sure to check out the hardhat-deploy examples to use UUPS proxies!
    // https://github.com/wighawag/template-ethereum-contracts

    // Verify the deployment
    if (!developmentChains.includes(network.name) && process.env.ETHERSCAN_API_KEY) {
        log("Verifying...")
        const boxAddress = (await ethers.getContract("Box_Implementation")).address;
        await verify(boxAddress, [])
    }
    log("----------------------------------------------------")
}

module.exports.tags = ["all", "box"]
```

The code block includes the following:

1. Importing helper modules such as `developmentChains` and `VERIFICATION_BLOCK_CONFIRMATIONS` from the `helper-hardhat-config file`, network from hardhat, and verify from `helper-functions`
2. Defining an export function that will be called by the Hardhat deployment script. The function will take two arguments, `getNamedAccounts` and deployments.
3. The function uses the deployments object to deploy the `Box` smart contract. It retrieves the deployer account from `getNamedAccountsand` uses it as the deployer of the contract.
4. The `waitBlockConfirmations` variable is set to 1 if the network is a development chain, else to `VERIFICATION_BLOCK_CONFIRMATIONS`.
5. The deploy function is called with arguments for the contract name, `Box`, from the deployer account, with no arguments for the constructor, and logging set to true. Additionally, it specifies the `waitBlockConfirmations` variable, and a proxy object is provided with the `proxyContract` and `viaAdminContract` properties.
6. The deployment is then verified by calling the `verify` function, but only if the network is not a development chain, and the `ETHERSCAN_API_KEY` environment variable is set.
7. Finally, the function logs a message indicating the deployment is complete.

In` 02-deploy-boxV2.js` write the following code:

```javascript
// deploy/02-deploy-box.js
const { developmentChains, VERIFICATION_BLOCK_CONFIRMATIONS } = require("../helper-hardhat-config")

const { network } = require("hardhat")
const { verify } = require("../helper-functions")

module.exports = async ({ getNamedAccounts, deployments }) => {
    const { deploy, log } = deployments
    const { deployer } = await getNamedAccounts()

    const waitBlockConfirmations = developmentChains.includes(network.name)
        ? 1
        : VERIFICATION_BLOCK_CONFIRMATIONS

    log("----------------------------------------------------")

    const box = await deploy("BoxV2", {
        from: deployer,
        args: [],
        log: true,
        waitConfirmations: waitBlockConfirmations,
    })

    // Verify the deployment
    if (!developmentChains.includes(network.name) && process.env.ETHERSCAN_API_KEY) {
        log("Verifying...")
        await verify(box.address, [])
    }
    log("----------------------------------------------------")
}

module.exports.tags = ["all", "boxv2"]
```
In the code above we use the deployments and `getNamedAccounts` functions provided by Hardhat to get the `deployment` information and named accounts respectively. It also imports two helper modules:` helper-hardhat-config` and `helper-functions.`

The `developmentChains` and `VERIFICATION_BLOCK_CONFIRMATIONS` are constants that are required from the `helper-hardhat-config` module.

The `waitBlockConfirmations` variable is used to determine how many block confirmations to wait for before considering the deployment complete. It is set to 1 for development chains and `VERIFICATION_BLOCK_CONFIRMATIONS` for other networks.

The `log` function is used to print messages to the console, indicating the start and end of the deployment process.

After deploying the `BoxV2` contract, the code checks if the network is a development network, and if not, it verifies the contract deployment using the `verify` function from the `helper-functions` module. The `verify` the function is used to ensure that the contract has been deployed correctly and matches the source code.

Finally, the module.exports.tags property is used to add tags to the deployment script.

Here are all the other functions exported in both `01-deploy-box.js`, and `02-deploy-boxV2.js`

1. **`verify` from `helper-functions.js` file**

```javascript
const { run } = require("hardhat")

const verify = async (contractAddress, args) => {
    console.log("Verifying contract...")
    try {
        await run("verify:verify", {
            address: contractAddress,
            constructorArguments: args,
        })
    } catch (e) {
        if (e.message.toLowerCase().includes("already verified")) {
            console.log("Already verified!")
        } else {
            console.log(e)
        }
    }
}

module.exports = {
    verify,
}
```

`verify` that uses the hardhat library to verify the `bytecode` of a smart contract deployed at a specific address and with specific constructor arguments.

The function logs a message indicating that the verification process has started and then calls the `run` method from the `hardhat` library with the v`erify:verify` task, passing in the `contractAddress` and `args` as arguments.

If the verification is successful, the function completes without any further action. If the contract has already been verified, the function logs a message indicating that it has already been verified. Otherwise, if there is an error during the verification process, the function logs the error message.

2.**`hardhat.config.js`**

```javascript
require("@nomiclabs/hardhat-waffle")
require("@nomiclabs/hardhat-etherscan")
require("hardhat-deploy")
require("solidity-coverage")
require("hardhat-gas-reporter")
require("hardhat-contract-sizer")
require("dotenv").config()
require("@openzeppelin/hardhat-upgrades")

/**
 * @type import('hardhat/config').HardhatUserConfig
 */

const MAINNET_RPC_URL =
    process.env.MAINNET_RPC_URL ||
    process.env.ALCHEMY_MAINNET_RPC_URL ||
    "https://eth-mainnet.alchemyapi.io/v2/your-api-key"
const SEPOLIA_RPC_URL =
    process.env.SEPOLIA_RPC_URL || "https://eth-sepolia.g.alchemy.com/v2/YOUR-API-KEY"
const POLYGON_MAINNET_RPC_URL =
    process.env.POLYGON_MAINNET_RPC_URL || "https://polygon-mainnet.alchemyapi.io/v2/your-api-key"
const PRIVATE_KEY = process.env.PRIVATE_KEY
// optional
const MNEMONIC = process.env.MNEMONIC || "your mnemonic"

// Your API key for Etherscan, obtain one at https://etherscan.io/
const ETHERSCAN_API_KEY = process.env.ETHERSCAN_API_KEY || "Your etherscan API key"
const POLYGONSCAN_API_KEY = process.env.POLYGONSCAN_API_KEY || "Your polygonscan API key"
const REPORT_GAS = process.env.REPORT_GAS || false

module.exports = {
    defaultNetwork: "hardhat",
    networks: {
        hardhat: {
            // // If you want to do some forking, uncomment this
            // forking: {
            //   url: MAINNET_RPC_URL
            // }
            chainId: 31337,
        },
        localhost: {
            chainId: 31337,
        },
        sepolia: {
            url: SEPOLIA_RPC_URL,
            accounts: PRIVATE_KEY !== undefined ? [PRIVATE_KEY] : [],
            //   accounts: {
            //     mnemonic: MNEMONIC,
            //   },
            saveDeployments: true,
            chainId: 11155111,
        },
        mainnet: {
            url: MAINNET_RPC_URL,
            accounts: PRIVATE_KEY !== undefined ? [PRIVATE_KEY] : [],
            //   accounts: {
            //     mnemonic: MNEMONIC,
            //   },
            saveDeployments: true,
            chainId: 1,
        },
        polygon: {
            url: POLYGON_MAINNET_RPC_URL,
            accounts: PRIVATE_KEY !== undefined ? [PRIVATE_KEY] : [],
            saveDeployments: true,
            chainId: 137,
        },
    },
    etherscan: {
        // npx hardhat verify --network <NETWORK> <CONTRACT_ADDRESS> <CONSTRUCTOR_PARAMETERS>
        apiKey: {
            sepolia: ETHERSCAN_API_KEY,
            polygon: POLYGONSCAN_API_KEY,
        },
    },
    gasReporter: {
        enabled: REPORT_GAS,
        currency: "USD",
        outputFile: "gas-report.txt",
        noColors: true,
        // coinmarketcap: process.env.COINMARKETCAP_API_KEY,
    },
    contractSizer: {
        runOnCompile: false,
        only: ["Box"],
    },
    namedAccounts: {
        deployer: {
            default: 0, // here this will by default take the first account as deployer
            1: 0, // similarly on mainnet it will take the first account as deployer. Note though that depending on how hardhat network are configured, the account 0 on one network can be different than on another
        },
        player: {
            default: 1,
        },
    },
    solidity: {
        compilers: [
            {
                version: "0.8.8",
            },
            {
                version: "0.4.24",
            },
        ],
    },
    mocha: {
        timeout: 200000, // 200 seconds max for running tests
    },
}
```
This is a configuration file for Hardhat, a popular development environment for Ethereum smart contracts. The configuration file specifies the various settings and options for the Hardhat environment, including network configurations, compiler versions, deployment settings, and testing options.

This file includes the following:

1. A list of required plugins including `Hardhat Waffle`, `Hardhat Etherscan,` `Hardhat Deploy,` `Solidity Coverage`, `Hardhat Gas Reporter`, and Hardhat Contract Sizer.
2. Configuration settings for several networks including `Hardhat`, `local`, `Sepolia`, `Mainnet`, and `Polygon`.
3. Configuration settings for `Etherscan` and `Polygonscan` APIs.
4. Gas reporting settings including enabled/disabled, currency, output file, and `CoinMarketCap` API key.
5. Configuration settings for named accounts including the `deployer` and `player`.
6. Solidity compiler versions to use.
7. `Mocha` testing settings, including a timeout of 200 seconds for running tests.

Overall, this configuration file is used to set up the Hardhat environment for efficient and effective smart contract development and testing.

3.**`helper-hardhat-config.js`**

```javascript
const networkConfig = {
  default: {
    name: "hardhat",
  },
  31337: {
    name: "localhost",
  },
  11155111: {
    name: "sepolia",
  },
  1: {
    name: "mainnet",
  },
};

const developmentChains = ["hardhat", "localhost"];
const VERIFICATION_BLOCK_CONFIRMATIONS = 6;

module.exports = {
  networkConfig,
  developmentChains,
  VERIFICATION_BLOCK_CONFIRMATIONS,
};
```
The code above defines a JavaScript module that exports three variables:

1. `networkConfig`: an object that maps network IDs to network names. Four networks are defined: `hardhat` (default), `localhost` (ID 31337), `sepolia` (ID 11155111), and mainnet (ID 1).
2. `developmentChains`: an array that contains the names of the networks that are considered to be for development purposes. In this case, it includes `hardhat` and `localhost`.
3. `VERIFICATION_BLOCK_CONFIRMATIONS`: a constant that represents the number of block confirmations required for a transaction to be considered verified. In this case, it is set to 6.

Overall, the code is defining some configuration settings related to network IDs, network names, and block confirmation requirements, which can be used by other parts of our application.

4.**`.env` file**

```bash
SEPOLIA_RPC_URL='https://eth-sepolia.g.alchemy.com/v2/YOUR-API-KEY'
POLYGON_MAINNET_RPC_URL='https://rpc-mainnet.maticvigil.com'
ALCHEMY_MAINNET_RPC_URL="https://eth-mainnet.alchemyapi.io/v2/your-api-key"
ETHERSCAN_API_KEY='YOUR_KEY'
POLYGONSCAN_API_KEY='YOUR_KEY'
PRIVATE_KEY='abcdefg'
MNEMONIC='abcdefsgshs'
REPORT_GAS=true
COINMARKETCAP_API_KEY="YOUR_KEY"
```

1. `SEPOLIA_RPC_URL`: This environment variable specifies the URL for the Sepolia RPC endpoint, which is used for interacting with the Ethereum blockchain. This particular endpoint is provided by Alchemy, and requires an API key to access it.
2. `POLYGON_MAINNET_RPC_URL`: This environment variable specifies the URL for the Polygon Mainnet RPC endpoint, which is used for interacting with the Polygon network. This particular endpoint is provided by MaticVigil, and does not require an API key to access it.
3. `ALCHEMY_MAINNET_RPC_URL`: This environment variable specifies the URL for the Alchemy Mainnet RPC endpoint, which is also used for interacting with the Ethereum blockchain. This particular endpoint is provided by Alchemy, and requires an API key to access it.
4. `ETHERSCAN_API_KEY`: This environment variable specifies the API key for Etherscan, which is a blockchain explorer that provides information about transactions, addresses, and blocks on the Ethereum blockchain.
5. `POLYGONSCAN_API_KEY`: This environment variable specifies the API key for Polygonscan, which is a blockchain explorer that provides information about transactions, addresses, and blocks on the Polygon network.
6. `PRIVATE_KEY`: This environment variable specifies the private key for a particular Ethereum account. This key is used for signing transactions and authenticating with the network.
7. `MNEMONIC`: This environment variable specifies a mnemonic phrase that can be used to generate a private key for an Ethereum account. This is an alternative way to specify the private key, and can be useful for managing multiple accounts.
8. `REPORT_GAS`: This environment variable specifies whether or not to report the gas cost of each transaction. If set to true, gas cost will be reported.
9. `COINMARKETCAP_API_KEY`: This environment variable specifies the API key for CoinMarketCap, which is a website that provides information about cryptocurrency prices and market capitalization.

After setting well the environment, let’s deploy the 2 contracts by running the following command:

```bash
yarn hardhat deploy
```

To Upgrade the contract, let’s create our scripts folder and create `upgrade-box.js` , let’s add the script to upgrade our contract `Box` to `BoxV2`

in `upgrade-box.js` write the following code:

```javascript
const { developmentChains, VERIFICATION_BLOCK_CONFIRMATIONS } = require("../helper-hardhat-config")
const { network, deployments, deployer } = require("hardhat")
const { verify } = require("../helper-functions")

async function main() {
    const { deploy, log } = deployments
    const { deployer } = await getNamedAccounts()

    const waitBlockConfirmations = developmentChains.includes(network.name)
        ? 1
        : VERIFICATION_BLOCK_CONFIRMATIONS

    log("----------------------------------------------------")

    const boxV2 = await deploy("BoxV2", {
        from: deployer,
        args: [],
        log: true,
        waitConfirmations: waitBlockConfirmations,
    })

    // Verify the deployment
    if (!developmentChains.includes(network.name) && process.env.ETHERSCAN_API_KEY) {
        log("Verifying...")
        await verify(boxV2.address, [])
    }

    // Upgrade!
    // Not "the hardhat-deploy way"
    const boxProxyAdmin = await ethers.getContract("BoxProxyAdmin")
    const transparentProxy = await ethers.getContract("Box_Proxy")
    const upgradeTx = await boxProxyAdmin.upgrade(transparentProxy.address, boxV2.address)
    await upgradeTx.wait(1)
    const proxyBox = await ethers.getContractAt("BoxV2", transparentProxy.address)
    const version = await proxyBox.version()
    console.log('New version : ',version.toString())
    log("----------------------------------------------------")
}

main()
    .then(() => process.exit(0))
    .catch((error) => {
        console.error(error)
        process.exit(1)
    })
```

The script performs the following steps:

1. It imports some necessary modules from the helper-hardhat-config and `helper-functions` files.
2. It defines an asynchronous function called main that will be executed when the script is run.
3. The function gets the named accounts using the `getNamedAccounts` function from `Hardhat`.
4. It determines the number of `block confirmations `to wait for before considering the deployment successful. If the network is a development network, the number of block confirmations is set to 1, otherwise, it is set to a constant value defined in the `helper-hardhat-config` file.
5. It deploys the new version of the contract using the `deployments.deploy` function from Hardhat, passing in the necessary arguments such as the `name` of the `contract`, the `address` of the `deployer`, and the number of `block confirmations` to wait for.
6. If the network is not a development network and an `Etherscan API key` is provided, the script attempts to verify the deployment of the new contract using the `verify` function from the `helper-functions` file.
7. The script then upgrades an existing contract instance to use the new version. It first retrieves the contract instances for the `BoxProxyAdmin` contract and the existing `Box_Proxy` contract using the `ethers.getContract` function from Hardhat. It then calls the `upgrade` function on the `BoxProxyAdmin` contract, passing in the `address` of the existing `Box_Proxy` contract and the `address` of the newly deployed `BoxV2` contract. Finally, it retrieves the contract instance for the upgraded `BoxV2` contract and logs its version.
8. The function is called at the end of the script using the `main() `function and any errors are logged to the console.

To check the upgrade by running the commands below:

```bash
yarn hardhat node
yarn hardhat run scripts/upgrade-box.js --network localhost
```

The output will be:

```bash
New version : 2
```

In conclusion, `transparent upgradable smart contracts` are a powerful tool for blockchain developers. They allow for seamless upgrades to deployed contracts without disrupting the network or requiring users to switch to a new version of the contract. In this article, we explained the concept of `transparent upgradable smart contracts` and demonstrated an example implementation using two Solidity contracts and their deployment scripts.

Useful resources:
1.[https://github.com/wighawag/template-ethereum-contracts](https://github.com/wighawag/template-ethereum-contracts)
2.[https://docs.openzeppelin.com/upgrades-plugins/1.x/](https://docs.openzeppelin.com/upgrades-plugins/1.x/)
3.[https://github.com/wighawag/template-ethereum-contracts/tree/examples/openzeppelin-proxies/deploy](https://github.com/wighawag/template-ethereum-contracts/tree/examples/openzeppelin-proxies/deploy)