How to Create Upgradeable Smart Contracts
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.
- Open your terminal and run
npx thirdweb create contract
to create a new Solidity project. SelectForge
as the framework. - Open the project in your code editor and delete the default contract in the
src
folder. - 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
- Create a new file called
Number.sol
in thesrc
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");
}
}
- 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.
- In your terminal, run
npx thirdweb deploy
and select theNumber
contract. - Choose
Arbitrum Sepolia
as the network to deploy to and confirm the transaction in your wallet. - 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.
- Create a new folder called
proxy
in yoursrc
directory and add a file namedProxy.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) {}
}
- 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 thetest
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.
- Run
npx thirdweb deploy
again in your terminal and this time select theProxy
contract. - When prompted, enter the address of the
Number
implementation contract you deployed earlier, and paste the initialization data we got from the test. - Select
Arbitrum Sepolia
as the network, confirm the transaction, and wait for the proxy contract to be deployed. - 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.
- In the thirdweb dashboard, go to the proxy contract's "Explorer" tab and expand the "Write Functions" section.
- Enter a new number in the
setNumber
function and click "Execute". Confirm the transaction in your wallet. - 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:
- Open the
Number.sol
contract in your code editor and update thesetNumber
function to the following:
function setNumber(uint256 _newNumber) public {
number = number * _newNumber;
}
- Run
npx thirdweb deploy
in your terminal, select theNumber
contract, and deploy it toArbitrum Sepolia
. This will deploy a new implementation contract. - Copy the address of the new implementation contract, then go back to the proxy contract in the thirdweb dashboard.
- Expand the "Write Functions" section, paste the new implementation address into the
upgradeTo
function, and click "Execute". Confirm the transaction. - 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:
- Create the implementation contract with the initial logic and the required
_authorizeUpgrade
function. - Deploy the implementation contract using the thirdweb CLI.
- Create the proxy contract that inherits from
ProxyForUpgradeable
and passes the implementation contract address and initialization data to the constructor. - Get the initialization data by encoding the call to the
initialize
function in a test. - Deploy the proxy contract, passing the implementation contract address and initialization data.
- Interact with the proxy contract, which delegates calls to the implementation contract.
- Create a new version of the implementation contract with updated logic.
- 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!