Yige

Yige

Build

Solidity Contract Upgrade

Contract Proxy Upgrade#

Low-Level Calls (Call / DelegateCall)#

  • Call: Calls a function of the target contract and executes it in the context of the target contract, but the storage state of the caller contract does not change.
  • DelegateCall: Calls a function of the target contract and executes it in the context of the caller contract, changing the storage state of the caller contract.

Low-Level Concepts#

  • The EVM does not care about state variables, but rather operates on storage slots. All variable state changes correspond to changes in the contents of the storage slots, so it is necessary to pay attention to slot conflicts, meaning that the slot layout of the current contract and the target contract must not conflict.

  • When a contract performs a delegatecall to a target smart contract, it executes the logic of the target contract in its own environment (equivalent to copying the code of the target contract to the current contract for execution).

Contract Creation#

There are several main ways to create contracts:

  1. Direct Deployment
contractA = new contractA(....)
  1. Factory Pattern: Operate through a factory contract, which is beneficial for batch creating similar contracts and can implement some additional logic or restrictions.
contract ContractFactory {
	function createContract(uint _value) public returns (address) {
		SimpleContract newContract = new SimpleContract(_value);
		return address(newContract);
	}
}
  1. Create2 Opcode: Allows for predicting contract addresses in advance, enabling more complex deployment strategies, such as delayed deployment.
contract Create2Factory {

	function deployContract(uint _salt, uint _value) public returns (address){
		return address(new SimpleContract{salt: bytes32(_salt)}(_value));	
	}
	
	function predictAddress(uint _salt, uint _value) public view returns (address) {
		bytes memory bytecode = abi.encodePacked(type(SimpleContract).creationCode, abi.encode(_value));
		bytes32 hash = keccak256(
			abi.encodePacked(
				bytes1(0xff),
				address(this),
				_salt,
				keccak256(bytecode)
			)
		);
		return address(uint160(uint(hash)));
	}
}
  1. Minimal Proxy Pattern (EIP-1167): Creates a very lightweight proxy contract that points to an already deployed implementation contract, significantly saving gas.
contract MinimalProxy {

	function clone(address target) external returns (address result) {
		// Convert the target contract's address to bytes20
		bytes20 targetBytes = bytes20(target);
		// Minimal proxy bytecode
		bytes memory minimalProxyCode = abi.encodePacked(
			hex"3d602d80600a3d3981f3363d3d373d3d3d363d73",
			targetBytes,
			hex"5af43d82803e903d91602b57fd5bf3"
		);
		
		// Use create to deploy a new proxy contract
		assembly {
			result := create(0, add(minimalProxyCode, 0x20), mload(minimalProxyCode))
		}
	}
}
  1. Proxy Pattern (Upgradeable Contracts): Creates upgradeable contracts that allow for updating contract logic without changing the contract address, suitable for complex systems that require long-term maintenance and updates.

contract UpgradeableContract is Initializable {

	uint public value;
	function initialize(uint _value) public initializer {
		value = _value;
	}
}

contract ProxyFactory {

	function deployProxy(address _logic, address _admin, bytes memory _data) public returns (address) {
		TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(_logic, _admin, _data);
		return address(proxy);
	}
}

Issues to Consider in Contract Upgrades#

Variable Issues#

  • Do not arbitrarily change variables when upgrading contracts, including types, definition order, etc. If new contract variables are added, they can be appended at the end with bytes32(uint(keccak256("eip1967.proxy.implementation")) - 1), to specify the slot to a position that will not cause conflicts.

  • Ensure that variables in the contract are initialized in the initialize function, rather than through the constructor, for example:


contract MyContract {
	uint256 public hasInitialValue = 42; 
}

should be changed to:


contract MyContract is Initializable {

	uint256 public hasInitialValue;
	function initialize() public initializer {
		hasInitialValue = 42; // set initial value in initializer
	}
}

Constructor Issues#

  1. To prevent the implementation contract from being directly initialized. In the proxy pattern, we want only the proxy contract to be able to call the initialize function, not the implementation contract itself. We need to define a regular function, usually called initialize, to replace the constructor, but since regular functions can be called multiple times, we also need to ensure that this function can only be called once.
  2. If inheriting from a parent contract, we also need to ensure that the parent contract's initialize function can only be called once during initialization.
  3. We can call the disableInitializers function in the constructor: constructor() { _disableInitializers();}.

