Proxy Pattern and Upgradeable Smart Contracts

Alvaro Serrano Rivas
Coinmonks
10 min readFeb 12, 2022

--

When smart contracts are deployed to the Ethereum blockchain, they are immutable and, therefore, not upgradeable. However, the code can be rearchitected into different contracts, thus allowing for logic upgrades while the storage remains the same. Having said that, do users agree that the token logic should be upgradeable?

Immutability comes with the drawback that bugs will not be fixed, gas optimizations won’t be implemented, existing functionality will not be improved… Ditching this property of the EVM would be a bad solution as well, as it would rob Ethereum one of its core features.

When to use it

- Adapt to a changing environment: fix bugs, overcome the limitations of immutable contract
- Virtual upgrades (the existing contracts still cannot be changed). This means that despite the original contracts remain unmodified, a new version can be deployed and its address replaces the old one in storage
- To avoid breaking dependencies of other contracts that are referencing the upgraded contract
- Users might not know about the release of a new contract version (which comes with a new address)

How it works

First, an external caller makes a function call to the proxy. Second, the proxy delegates the call to the delegate, where the function code is located. Third, the result is returned to the proxy, which forwards it to the caller. Because the `delegatecall` is used to delegate the call, the called function is executed in the context of the proxy. This means that the storage of the proxy is used for function execution, thus resulting in the limitation that the storage of the delegate contract has to be append only. The `delegatecall` opcode was introduced in EIP-7

Given the logic of the pattern, the proxy is also know as a dispatcher that delegates calls to the specific modules. These modules are known as the delegates (as the work is delegated to them by the proxy contract).

What does it mean that the storage of the proxy is used for function execution?

We know that the result of this behavior is the limitation that the storage of the delegate contract has to be append only. Now, in the case of an upgrade, existing storage variables cannot be changed or omitted. Instead, only new variables can be added.

The reason for that is because changing the storage structure in the delegate would mess up storage in the proxy, which is expecting the previous structure.

Here is one example

What about the context?

The execution context stays the same since the storage of the caller is used. Therefore, `msg.sender` and `msg.value` don’t change.

Divide smart contracts into several related smart contracts

This is a common approach used in languages other than Solidity. For example, you can have a contract for the sale of tokens in which case the rules for calculating the number of tokens that need to be sent to the wallet the Ether came from are not clearly specified. In this case, the amount calculations are done in a separate contract that can later on be upgraded if necessary.

The basics of contract functions calls

Every transaction in Ethereum has an optional `data` field which must be left empty when ethers are being transferred. However, when interacting with a contract must contain something called `call data`.
- Function identifier (first 4 bytes of the hashed function signature) e.g. `keccak256(“transfer(address, uint256)”)`
- Function arguments that come after the function identifier and that are encoded according the the ABI specification

The Solidity compiler has a branching logic that parses call data and decides which function to call depending on the function identifier extracted form call data. Since Solidity won’t allow us to make decisions on that level of deepness, we will later on show that we will be forced to use assembly to write some logic.

How to implement the proxy delegate pattern

Essentially, the business logic is implemented in the functions that are called from the proxy contract. As a result of such chaining, it becomes possible to swap the implementation contract with a different on. This is because the proxy contract only knows the address of the contract that is implementing the actual business logic.
Since Solidity is a high-level abstraction, all it can gives us is a fallback function. This is a special function that is called whenever a function unsupported by a contract is called.

Ultimately, our goal with upgradeable contracts is to get `call data` and pass it to the implementation contract as is, without parsing or modifying it.

First, we need to load call data into memory


let ptr := mload(0x40)
calldatacopy(ptr, 0, calldatasize())

The EVM’s memory is handled in slots. Every slots has an index and occupies 32 bytes. The `calldatasize()` function above gets the call data size and copies the call data of a specific size to a memory slot located at an index `ptr`, occupying other memory slots if it doesn't fit. `mload` reads 32 bytes from the specified index. The `0x40` is a special slot that points to the index of the next free memory slot, so that we can save call data into a free slot in memory. Next the sload function will read the value at that address. The following 2 zeroes are `out` and `outsize` respectively, and they allow to define where in memory to store the return data

The way to relay the call would be something like this:

let result := delegatecall(
gas(),
sload(implementation.slot),
ptr,
calldatasize(),
0,
0
)

`gas` means how much gas is remaining in the current contract call and tells the other contract how much of it it is allowed to spend. The `implementation` state variable is a feature of Solidity that allows to easily get the memory slot address of a state variable.

What `delegatecall` actually means

A `delegatecall` is used to execute functions at a delegate in the context of the proxy structure. This means that `msg.data` is forwarded (function identifier in the first 4 bytes). After being forwarded, in order to be executed and trigger the forwarding mechanism for every function call, it is placed in the proxy contract’s fallback function. However, the `delegatecall` only returns a boolean, telling us whether the execution was successful or not.

To solve this limitation, inline assembly is used. This allows for more granular control over the stack wit a language similar to the one used by the EVM. Using inline assembly we can dissect the return value of the `delegatecall` and return the result to the caller.

