How to Create Upgradeable Smart Contracts

vasiliy upgrade contracts thumbnail from thirdweb youtube

One of the key features of blockchains is immutability - once a transaction is verified and added onchain, it cannot be changed. This immutability also applies to smart contracts. Once you deploy a smart contract, you cannot modify its code if you find an error or want to add new functionality.

However, there are ways to create upgradeable smart contracts that allow you to update the contract logic while preserving the contract address and state. In this guide, you'll learn how to use the Universal Upgradeable Proxy Standard (UUPS) pattern to create an upgradeable contract in Solidity with thirdweb.

Check out the video version of this tutorial here:

Before you create an upgradeable smart contract

You will need:

  • A thirdweb account
  • A wallet like MetaMask to connect to thirdweb
  • Test funds on your testnet of choice. We'll be using Arbitrum Sepolia in this example.

How to Create an Upgradeable smart contract

There are a few key steps to creating an upgradeable smart contract, so let's break it down into steps outlining how to create and deploy the implementation contract, how to create and deploy the proxy contract, and then how those pieces make up your upgradeable smart contract.

Let's dive in:

Create the Implementation Smart Contract

First, let's create the implementation contract that will contain the upgradeable logic. We'll start with a simple smart contract that stores a number and allows the owner to update it.

  1. Open your terminal and run npx thirdweb create contract to create a new Solidity project. Select Forge as the framework.
  2. Open the project in your code editor and delete the default contract in the src folder.
  3. Add the correct remappings to foundry.toml.
[profile.default]
src = 'src'
out = 'out'
libs = ['lib']
remappings = [
    '@thirdweb-dev/=node_modules/@thirdweb-dev/',
]


# See more config options https://github.com/foundry-rs/foundry/tree/master/config
  1. Create a new file called Number.sol in the src folder and add the following code:
//SPDX-License-Identifier: MIT

import {Initializable} from "@thirdweb-dev/contracts/extension/Initializable.sol";
import {Upgradeable} from "@thirdweb-dev/contracts/extension/Upgradeable.sol";

pragma solidity 0.8.26;

contract Number is Initializable, Upgradeable {
    uint256 public number;
    address public owner;

    function initialize(uint256 _initialValue) public initializer {
        require(_initialValue >= 0, "Number must be equal or greater than 0");
        number = _initialValue;
        owner = msg.sender;
    }

    function _authorizeUpgrade(address) internal view override {
        require(msg.sender == owner, "Only owner can upgrade");
    }
}
  1. Open your terminal, navigate to the project directory, and run forge build to compile the contract. Make sure there are no errors.

Deploy the Implementation Contract

Next, let's deploy the implementation contract to our chosen network using the thirdweb CLI.

  1. In your terminal, run npx thirdweb deploy and select the Number contract.
  2. Choose Arbitrum Sepolia as the network to deploy to and confirm the transaction in your wallet.
  3. Once deployed, you'll see the contract in your thirdweb dashboard. Click into it and copy the contract address as we'll need it in the next step.

Create the Proxy Smart Contract

Now we need to create the proxy contract that will delegate calls to the implementation contract. The proxy contract will maintain the storage variables and the contract address that users interact with.

  1. Create a new folder called proxy in your src directory and add a file named Proxy.sol with the following code:
//SPDX-License-Identifier: MIT

import {ProxyForUpgradeable} from "@thirdweb-dev/contracts/extension/ProxyForUpgradeable.sol";

pragma solidity ^0.8.26;

contract Proxy is ProxyForUpgradeable {
    constructor(
        address _logic,
        bytes memory _data
    ) ProxyForUpgradeable(_logic, _data) {}
}

  1. To get the initialization data to pass to the proxy constructor, we need to encode the call to the initialize function of the implementation contract.

Let's create a test to get this data:

  • Create a new file called InitializeTest.sol in the test folder with the following code:
//SPDX-License-Identifier: MIT

pragma solidity ^0.8.26;

import {Test} from "forge-std/Test.sol";
import {Number} from "../src/Number.sol";

contract InitializeTest is Test {
    function testInitializeData() public {
        uint initialValue = 42;

        emit log_bytes(abi.encodeCall(Number.initialize, (initialValue)));
    }
}

  • Run forge test --mt testInitializeData -vvv in your terminal. This will execute the test and output the encoded initialization data that we need. Copy this data as we'll use it when deploying the proxy contract.

Deploy the Proxy Contract

We're now ready to deploy our proxy contract, passing it the address of the implementation contract and the initialization data.

  1. Run npx thirdweb deploy again in your terminal and this time select the Proxy contract.
  2. When prompted, enter the address of the Number implementation contract you deployed earlier, and paste the initialization data we got from the test.
  3. Select Arbitrum Sepolia as the network, confirm the transaction, and wait for the proxy contract to be deployed.
  4. View the proxy contract in the thirdweb dashboard. Notice that it has the same name and functions as the implementation contract, because it's delegating all calls to it.

Test the Upgradeable Smart Contract

Let's test out our upgradeable contract by updating the number and then upgrading the implementation.

  1. In the thirdweb dashboard, go to the proxy contract's "Explorer" tab and expand the "Write Functions" section.
  2. Enter a new number in the setNumber function and click "Execute". Confirm the transaction in your wallet.
  3. Expand the "Read Functions" section and click "Query" next to the number function. You should see the updated number returned.

Now let's upgrade the contract to a new version that multiplies the number instead of setting it:

  1. Open the Number.sol contract in your code editor and update the setNumber function to the following:
function setNumber(uint256 _newNumber) public {
    number = number * _newNumber;
}
  1. Run npx thirdweb deploy in your terminal, select the Number contract, and deploy it to Arbitrum Sepolia. This will deploy a new implementation contract.
  2. Copy the address of the new implementation contract, then go back to the proxy contract in the thirdweb dashboard.
  3. Expand the "Write Functions" section, paste the new implementation address into the upgradeTo function, and click "Execute". Confirm the transaction.
  4. Now when you call the setNumber function on the proxy contract, it will multiply the stored number by the input instead of replacing it. Test it out and verify the new behavior!

And that's it! You've now created an upgradeable smart contract using the UUPS pattern and thirdweb. The proxy contract maintains the same address and state, while delegating function calls to the implementation contract which can be swapped out to add new features or fix bugs.

Deploy Upgradeable smart contracts with ease

Congratulations! You've now learned how to create upgradeable smart contracts using the Universal Upgradeable Proxy Standard (UUPS) pattern and thirdweb. By separating the contract logic into implementation contracts and using a proxy contract to delegate calls, you can update your contract's functionality while preserving its address and state.

Here are the key steps we covered:

  1. Create the implementation contract with the initial logic and the required _authorizeUpgrade function.
  2. Deploy the implementation contract using the thirdweb CLI.
  3. Create the proxy contract that inherits from ProxyForUpgradeable and passes the implementation contract address and initialization data to the constructor.
  4. Get the initialization data by encoding the call to the initialize function in a test.
  5. Deploy the proxy contract, passing the implementation contract address and initialization data.
  6. Interact with the proxy contract, which delegates calls to the implementation contract.
  7. Create a new version of the implementation contract with updated logic.
  8. Deploy the new implementation contract and use the upgradeTo function on the proxy contract to point it to the new implementation.

Continue exploring upgradeable contracts and experiment with different patterns to find what works best for your use case. Join thirdweb's community of developers and explore our documentation to learn more!