WTF Solidity 49. UUPS
I am currently relearning Solidity to solidify some of the details and create a "WTF Solidity Tutorial" for beginners (advanced programmers may want to find another tutorial). I will update 1-3 lessons weekly.
Twitter: @0xAA_Science
Community: Discord|WeChat Group|Official website wtf.academy
All code and tutorials are open source on Github: github.com/AmazingAng/WTFSolidity
In this lesson, we will introduce another solution to the selector clash problem in proxy contracts: the Universal Upgradeable Proxy Standard (UUPS). The teaching code is simplified from UUPSUpgradeable
provided by OpenZeppelin
and should NOT BE USED IN PRODUCTION.
UUPS
In the previous lesson, we learned about "selector clash", which refers to the presence of two functions with the same selector in a contract, which can cause serious consequences. As an alternative to transparent proxies, UUPS can also solve this problem.
UUPS (Universal Upgradeable Proxy Standard) puts the upgrade function in the logic contract. This way, if there is a "selector clash" between the upgrade function and other functions, an error will occur during compilation.
The following table summarizes the differences between regular upgradeable contracts, transparent proxies, and UUPS:
UUPS contract
First, let's review WTF Solidity Minimalist Tutorial Lesson 23: Delegatecall. If user A delegatecall
s contract C (logic contract) through contract B (proxy contract), the context is still the context of contract B, and msg.sender
is still user A rather than contract B. Therefore, the UUPS contract can place the upgrade function in the logical contract and check whether the caller is admin.
UUPS proxy contract
The UUPS proxy contract looks like an unupgradable proxy contract and is very simple because the upgrade function is placed in the logic contract. It contains three variables:
implementation
: address of the logic contract.admin
: address of the admin.words
: string that can be changed by functions in the logic contract.
It contains 2
functions:
- Constructor: initializes the admin and logic contract address.
fallback()
: callback function that delegates the call to the logic contract.
contract UUPSProxy {
// Address of the logic contract
address public implementation;
// Address of admin
address public admin;
// A string, which can be changed by the function of the logic contract
string public words;
// Constructor function, initialize admin and logic contract addresses
constructor(address _implementation){
admin = msg.sender;
implementation = _implementation;
}
// Fallback function delegates the call to the logic contract
fallback() external payable {
(bool success, bytes memory data) = implementation.delegatecall(msg.data);
}
}
UUPS Logic Contract
The UUPS logic contract is different from the one in Lesson 47 in that it includes an upgrade function. The UUPS logic contract contains 3
state variables to be consistent with the proxy contract and prevent slot conflicts. It includes 2
functions:
upgrade()
: an upgrade function that changes the logic contract addressimplementation
, which can only be called by theadmin
.foo()
: The old UUPS logic contract will change the value ofwords
to"old"
, and the new one will change it to"new"
.
// UUPS logic contract(upgrade function inside logic contract)
contract UUPS1{
// consistent with the proxy contract and prevent slot conflicts
address public implementation;
address public admin;
// A string, which can be changed by the function of the logic contract
string public words;
// change state variable in proxy, selector: 0xc2985578
function foo() public{
words = "old";
}
// upgrade function, change logic contract's address, only admin is permitted to call. selector: 0x0900f010
// in UUPS, logic contract HAS TO include a upgrade function, otherwise it cannot be upgraded any more.
function upgrade(address newImplementation) external {
require(msg.sender == admin);
implementation = newImplementation;
}
}
// new UUPS logic contract
contract UUPS2{
// consistent with the proxy contract and prevent slot conflicts
address public implementation;
address public admin;
// A string, which can be changed by the function of the logic contract
string public words;
// change state variable in proxy, selector: 0xc2985578
function foo() public{
words = "new";
}
// upgrade function, change logic contract's address, only admin is permitted to call. selector: 0x0900f010
// in UUPS, logic contract HAS TO include a upgrade function, otherwise it cannot be upgraded any more.。
function upgrade(address newImplementation) external {
require(msg.sender == admin);
implementation = newImplementation;
}
}
Implementation with Remix
- Deploy the upgradeable implementation contracts
UUPS1
andUUPS2
.
- Deploy the upgradeable proxy contract
UUPSProxy
and point theimplementation
address to the old logic contractUUPS1
.
- Use the selector
0xc2985578
to call thefoo()
function in the proxy contract, which will delegate the call to the old logic contractUUPS1
and change the value ofwords
to"old"
.
- Use an online ABI encoder, like HashEx, to get the binary encoding and call the upgrade function
upgrade()
, which will change theimplementation
address to the new logic contractUUPS2
.
- Using the selector
0xc2985578
, call thefoo()
function of the new logic contractUUPS2
in the proxy contract, and change the value ofwords
to"new"
.
Summary: In this lesson, we introduced another solution to the "selector clash" in proxy contracts: UUPS. Unlike transparent proxies, UUPS places upgrade functions in the logic contract, making "selector clash" unable to pass compilation. Compared to transparent proxies, UUPS is more gas-efficient but also more complex.