A Hands-On Guide to Deploying an Upgradeable Contract with Foundry and OpenZeppelin
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:
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).
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:
@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
:
[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
.
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.
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:
[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.
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.