This content originally appeared on DEV Community and was authored by Jamiebones
Smart contracts are immutable meaning once deployed on the blockchain, they can no longer be edited. This is a good thing but what if the deployed contract is buggy or you need to add a specific function to an already deployed smart contract without deploying a new contract thereby losing the state of the previous deployed contract.
OpenZeppelin comes to the rescue by providing upgradables that allow smart contract to be updated without state loss. This tutorial makes use of Hardhat and OpenZeppelin upgradable contract.
Create a new npm project by opening your terminal and typing: ( this tutorial assumes the user already has Node and npm installed in the system )
Tutorial Code lives here
- Project Dependencies
- Project
- How Upgradable Contract Works
- Verify Upgradable Contract
- Upgrading a smart contract
- Things to know when working with Upgradable Contracts
Project Dependencies
npm init --y
Open the package.json
file that was created when the above command was run and add the following dependencies code below to the package.json
file
"devDependencies": {
"@nomiclabs/hardhat-ethers": "^2.0.3",
"@nomiclabs/hardhat-etherscan": "^2.1.8",
"@openzeppelin/hardhat-upgrades": "^1.12.0",
"ethers": "^5.5.2",
"hardhat": "^2.8.0"
},
"dependencies": {
"dotenv": "^16.0.0"
}
Install the above dependencies by running npm i
. This installs the dependencies into the project. @openzeppelin/hardhat-upgrades
provides functionality for creating and deploying upgradable contracts. @nomiclabs/hardhat-etherscan
is used for verifying the contract using Etherscan. @nomiclabs/hardhat-ethers
allows hardhat to work with ether.js.
Project
Create a new hardhat project by running in the terminal:
npx hardhat
This presents us options to select a project template. Select the first option which is Create a sample project
and this creates a sample project with boiler plate code.
Create a .env
file in the project directory. This file will contain our environment variable. In this project, we will need values for the following environmental variables which are:
INFURA_API_KEY
PRI_KEY
ETHERSCAN_API_KEY
INFURA_API_KEY
: our API key we get from Infura.We will need this to connect to infura
PRI_KEY
: the primary key of your account in Meta mask. This is used for signing a transaction
ETHERSCAN_API_KEY
: your API key from Etherscan. This will be used for verifying a contract.
Open the hardhat-config.js
file and configure it by adding the code below.
require("@nomiclabs/hardhat-ethers");
require("@openzeppelin/hardhat-upgrades");
require("@nomiclabs/hardhat-etherscan");
require('dotenv').config();
module.exports = {
solidity: "0.8.10",
networks: {
ropsten: {
url: `https://ropsten.infura.io/v3/${process.env.INFURA_API_KEY}`,
accounts: [process.env.PRI_KEY],
},
rinkeby: {
url: `https://rinkeby.infura.io/v3/${process.env.INFURA_API_KEY}`,
accounts: [process.env.PRI_KEY]
}
},
etherscan: {
apiKey: process.env.ETHERSCAN_API_KEY,
},
};
Open the contract folder in the project and delete the Greeter.sol
file. Create a new file called CalculatorV1.sol
. This will contain the smart contract we will deploy to the rinkeby
network.
Inside the file CalculatorV1.sol
replace it with the following code below:
pragma solidity 0.8.10;
import "hardhat/console.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
contract CalculatorV1 is Initializable {
uint public val;
function initialize(uint256 _val ) external initializer{
val = _val;
}
function add(uint a, uint b) public pure returns (uint) {
return a + b;
}
function getVal() public view returns (uint) {
return val;
}
}
This smart contract is a simple contract of a calculator. The contract inherits from the Initializable
contract which is an Openzeppelin contract. It ensures that the initialize
function is called only once. An upgradable contract does not have a constructor so the initialize
function acts as a constructor and it must be called only once. The initializer
modifier ensures the function is called once.
The contract has a public variable named val
and three functions which are initialize
, add
and getVal
. We want to deploy this contract to the Rinkeby network which we have set up in the hardhat-config.js
file.
Create a new file inside the scripts folder and call it deploy_contract.js
. This file will contain the code that will deploy our Calculator contract for us.
Inside the deploy_contract.js
file add the following code:
//scripts/deploy_contract.js
const { ethers, upgrades } = require("hardhat");
async function main() {
const CalculatorV1 = await ethers.getContractFactory("CalculatorV1");
console.log("Deploying Calculator...");
const calculator = await upgrades.deployProxy(CalculatorV1, [42], {
initializer: "initialize",
});
await calculator.deployed();
console.log("Calculator deployed to:", calculator.address);
}
main();
The code above requires ethers
and upgrades
from hardhat
. An async
function is created and inside the function, the contract factory is retrieved using ethers
with the name of the contract ( CalculatorV1 ). The upgrades.deployProxy
is used to deploy the contract passingin the contract factory and the initialization function with its parameter passed.
Remember in the contract, we have an initialize
function that sets the value of the val
variable. This function is called as the contract is being deployed passing in the value 42
as the parameter to the function.
Run on the terminal the following code to deploy the contract:
npx hardhat run --network rinkeby scripts/deploy_contract.js
After some few second the contract is deployed with the
contract address logged to the console.
How Upgradable Contract Works
When we deployed the contract, three contracts were deployed in total. These are a Proxy
contract, a Proxy Admin
contract and the Implementation contract which is our CalculatorV1
. When a user interacts with the contract, he is actually interacting with the Proxy
contract. The Proxy
contract makes a delegateCall to our CalculatorV1
contract. For example A contract named A
makes a delegateCall to a contract B
calling a function in contract B
. The function in B
is executed in the state of variable A
.
For our upgradable contract, the Proxy
contract calls the Implementation
contract (CalculatorV1). The state change is made on the Proxy
contract. The Proxy Admin contract is used for updating the address of the implementation contract
inside the Proxy contract.
Upgrading a Contract
Create a new file inside the contract folder and name it CalculatorV2
.
pragma solidity 0.8.10;
import "hardhat/console.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
contract CalculatorV2 is Initializable {
uint public val;
function add(uint a, uint b) public pure returns (uint) {
return a + b;
}
function multiply(uint a, uint b) public pure returns (uint) {
return a * b;
}
function getVal() public view returns (uint) {
return val;
}
}
We have added a new function to this version of the contract. The multiply
function is added. To upgrade the deployed contract. Create a script inside the scripts
folder and create a file upgrade_contract.js
. Inside this file put the following code.
const { ethers, upgrades } = require("hardhat");
//the address of the deployed proxy
const PROXY = "0xaf03a6F46Eea7386F3E5481a4756efC678a624e6";
async function main() {
const CalculatorV2 = await ethers.getContractFactory("CalculatorV2");
console.log("Upgrading Calculator...");
await upgrades.upgradeProxy(PROXY, CalculatorV2);
console.log("Calculator upgraded");
}
main();
The address of the implementation Proxy and the contract factory of the new version of the contract is passed as parameters to upgrades.upgradeProxy
. Run the code by typing on the terminal :
npx hardhat run --network rinkeby scripts/upgrade_contract.js
This will update the address of the Implementation contract in the Proxy contract to make use of the new version deployed. Run the getVal
contract to retrieve the value of the state variable val
. You will notice that the value of val
is still the value we initiated it to be when we deployed the first version of the contract. That is the beauty of upgradable contracts which is the preservation of variable state.
To verify the contract, we have to perform the same steps that was used to verify the first version of the contract.
Things to know when working with Upgradable Contracts
When working with Upgradable contracts the following points should be noted:
- Constructor: An upgradable contract can not have a
constructor
. If you have code that must run when the contract is created. The code should be placed in an init function that will get called when the contract is deployed. OpenaeppelinInitializable
can be used to ensure a function is called once. (initializer
)
function initialize(uint256 _val ) external initializer {
val = _val;
}
The initialize
function will be called only once because of the initializer
modifier attached to it.
- state variables : state variables in upgradable contracts once declared cannot be removed. Assuming we have a version one contract where we define the following state variables :
uint public val;
string public name;
When deploying version two of the contract, we must ensure that version two of the contract upgrade also contain the same variable as version one in the same order as was defined in version one. The order of the variable matters. if we want to use new state variables, they are added at the bottom.
uint public val;
string public name;
string public newVariableOne;
uint public newVariableTwo;
- variable initialization : only state variable declared as
const
andimmutable
can be initialize. This is because initializing a state variable will attempt to create a storage for that variable. And as we know the Implementation contract don't use its state. The Proxy contract provides the storage used by the Implementation contract.
The value of variables declared as const
are placed in the application code of the contract instead of in storage. That's why only const
variable can be initialize.
- Implementation contract can not contain code that will self destruct the contract. If a contract is self destruct and removed from the blockchain, the Proxy contract will no longer know where to look to execute functions.
function kill() external {
selfdestruct(payable(address(0)));
}
Summary
Having a way to upgrade smart contracts could come in useful when you need to change and improve the contract code. Thanks for reading...
This content originally appeared on DEV Community and was authored by Jamiebones
Jamiebones | Sciencx (2022-04-05T13:43:06+00:00) Creating Upgradable Solidity Contract With Hardhat. Retrieved from https://www.scien.cx/2022/04/05/creating-upgradable-solidity-contract-with-hardhat/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.