Solidity Upgrade Patterns
An upgrade pattern is a plan for replacing a proxy logic contract with a new version. The proxy keeps its state, while the logic contract changes underneath it.
A value set before an upgrade survives it
The first logic version sets a value through the proxy. After the upgrade, the new version reads the same value.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract V1 {
uint256 public value;
function setValue(uint256 v) public {
value = v;
}
}
contract V2 {
uint256 public value;
function setValue(uint256 v) public {
value = v;
}
}
contract Proxy {
bytes32 constant IMPL_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
bytes32 constant ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;
constructor(address impl) {
_setImplementation(impl);
_setOwner(msg.sender);
}
function implementation() public view returns (address impl) {
bytes32 slot = IMPL_SLOT;
assembly { impl := sload(slot) }
}
function owner() public view returns (address admin) {
bytes32 slot = ADMIN_SLOT;
assembly { admin := sload(slot) }
}
function _setImplementation(address impl) internal {
bytes32 slot = IMPL_SLOT;
assembly { sstore(slot, impl) }
}
function _setOwner(address admin) internal {
bytes32 slot = ADMIN_SLOT;
assembly { sstore(slot, admin) }
}
function upgrade(address impl) public {
require(msg.sender == owner(), "only owner");
_setImplementation(impl);
}
fallback() external payable {
address impl = implementation();
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
}Only the owner can upgrade the proxy
A stranger trying to upgrade is refused. The owner upgrade succeeds and points the proxy at the new logic.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract V1 {
uint256 public value;
function setValue(uint256 v) public {
value = v;
}
}
contract V2 {
uint256 public value;
function setValue(uint256 v) public {
value = v;
}
}
contract Proxy {
bytes32 constant IMPL_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
bytes32 constant ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;
constructor(address impl) {
_setImplementation(impl);
_setOwner(msg.sender);
}
function implementation() public view returns (address impl) {
bytes32 slot = IMPL_SLOT;
assembly { impl := sload(slot) }
}
function owner() public view returns (address admin) {
bytes32 slot = ADMIN_SLOT;
assembly { admin := sload(slot) }
}
function _setImplementation(address impl) internal {
bytes32 slot = IMPL_SLOT;
assembly { sstore(slot, impl) }
}
function _setOwner(address admin) internal {
bytes32 slot = ADMIN_SLOT;
assembly { sstore(slot, admin) }
}
function upgrade(address impl) public {
require(msg.sender == owner(), "only owner");
_setImplementation(impl);
}
fallback() external payable {
address impl = implementation();
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
}An upgrade can add a new function
The new logic version adds an increment function. It works on the value the old version stored.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract V1 {
uint256 public value;
function setValue(uint256 v) public {
value = v;
}
}
contract V2 {
uint256 public value;
function setValue(uint256 v) public {
value = v;
}
function increment() public {
value += 1;
}
}
contract Proxy {
bytes32 constant IMPL_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
bytes32 constant ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;
constructor(address impl) {
_setImplementation(impl);
_setOwner(msg.sender);
}
function implementation() public view returns (address impl) {
bytes32 slot = IMPL_SLOT;
assembly { impl := sload(slot) }
}
function owner() public view returns (address admin) {
bytes32 slot = ADMIN_SLOT;
assembly { admin := sload(slot) }
}
function _setImplementation(address impl) internal {
bytes32 slot = IMPL_SLOT;
assembly { sstore(slot, impl) }
}
function _setOwner(address admin) internal {
bytes32 slot = ADMIN_SLOT;
assembly { sstore(slot, admin) }
}
function upgrade(address impl) public {
require(msg.sender == owner(), "only owner");
_setImplementation(impl);
}
fallback() external payable {
address impl = implementation();
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
}Example
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract V1Score {
uint256 public score;
function setScore(uint256 v) public {
score = v;
}
}
contract V2Score {
uint256 public score;
function setScore(uint256 v) public {
score = v;
}
function doubleScore() public {
score *= 2;
}
}
contract Proxy {
bytes32 constant IMPL_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
bytes32 constant ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;
constructor(address impl) {
_setImplementation(impl);
_setOwner(msg.sender);
}
function implementation() public view returns (address impl) {
bytes32 slot = IMPL_SLOT;
assembly { impl := sload(slot) }
}
function owner() public view returns (address admin) {
bytes32 slot = ADMIN_SLOT;
assembly { admin := sload(slot) }
}
function _setImplementation(address impl) internal {
bytes32 slot = IMPL_SLOT;
assembly { sstore(slot, impl) }
}
function _setOwner(address admin) internal {
bytes32 slot = ADMIN_SLOT;
assembly { sstore(slot, admin) }
}
function upgrade(address impl) public {
require(msg.sender == owner(), "only owner");
_setImplementation(impl);
}
fallback() external payable {
address impl = implementation();
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
}Key points
- Upgrade: replacing the logic contract a proxy points at.
- State kept: proxy values survive an upgrade because they live in the proxy.
- Transparent proxy: admin calls are handled by the proxy itself, while user calls are forwarded.
- UUPS proxy: the upgrade function lives in the logic contract instead of the proxy.
- Risk warning: an upgrade is never free of risk, since a layout mistake corrupts stored values.
In this example, a score contract sets a score of 10 through a proxy and then upgrades to a version with a doubling function. The upgraded version doubles the kept score to 20.
Note
compare the patterns before choosing one, and test every upgrade on a copy first.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Remix IDE
Exercise
In a UUPS proxy, where does the upgrade function live?