Can we circumvent the need for using inline assembly?

This can be avoided by returning the result to the caller via events. Since events cannot be listened to or from within the contracts, we would use a fronted and act according to the result from there on.

What would it look like without calling `delegatecall`?

When we want the proxy contract to return whatever was returned from the callee and we do not know the return data type in advance, we can use a code snippet like the one below:


fallback() external {
assembly {
let ptr := mload(0x40)
calldatacopy(ptr, 0, calldatasize())

let result := delegatecall(
gas(),
sload(implementation.slot),
ptr,
calldatasize(),
0,
0
)

let size := returndatasize()

returndatacopy(ptr, 0, size)

switch result
case 0 {
revert(ptr, size)
}
default {
return(ptr, size)
}
}
}

How to implement the delegate?

The delegate can be implemented just like any other regular contract. It does not need to know about the proxy using its code. The only thing to be aware is that while upgrading the contract, the storage sequence has to be the same. Remember that only additions are permitted.

Hence, the upgrading mechanism, storing of the current version of the delegate, can either happen in external storage or in the proxy itself.
- If the address is stored in the proxy, a guarded function needs to be implemented. This lets an authorized address update the delegate address.

This would be an example that stores the current version of the delegate in its own storage


contract Proxy{
address delegate; // store the address of the delegate
address owner = msg.sender // store the address of the owner
/// @notice this function allows a new version of the delegate being used without the caller having to worry about it
function upgradeDelegate(address _newDelegateAddress) public {
require(msg.sender == owner);
delegate = _newDelegateAddress;
}
function () external payable {
assembly {
let _target := sload(0)
calldatacopy(0x0, 0x0, calldatasize)
let result := delegatecall(gas, _target, 0x0, calldatasize, 0x0, 0)
returndatacopy(0x0, 0x0, returndatasize)
switch result case 0 {revert(0,0)} default {return (0, returndatasize)}
}
}
}

The forwarding mechanism comes into place in the second function, which is the fallback function being called for every unknown function identifier. Consequently, every function call to the proxy will trigger this function and execution the assembly code.

- Line 14 loads the first variable in storage, which is the address of the delegate, and stores it in the memory variable `_target`
- Line 15 copies the function signature and any parameters into memory
- Line 16 makes the `delegatecall` to the `_target` address, including the function data that has been stored in memory
- Line 17 copies the return value into memory
- The switch statement checks the execution boolean outcome.
— If the outcome is positive, the result is returned to the caller of the function
— Otherwise, any state changes are reverted


contract Delegate {
uint public n = 1;
function add() public {
n = 5;
}
}

contract Caller {
Delegate proxy;
function caller(address _proxyAddress) public {
proxy = Delegate(_proxyAddress);
}
function go() public {
proxy.adds();
}
}

call vs delegatecall, state vs logic

Smart contract’s state is persistent and it is stored in the blockchain. This state is accessible via state variables. Both `call` and `delegatecall` are used to call another contract. However, they differ in how they handle the callee contract’s state
- When using `call`, caller and callee have their own separate states (this is expected by default)
- When using `delegatecall`, the callee uses the caller’s state, which means that the contract you are calling with `delegatecall` uses the state of the caller contract.

Callee contract initialization

It is important to mention that the callee contract constructor cannot be used for initialization when used via a proxy. When we use the constructor to initialize state, we want it to be initialized within the state of the proxy contract. A workaround would look like:

contract BusinessLogic {
bool initialized;
uint 256 someNumber;
function init() public {
require(!initialized, “already initialized”);
someNumber = 0x42;
initialized = true;
}
}

Implications of using this pattern

1. Increase in complexity due to inline assembly
2. The complexity of the pattern increases the chances of bugs or unexpected behaviors
3. Storage changes: fields cannot be rearranged nor removed
4. Potential loss of trust from users. With upgradeable contracts, one of the key benefits of blockchains, which is immutability, is avoided.
5. Users have to trust the responsible entities not to introduce any unwanted behavior on their upgrades
6. It is necessary to carefully delimit access to a function that changes the address of the active contract

Use cases

- Big Dapps containing a large number of contracts. One example could be prediction markets, where users bet on the outcome of future events. In this case the address of the upgradeable contract is not stored in the proxy itself, but in some kind of address resolver.
- Solve bugs found in the contract
- Solve errors that might lead to a loss of funds in the contract

Examples

- The Upgradeable contract verifies that the target (address of the active contract version) is stored in the same slot as in the current version.
- Validations can be implemented to other storage fields
- Before deploying these contracts to the network, it is necessary to test all options. Otherwise you could end up without a functioning contract after the next update and it would be impossible for you to update it.
- Entire workflow

Conclusions

To create upgradeable smart contracts, the proxy pattern seems to be a well-rounded strategy. It allows developers to separate the upgradeable mechanism from the contract design. This simplifies the logic design and will be less prone to errors. No one strategy is perfect and making the right choice will depend on use cases. All strategies and design patterns are complex on their own and application developers must always avoid security vulnerabilities.

Join Coinmonks Telegram Channel and Youtube Channel learn about crypto trading and investing

Also Read

--

--