deploy smart contract upgradeable blockchain engineer

A Hands-On Guide to Deploying an Upgradeable Contract with Foundry and OpenZeppelin

picture of author thomas cosiallsThomas Cosialls
linkedin-logogithub-logowebsite-logo

We're going to deploy an upgradeable ERC20 token using Foundry and OpenZeppelin. This guide will cover setting up your development environment with Foundry, writing the token contract using OpenZeppelin's libraries for secure, standard-compliant proxy patterns, testing your contract to ensure reliability, and finally, deploying it to the Polygon Mumbai blockchain. By the end of this, you'll have a live, upgradeable ERC20 token that you created and deployed yourself. Let's get started!

Step 1: Setting Up Your Foundry Project

Before we begin, you'll need to have Foundry installed on your machine. Foundry is a blazing-fast development toolkit for Ethereum, allowing you to compile, test, and deploy smart contracts with ease. Follow the official Foundry installation guide to get set up.

After initializing your project with forge init, proceed to install the OpenZeppelin contracts.

forge init my-upgradeable-contract forge install openzeppelin/openzeppelin-contracts-upgradeable

Step 2: Crafting Your Upgradeable ERC20 Token

Navigate to the src/ directory and create a Solidity file for your token, for example, MyUpgradeableToken.sol. Use OpenZeppelin's upgradeable contracts to ensure your token is ready for future updates:

src/MyUpgradeableToken.sol
pragma solidity ^0.8.25;

import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

contract MyUpgradeableToken is Initializable, ERC20Upgradeable {
    function initialize() initializer public {
        __ERC20_init("MyUpgradeableToken", "MUT");
        _mint(msg.sender, 1000 * 10 ** decimals());
    }
}

This contract uses OpenZeppelin's Initializable and ERC20Upgradeable to create an ERC20 token that can be upgraded. The initialize function replaces the constructor in non-upgradeable contracts, setting initial values and minting the token supply to the deployer.

Let's make our contract ownable and add a custom claim function to allow users to get some free tokens while maintaining the total supply constant (with the _burn() function).

src/MyUpgradeableToken.sol
pragma solidity ^0.8.25;

import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";

contract MyUpgradeableToken is Initializable, ERC20Upgradeable, OwnableUpgradeable {
    
    function initialize() initializer public {
        __ERC20_init("MyUpgradeableToken", "MUT");
        _mint(msg.sender, 1000 * 10 ** decimals());
        __Ownable_init(msg.sender);
    }

    function claim() public {
        uint256 amount = 10 * 10 ** decimals();
        require(balanceOf(owner()) >= amount, "Contract owner does not have enough tokens to burn");
        require(balanceOf(msg.sender) < amount, "User cannot claim tokens");
        _burn(owner(), amount);
        _mint(msg.sender, amount);
    }
}

Step 3: Testing With Foundry

To handle OpenZeppelin upgrades in Foundry, we need to install some special packages first. Check this Openzeppelin Github repository for more details.

forge install OpenZeppelin/openzeppelin-foundry-upgrades forge install OpenZeppelin/openzeppelin-contracts-upgradeable

Then, add these two lines in remappings.txt:

remappings.txt
@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/
@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/

Finally, add this configuration in foundry.toml:

foundry.toml
[profile.default]
ffi = true
ast = true
build_info = true
extra_output = ["storageLayout"]

We can now write some tests for our upgradeable contract in the test/ directory. You can call this test file MyUpgradeableTokenTest.t.sol.

test/MyUpgradeableTokenTest.t.sol
pragma solidity ^0.8.25;

import {Test, console} from "forge-std/Test.sol";
import {Upgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol";
import "../src/MyUpgradeableToken.sol"; // Adjust the path according to your project structure

contract MyUpgradeableTokenTest is Test {
    MyUpgradeableToken public proxy;
    address implementationAddress;
    address proxyAddress;
    address owner = address(1);

    function setUp() public {
        vm.prank(owner);
        address _proxyAddress = Upgrades.deployTransparentProxy(
            "MyUpgradeableToken.sol",
            owner,
            abi.encodeCall(MyUpgradeableToken.initialize)
        );

        implementationAddress = Upgrades.getImplementationAddress(
            _proxyAddress
        );
        proxyAddress = _proxyAddress;
        proxy = MyUpgradeableToken(proxyAddress);
    }

    function testClaim() public {
        uint256 expectedBalance = 10 * 10 ** proxy.decimals();
        uint256 initBalance = 1000000000 * 10 ** proxy.decimals();
        address claimer = address(2);
        vm.prank(claimer);
        proxy.claim();
        assertEq(
            proxy.balanceOf(claimer),
            expectedBalance,
            "Claimer does not have the expected balance of tokens"
        );
        assertLt(
            proxy.balanceOf(owner),
            initBalance,
            "Owner did not loose tokens during the claim"
        );
    }
}

You can run your tests with the following command.

forge test

For comprehensive testing practices, refer to the Foundry documentation.

Step 4: Deploying Your Upgradeable Contract

You would normally use the forge create command to deploy smart contract with Foundry. However, since our smart contract is upgradeable we will use a Script to achieve the deployment.

In /script, create a file called 01_Deploy.s.sol with the following code inside. Don't forget to create a variable called PRIVATE_KEY in your .env file that contains one of your wallet's private key. I strongly advise you to use a hardware wallet here when you deploy to mainnet.

src/ 01_Deploy.s.sol
pragma solidity ^0.8.25;

import "forge-std/Script.sol";
import "../src/MyUpgradeableToken.sol";
import {Upgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol";

contract DeployScript is Script {

    function run() external returns (address, address) {
        //we need to declare the sender's private key here to sign the deploy transaction
        uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
        vm.startBroadcast(deployerPrivateKey);

        // Deploy the upgradeable contract
        address _proxyAddress = Upgrades.deployTransparentProxy(
            "MyUpgradeableToken.sol",
            msg.sender,
            abi.encodeCall(MyUpgradeableToken.initialize, (msg.sender))
        );

        // Get the implementation address
        address implementationAddress = Upgrades.getImplementationAddress(
            _proxyAddress
        );

        vm.stopBroadcast();

        return (implementationAddress, _proxyAddress);
    }
}

We are almost there! I picked Polygon Mumbai as my target network, so let's add a custom RPC url config in the foundry.toml file:

foundry.toml
[rpc_endpoints]
mumbai = "https://rpc.ankr.com/polygon_mumbai"

Now, let's run the following command to deploy your code to Polygon Mumbai:

forge script script/01_Deploy.s.sol:DeployScript --sender ${YOUR_PUBLIC_KEY} --rpc-url mumbai --broadcast -vvvv

You should have a similar success message when the deploy transaction has been validated on the network.

foundry deploy script upgradeable contract

Great! Our upgradeable contract is now live on Polygon Mumbai!

One more thing, let's verify it so we can directly play with the Read / Write functions from Polygonscan. You can get a Polygonscan api key by signing up here and the implementation address of our proxy contract will be displayed in your console while the deploy script is running.

forge verify-contract --chain mumbai --etherscan-api-key ${YOUR_POLYGONSCAN_API_KEY} ${IMPLEMENTATION_CONTRACT_ADRESS} src/MyUpgradeableToken.sol:MyUpgradeableToken

Congratulations! You've just deployed your upgradeable ERC20 token using Foundry and OpenZeppelin's upgradeable contracts. This setup ensures your token can evolve over time, receiving new features or fixes without losing its state or requiring users to migrate assets.