Revamping the Foundation: Enhancing Smart Contract and Proxies for Future-proof Performance [2/8]
In our previous article, we discussed the basics, definitions, and the potential issues that can arise when using Smart Contracts without Proxies. In this article, we will address two specific topics. If you are already familiar with these topics, you may wish to move on to the next series, which covers more advanced subjects:
-Standards for Smart Contract Upgrades
-Eternal Storage without Proxy
Standards for Smart Contract upgrades
These are the various criteria that have developed for upgrading Smart Contracts:
- Eternal Storage Pattern:
In fact, the original proposal came from Elena Dimitrova on this Blog - We trace the origin of the expansion with Proxies to where it began (seemingly):
the upgradeable.sol gist from Nick Johnson, Lead developer of ENS & Ethereum Foundation alum. - EIP-897: ERC DelegateProxy
Created 2018–02–21 by Jorge Izquierdo and Manuel Araoz - EIP-1822: Universal Upgradeable Proxy Standard (UUPS)
Created 2019–03–04 by Gabriel Barros and Patrick Gallagher - EIP-1967: Standard Proxy Storage Slots
Created 2019–04–24 by Santiago Palladino That’s OpenZeppelin is using. - EIP-1538: Transparent Contract Standard
Created 2018–10–31 by Nick Mudge - EIP-2535: Diamond Standard
Created 2020–02–22 by Nick Mudge - Not really a standard, but I think Metamorphic Smart Contracts should be covered as well. Those are Smart Contracts that get re-deployed to the same address with different logic using EIP-1014 CREATE2. It’s said to be wild magic in Ethereum.
It is crucial for me to comprehend the fundamental workings of Smart Contracts, which is why I will only use the essential components of the architecture to explain the Storage Patterns concept. There is no possession, management, or administration involved — solely the underlying theory. If you require a complete and readily applicable solution, please consider OpenZeppelin.
Eternal Storage without Proxy
The initial challenge to address is the loss of data that occurs during the process of re-deployment. To resolve this issue, it is suggested to separate the logic from the storage. The question is how to accomplish this task?
The approach involves transitioning from the left side of the diagram to the right side.
The Eternal Storage pattern involves relocating the storage via setters and getters to a distinct Smart Contract. Only the logic Smart Contract can read from and write to this storage. You may use a Smart Contract that exclusively deals with the variables required or generalize it based on variable types. I will demonstrate this with an example that is based on Elena Dimitrova’s work, but simplified to focus on the core concept. These Smart Contracts are not exhaustive but feature the most crucial components to comprehend the underlying mechanics. Note that I have converted them to Solidity 0.8.1. This is an example of what it may look like:
//SPDX-License-Identifier: MIT
pragma solidity 0.8.1;
contract EternalStorage{
mapping(bytes32 => uint) UIntStorage;
function getUIntValue(bytes32 record) public view returns (uint){
return UIntStorage[record];
}
function setUIntValue(bytes32 record, uint value) public
{
UIntStorage[record] = value;
}
mapping(bytes32 => bool) BooleanStorage;
function getBooleanValue(bytes32 record) public view returns (bool){
return BooleanStorage[record];
}
function setBooleanValue(bytes32 record, bool value) public
{
BooleanStorage[record] = value;
}
}
library ballotLib {
function getNumberOfVotes(address _eternalStorage) public view returns (uint256) {
return EternalStorage(_eternalStorage).getUIntValue(keccak256('votes'));
}
function setVoteCount(address _eternalStorage, uint _voteCount) public {
EternalStorage(_eternalStorage).setUIntValue(keccak256('votes'), _voteCount);
}
}
contract Ballot {
using ballotLib for address;
address eternalStorage;
constructor(address _eternalStorage) {
eternalStorage = _eternalStorage;
}
function getNumberOfVotes() public view returns(uint) {
return eternalStorage.getNumberOfVotes();
}
function vote() public {
eternalStorage.setVoteCount(eternalStorage.getNumberOfVotes() + 1);
}
}
This is a straightforward Smart Contract for voting purposes. You can call the vote() function to increment a number — it is a simple business logic. However, the real magic happens behind the scenes. First, we must deploy the Eternal Storage Smart Contract, which remains static and unchanged.
Next, we deploy the Ballot Smart Contract, which will utilize the library and the Ballot Contract to perform the actual logic.
Behind the scenes, a library executes a delegatecall, running the library’s code within the context of the Ballot Smart Contract. If msg.sender is utilized within the library, it retains the same value as it would within the Ballot Smart Contract. To validate this concept, let’s conduct a few votes within the new Ballot Instance.
Suppose a bug is identified, where every voter has the ability to cast an unlimited number of votes. We rectify this issue and solely redeploy the Ballot Smart Contract (disregarding the fact that the prior version remains active and there is no way to cease it without supplementary code). The code below replaces everything.
//SPDX-License-Identifier: MIT
pragma solidity 0.8.1;
contract EternalStorage{
mapping(bytes32 => uint) UIntStorage;
function getUIntValue(bytes32 record) public view returns (uint){
return UIntStorage[record];
}
function setUIntValue(bytes32 record, uint value) public
{
UIntStorage[record] = value;
}
mapping(bytes32 => bool) BooleanStorage;
function getBooleanValue(bytes32 record) public view returns (bool){
return BooleanStorage[record];
}
function setBooleanValue(bytes32 record, bool value) public
{
BooleanStorage[record] = value;
}
}
library ballotLib {
function getNumberOfVotes(address _eternalStorage) public view returns (uint256) {
return EternalStorage(_eternalStorage).getUIntValue(keccak256('votes'));
}
function getUserHasVoted(address _eternalStorage) public view returns(bool) {
return EternalStorage(_eternalStorage).getBooleanValue(keccak256(abi.encodePacked("voted",msg.sender)));
}
function setUserHasVoted(address _eternalStorage) public {
EternalStorage(_eternalStorage).setBooleanValue(keccak256(abi.encodePacked("voted",msg.sender)), true);
}
function setVoteCount(address _eternalStorage, uint _voteCount) public {
EternalStorage(_eternalStorage).setUIntValue(keccak256('votes'), _voteCount);
}
}
contract Ballot {
using ballotLib for address;
address eternalStorage;
constructor(address _eternalStorage) {
eternalStorage = _eternalStorage;
}
function getNumberOfVotes() public view returns(uint) {
return eternalStorage.getNumberOfVotes();
}
function vote() public {
require(eternalStorage.getUserHasVoted() == false, "ERR_USER_ALREADY_VOTED");
eternalStorage.setUserHasVoted();
eternalStorage.setVoteCount(eternalStorage.getNumberOfVotes() + 1);
}
}
As you can observe, solely the Library has undergone modifications. The Storage remains unaltered. But how do we deploy the update? We simply redeploy the “Ballot” Smart Contract and provide it with the Storage Contract’s address. That is all.
There are no modifications made to the Storage Contract, hence there is no requirement to redeploy it. Simply utilize the existing one! As demonstrated in the screenshot, a user can only vote once. If they attempt to vote again, their account is flagged and an error (3) is displayed.
As a result of this technique, the original Storage Smart Contract by Elena comprises additional variable types, as uint and boolean alone would be inadequate.
Although this approach seems promising, it does come with a few pros and cons.
Pros:
- This technique is comparatively easy to comprehend, as it does not require any complex assembly manipulations. If you have a background in conventional software development, these patterns would appear quite recognizable to you.
- The approach could function even in the absence of Libraries, by utilizing a separate Storage Smart Contract running under its unique address.
- This method eliminates the need for Storage Migration following Smart Contract updates
Cons:
- The approach of accessing variables through this pattern can be quite complicated.
- This method cannot be directly applied to existing Smart Contracts such as Tokens.
Recap
In some cases, a simple solution like the Eternal Storage pattern can be very effective. It may be preferable to use a simpler approach, especially when it comes to Smart Contracts. For example, Morpher.com’s Smart Contracts MorpherState and MorpherToken are linked using basic getters and setters, which can be easily audited and understood.
However, other projects may use the proxy pattern where the address of the updated Smart Contract remains constant. This will be discussed next.
Link to previous article :
Revamping the Foundation: Enhancing Smart Contract and Proxies for Future-proof Performance [1/10]
discord: am.dd#3991