The purpose is to:

  • Disable the initializer constructor to prevent malicious attackers from directly calling the implementation contract's initialize function, leading to unintended or malicious state changes.
  • During upgrades, the new implementation contract should not be initialized, as it will inherit the state of the previous version. Disabling the initializer can prevent accidental reinitialization of the contract during upgrades.

Selfdestruct Issues#

Through the proxy contract, there is no direct interaction with the underlying logic contract. Even if there are malicious actors sending transactions directly to the logic contract, it does not matter because we manage the storage state assets through the proxy contract. Changes to the storage state of the underlying logic contract will not affect the proxy contract. However, one point to note is:

If the logic contract triggers a selfdestruct operation, then the logic contract will be destroyed, and the proxy contract will no longer be usable, as delegatecall will not be able to call a destroyed contract.

Similarly, if the logic contract contains a delegatecall that can be controlled externally, this could be maliciously exploited. An attacker could make this delegatecall point to a malicious contract containing selfdestruct. Since it is called through delegatecall, the selfdestruct is executed in the context of the logic contract, which will actually destroy the calling contract (in this case, the logic contract), leading to the destruction of the logic contract.

For example:

// The attacker can set the implementation to the address of MaliciousContract and then call the delegateCall function with the destroy function's call data
// Causing LogicContract to execute MaliciousContract's destroy function, thereby destroying LogicContract itself

contract LogicContract {

	address public implementation;
	function setImplementation(address _impl) public {
		implementation = _impl;
	}

	function delegateCall(bytes memory _data) public {
		(bool success, ) = implementation.delegatecall(_data);
		require(success, "Delegatecall failed");
	}
}

contract MaliciousContract {
	function destroy() public {
		selfdestruct(payable(msg.sender));
	}
}

Therefore, it is necessary to prohibit the use of selfdestruct or delegatecall in logic contracts. The Ethereum community is discussing the possibility of completely removing selfdestruct.

Transparent Proxy and UUPS Proxy#


// SPDX-License-Identifier: MIT

pragma solidity ^0.8.21;


// A simple upgradeable contract where the admin can change the logic contract address through the upgrade function, thereby changing the contract's logic.
contract SimpleUpgrade {

	address public implementation; // Logic contract address
	address public admin; // Admin address
	string public words; // String that can be changed through the logic contract's functions

	// Constructor to initialize admin and logic contract address
	constructor(address _implementation) {
		admin = msg.sender;
		implementation = _implementation;
	}

	// Fallback function to delegate calls to the logic contract
	fallback() external payable {
		(bool success, bytes memory data) = implementation.delegatecall(msg.data);
	}

	// Upgrade function to change the logic contract address, can only be called by admin
	function upgrade(address newImplementation) external {
		require(msg.sender == admin);
		implementation = newImplementation;
	}

}

The above is the simplest upgradeable proxy contract, which delegates all calls to the logic contract via delegatecall, while defining an upgrade() function to achieve contract proxy upgrades. However, there is an issue here: when calling via delegatecall, the parameters are all function selectors (selectors), which are the first 4 bytes of the hash of the function signature. Therefore, a mechanism is needed to avoid such conflicts, leading to the introduction of transparent proxies and UUPS proxies.

Transparent Proxy#

The logic of the transparent proxy is very simple: the admin may mistakenly call the upgradeable function of the proxy contract when calling a function of the logic contract due to "function selector conflicts." Therefore, restricting the admin's permissions so that they cannot call any functions of the logic contract can solve the conflict:

  • The admin becomes a tool person, only able to call the upgradeable functions of the proxy contract for contract upgrades, and cannot call the logic contract through callback functions.
  • Other users cannot call the upgradeable functions but can call the functions of the logic contract.

You can refer to the implementation in OpenZeppelin TransparentUpgradeableProxy.

UUPS Proxy#

The transparent proxy logic is simple, but it also has a problem: every time a user calls a function, there is an extra check to see if they are the admin, consuming more gas. This leads to another solution, the UUPS Proxy.

UUPS (Universal Upgradeable Proxy Standard) places the upgrade function in the logic contract, so if there are other functions that have "selector conflicts" with the upgrade function, it will throw an error at compile time.

Reference link: UUPS

Summary#

The differences between ordinary upgradeable contracts, transparent proxies, and UUPS are:

StandardUpgrade Function inWill There Be "Selector Conflicts"Disadvantages
Upgradeable ProxyProxy ContractYesSelector conflicts
Transparent ProxyProxy ContractNoGas cost
UUPSLogic ContractNoMore complex
Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.