There are situations when you want to upgrade a Solidity smart contract. You might want to fix a software vulnerability, change the logic of the contract or add a new feature. As you are aware smart contracts are immutable and once they are deployed to the blockchain they cannot be changed. If you deploy a new version of the contract you also start with an empty storage. In this tutorial we will review several methods that will allow you to upgrade a Solidity smart contract. These methods will work on Ethereum, Binance Smart Chain, Polygon, or any other EVM compatible blockchain.
Upgrade Method 1 – Proxy contract using a delegate call
For this first upgrade method users will interact with a proxy contract that does not contain any business logic. The proxy contract will then interact with the actual contract to execute all calls. To have the proxy contract interact with the actual contract we will use a delegate call. A delegate call will allow you to execute a function in the context of another contract.
In the event of an upgrade the proxy contract is still used. Users will interact with the same proxy contract and all data remains stored in state. To upgrade business logic you would create a new smart contract that the proxy interacts with. The proxy contract does not contain any business logic. This method separates stored data (proxy contract) and business logic (separate contract).
Proxy contract using a delegate call process flow:
- Bob deploys smart contractV1 which contains business logic
- Then he deploys a proxy contract that is built to call smart contractV1
- Users interact with the proxy contract and fallback function calls smart contractV1
- All data is stored in the proxy contract
- Bob wants to change functionality in his smart contract so he deploys smart contractV2
- He updates his Proxy contract to point to the new contractV2 address
To test a proxy contract using a delegate call perform the following steps:
- Deploy the smartContractV1 contract below
- Deploy the proxy contract and set the SMARTCONTRACTWITHLOGIC to the smartContractV1 address
- Test the V1 contract
- Then deploy the smartContractV2 contract below
- Using the Proxy upgrade function to set the address to the smartContractV2 contract
- Test the V2 contract
Proxy contract
pragma solidity ^0.8.6;
contract sampleProxy {
//two assembly memory slots locations
bytes32 private constant _OWNER_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;
bytes32 private constant _SMARTCONTRACTWITHLOGIC_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
constructor() {
bytes32 slot = _OWNER_SLOT;
address _admin = msg.sender;
assembly {
//allows you to store a value in storage
sstore(slot, _admin)
}
}
//address of the owner
function admin() public view returns (address owner) {
bytes32 slot = _OWNER_SLOT;
assembly {
//read a value in storage
owner := sload(slot)
}
}
//address of the contract with business logic
function SMARTCONTRACTWITHLOGIC() public view returns (address contractwithlogic) {
bytes32 slot = _SMARTCONTRACTWITHLOGIC_SLOT;
assembly {
contractwithlogic := sload(slot)
}
}
//function used to change the address of the contract containing business logic
function upgrade(address newContract) external {
//verify the sender is the admin
require(msg.sender == admin(), 'You must be an owner only');
bytes32 slot = _SMARTCONTRACTWITHLOGIC_SLOT;
assembly {
//store in memory the new address
sstore(slot, newContract)
}
}
//user calls a function that does not exist in this contract so the fallback function is called
//assembly is used
fallback() external payable {
assembly {
//get the address of the contract that contains business logic
//save address in temporary memory
let _target := sload(_SMARTCONTRACTWITHLOGIC_SLOT)
//copy the function call in memory
//first parameter is the memory slot we want to copy the function call to
//second parameter is the memory slot we want to copy from
//third parameter is the size we want to copy which is all data
calldatacopy(0x0, 0x0, calldatasize())
//forward the call to the smart contract that contains the business logic
//specify the gas, address of contract, function we want to call and size
//if the call is successful it will be stored in the bool result
let result := delegatecall(gas(), _target, 0x0, calldatasize(), 0x0, 0)
//copy the return data into memory
returndatacopy(0x0, 0x0, returndatasize())
//if the result is 0 and failed then revert
switch result case 0 {revert(0, 0)} default {return (0, returndatasize())}
}
}
}
Try on Remix
smartContractV1 and smartContractV2
pragma solidity ^0.8.6;
contract smartContractV1 {
uint public age;
//set
function setAge(uint newAge) external {
age = newAge;
}
}
pragma solidity ^0.8.6;
//in the upgraded contract you need to keep all existing state variables in the same order
//add new state variables below existing state variables or you will overwrite data
contract smartContractV2 {
uint public age1;
uint public age2;
//set
function setAge1(uint newAge1) external {
age1 = newAge1;
}
//set
function setAge2(uint newAge2) external {
age2 = newAge2;
}
}
Try on Remix
Upgrade Method 2 – the interface pattern
For the second upgrade method a smart contract will use an interface to call a function in another contract. The goal is to abstract a contract implementation behind an interface that only defines its function signatures. This is a well known pattern in object oriented programming so some developers will be familiar with this concept of abstraction.
This main contract contains most of the business logic but interfaces with one or many contracts to perform more functions. For example a smart contract that performs a flash loan arbitrage on Uniswap and SushiSwap. Some of the contracts main functions rely on other satellite contracts for execution. At any time you can deploy a new satellite contract and update its address in the main contract.
Users call functions in the main contract which might execute the function itself or interface with another contract for execution. A contract can interface with one or many contracts are and can use a combination of their functions. This is different from a proxy contract using a delegate call because business logic exists in the main contract.
To upgrade the contract you cannot change any functionality in the main contract but you can change the functionality in a satellite contract. as long as it respects the interface. Then update the address of the satellite contract in the main contract. This is a very popular patter to follow.
The smart contract interface pattern process flow:
- Bob deploys main contract that imports a defined interface
- The main contract is responsible for business logic and storage
- Then he deploys a satelliteV1 contract
- He configures the satelliteV1 address in the main contract
- Users interact with the main contract.
- The main contract has functions implemented and calls additional functions in the satelliteV1 contract
- Bob wants to change business logic in his smart contract so he deploys satelliteV2 contract
- He updates his main contract with the satelliteV2 contract address
To test the interface pattern perform the following steps:
- Deploy the main contract below
- Deploy the satelliteV1 contract
- Call the upgrade function of the main contact using the address of the satelliteV1 contract
- Test the main contract by calling the getAge function
- Then deploy the satelliteV2 contract below
- Call the upgrade function of the main contact using the address of the satelliteV2 contract
- Test the main contract by calling the getAge function
Main Contract with defined interface
pragma solidity ^0.8.6;
//defined interface needed to interact with other contract
interface Ibusinesslogic {
function getAge() external pure returns(uint);
}
contract MainContract {
//set an admin address
address public admin;
//interface contract address
Ibusinesslogic public businesslogic;
//the admin is the owner
constructor() {
admin = msg.sender;
}
//function to upgrade the contract to point to execute function
function upgrade(address _businesslogic) external {
require(msg.sender == admin, 'only admin');
businesslogic = Ibusinesslogic(_businesslogic);
}
//call the getAge function using the businesslogic function
function getAge() external view returns(uint) {
return businesslogic.getAge();
}
}
Try on Remix
Satellite contracts with defined interface
pragma solidity ^0.8.6;
//defined interface needed to interact with other contract
interface Ibusinesslogic {
function getAge() external pure returns(uint);
}
pragma solidity ^0.8.6;
//satelliteV1 uses the Ibusinesslogic interface
contract satelliteV1 is Ibusinesslogic {
function getAge() override external pure returns(uint) {
return 25;
}
}
pragma solidity ^0.8.6;
//satelliteV2 uses the Ibusinesslogic interface
contract satelliteV2 is Ibusinesslogic {
function getAge() override external pure returns(uint) {
return 32;
}
}
Try on Remix
Upgrade Method 3 – store all data in a storage contract
The third upgrade method is to use one contract for all business logic and a second contract for storing data. This use case is valuable when you want to upgrade a smart contract and do not care that the address changes but want to preserve all data. Users interact with the business logic contract and data is saved in the storage contract.
When you upgrade you cannot change any of the functionality in the storage contract but you can replace the business logic contract. The business logic contract is responsible for all logic and interacting with the storage contract by getting and setting data.
The using two contract (business logic and data storage) pattern process flow:
- Bob deploys a userStorage contract
- The userStorage contracts purpose is to save data
- Bob deploys the userContract
- The userContract is responsible for business logic and sends requests to the userStorage contract
- He configures the userStorage address in the userContract
- He configures the userContract address in the userStorage contract. This adds a level of security so only authorized contracts can update data.
- Users interact with the userContract which calls functions and stores data in the userStorage contract
- Bob wants to change functionality in his userContract so he deploys a new usercontractV2
- He updates his userContract address in the userStorage contract and the userStorage address in the userContract
To test this pattern using two contract (business logic and data storage) perform the following steps:
- Deploy the userStorage contract below
- Deploy the userContract below
- In the userStorage contract call the allowAccess function using the userContract address. This will give the userContract permission to write to the userStorage contract.
- In the userContract call the setStorageContract function using the userStorage address. This instructs the contract where to get and set data.
- Test the userContract by setting the age variable then getting the age variable.
User Storage Contract
pragma solidity ^0.8.6;
//this contract is used to store data
contract UserStorage {
//a mapping to determine which contract has access to write data to this contract
//used in the modifier below
mapping(address => bool) accessAllowed;
uint private age;
//a basic mapping that allows one to set an address and a bool value
//for example - is this address registered on the platform?
mapping(address => bool) addressSet;
//function modifier checks to see if an address has permission to update data
//bool has to be true
modifier isAllowed() {
require(accessAllowed[msg.sender] == true);
_;
}
//access is allowed to the person that deployed the contract
function UserStorageAccess() public {
accessAllowed[msg.sender] = true;
}
//set an address to the accessAllowed map and set bool to true
//uses the isAllowed function modifier to determine if user can change data
//this function controls which addresses can write data to the contract
//if you update the UserContract you would add the new address here
function allowAccess (address _address) isAllowed public {
accessAllowed[_address] = true;
}
//set an address to the accessAllowed map and set bool to false
//uses the isAllowed function modifier to determine if user can change data
//this function controls which addresses need to have thier write access removed from the contract
//if you update the UserContract you would set the old contract address to false
function denyAccess (address _address) isAllowed public {
accessAllowed[_address] = false;
}
//gets an address from the addressSet map and displays true or false
function getAddressSet (address _address) public view returns(bool) {
return addressSet[_address];
}
//sets an address to the addressSet map and sets the bool true or false
//uses the isAllowed function modifier to determine if user can change data
function setAddressSet (address _address, bool _bool) isAllowed public {
addressSet[_address] = _bool;
}
//get the age from the age variable
function getAge () public view returns (uint) {
return age;
}
//set an age to the age variable
//uses the isAllowed function modifier to determine if user can change data
function setAge(uint newAge) isAllowed public {
age = newAge;
}
}
Try on Remix
User contract that contains business logic
pragma solidity ^0.8.6;
//logic is in the UserContract and data storage is in the UserStorage contract
//if we want to upgrade the usercontract we can and will not loose any data
contract UserContract {
UserStorage userStorage;
//set the address of the storage contract that this contract should user
//all functions will read and write data to this contract
function setStorageContract(address _userStorageAddress) public {
userStorage = UserStorage(_userStorageAddress);
}
//reads the addressSet map in the UserStorage contract
function isMyUserNameRegistered() public view returns(bool) {
return userStorage.getAddressSet(msg.sender);
}
//writes to the addressSet map in the UserStorage contract
function registerMe() public {
userStorage.setAddressSet(msg.sender, true);
}
//set the age in the storage contract
function setAge(uint newAge) public {
userStorage.setAge(newAge);
}
//get the age in the storage contract
function getAge() public view returns (uint){
return userStorage.getAge();
}
}
Try on Remix
Although Solidity code is immutable one can implement a method to work around this concept and have code in multiple contracts to have mutability. This allows one to upgrade a Solidity smart contract.
Resources
Blockchain Networks
Below is a list of EVM compatible Mainnet and Testnet blockchain networks. Each link contains network configuration, links to multiple faucets for test ETH and tokens, bridge details, and technical resources for each blockchain. Basically everything you need to test and deploy smart contracts or decentralized applications on each chain. For a list of popular Ethereum forums and chat applications click here.
Ethereum test network configuration and test ETH faucet information | |
Optimistic Ethereum Mainnet and Testnet configuration, bridge details, etc. | |
Polygon network Mainnet and Testnet configuration, faucets for test MATIC tokens, bridge details, etc. | |
Binance Smart Chain Mainnet and Testnet configuration, faucets for test BNB tokens, bridge details, etc. | |
Fanton networt Mainnet and Testnet configuration, faucets for test FTM tokens, bridge details, etc. | |
Kucoin Chain Mainnet and Testnet configuration, faucets for test KCS tokens, bridge details, etc. |
Web3 Software Libraries
You can use the following libraries to interact with an EVM compatible blockchain.
- Python: Web3.py A Python library for interacting with Ethereum. Web3.py examples
- Js: web3.js Ethereum JavaScript API
- Java: web3j Web3 Java Ethereum Ðapp API
- PHP: web3.php A php interface for interacting with the Ethereum blockchain and ecosystem.
Nodes
Learn how to run a Geth node. Read getting started with Geth to run an Ethereum node.
Fix a transaction
How to fix a pending transaction stuck on Ethereum or EVM compatible chain
1 thought on “Multiple ways to upgrade a Solidity smart contract”