The following describes a standard for proxy contracts which is universally compatible with all contracts, and does not create incompatibility between the proxy and business-logic contracts. This is achieved by utilizing a unique storage position in the proxy contract to store the Logic Contract’s address. A compatibility check ensures successful upgrades. Upgrading can be performed unlimited times, or as determined by custom logic. In addition, a method for selecting from multiple constructors is provided, which does not inhibit the ability to verify bytecode.
Motivation
Improve upon existing proxy implementations to improve developer experience for deploying and maintaining Proxy and Logic Contracts.
Standardize and improve the methods for verifying the bytecode used by the Proxy Contract.
Terminology
delegatecall() - Function in contract A which allows an external contract B (delegating) to modify A’s storage (see diagram below, Solidity docs)
Proxy Contract - The contract A which stores data, but uses the logic of external contract B by way of delegatecall().
Logic Contract - The contract B which contains the logic used by Proxy Contract A
Proxiable Contract - Inherited in Logic Contract B to provide the upgrade functionality
Specification
The Proxy Contract proposed here should be deployed as is, and used as a drop-in replacement for any existing methods of lifecycle management of contracts. In addition to the Proxy Contract, we propose the Proxiable Contract interface/base which establishes a pattern for the upgrade which does not interfere with existing business rules. The logic for allowing upgrades can be implemented as needed.
Proxy Contract
Functions
fallback
The proposed fallback function follows the common pattern seen in other Proxy Contract implementations such as Zeppelin or Gnosis.
However, rather than forcing use of a variable, the address of the Logic Contract is stored at the defined storage position keccak256("PROXIABLE"). This eliminates the possibility of collision between variables in the Proxy and Logic Contracts, thus providing “universal” compatibility with any Logic Contract.
The proposed constructor accepts any number of arguments of any type, and thus is compatible with any Logic Contract constructor function.
In addition, the arbitrary nature of the Proxy Contract’s constructor provides the ability to select from one or more constructor functions available in the Logic Contract source code (e.g., constructor1, constructor2, … etc. ). Note that if multiple constructors are included in the Logic Contract, a check should be included to prohibit calling a constructor again post-initialization.
It’s worth noting that the added functionality of supporting multiple constructors does not inhibit verification of the Proxy Contract’s bytecode, since the initialization tx call data (input) can be decoded by first using the Proxy Contract ABI, and then using the Logic Contract ABI.
The contract below shows the proposed implementation of the Proxy Contract.
contractProxy{// Code position in storage is keccak256("PROXIABLE") = "0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7"constructor(bytesmemoryconstructData,addresscontractLogic)public{// save the code addressassembly{// solium-disable-linesstore(0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7,contractLogic)}(boolsuccess,bytesmemory_)=contractLogic.delegatecall(constructData);// solium-disable-linerequire(success,"Construction failed");}function()externalpayable{assembly{// solium-disable-lineletcontractLogic:=sload(0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7)calldatacopy(0x0,0x0,calldatasize)letsuccess:=delegatecall(sub(gas,10000),contractLogic,0x0,calldatasize,0,0)letretSz:=returndatasizereturndatacopy(0,0,retSz)switchsuccesscase0{revert(0,retSz)}default{return(0,retSz)}}}}
Proxiable Contract
The Proxiable Contract is included in the Logic Contract, and provides the functions needed to perform an upgrade. The compatibility check proxiable prevents irreparable updates during an upgrade.
:warning: Warning: updateCodeAddress and proxiable must be present in the Logic Contract. Failure to include these may prevent upgrades, and could allow the Proxy Contract to become entirely unusable. See below Restricting dangerous functions
Functions
proxiable
Compatibility check to ensure the new Logic Contract implements the Universal Upgradeable Proxy Standard. Note that in order to support future implementations, the bytes32 comparison could be changed e.g., keccak256("PROXIABLE-ERC1822-v1").
updateCodeAddress
Stores the Logic Contract’s address at storage keccak256("PROXIABLE") in the Proxy Contract.
The contract below shows the proposed implementation of the Proxiable Contract.
contractProxiable{// Code position in storage is keccak256("PROXIABLE") = "0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7"functionupdateCodeAddress(addressnewAddress)internal{require(bytes32(0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7)==Proxiable(newAddress).proxiableUUID(),"Not compatible");assembly{// solium-disable-linesstore(0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7,newAddress)}}functionproxiableUUID()publicpurereturns(bytes32){return0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7;}}
Pitfalls when using a proxy
The following common best practices should be employed for all Logic Contracts when using a proxy contract.
Separating Variables from Logic
Careful consideration should be made when designing a new Logic Contract to prevent incompatibility with the existing storage of the Proxy Contract after an upgrade. Specifically, the order in which variables are instantiated in the new contract should not be modified, and any new variables should be added after all existing variables from the previous Logic Contract
To facilitate this practice, we recommend utilizing a single “base” contract which holds all variables, and which is inherited in subsequent logic contract(s). This practice greatly reduces the chances of accidentally reordering variables or overwriting them in storage.
Restricting dangerous functions
The compatibility check in the Proxiable Contract is a safety mechanism to prevent upgrading to a Logic Contract which does not implement the Universal Upgradeable Proxy Standard. However, as occurred in the parity wallet hack, it is still possible to perform irreparable damage to the Logic Contract itself.
In order to prevent damage to the Logic Contract, we recommend restricting permissions for any potentially damaging functions to onlyOwner, and giving away ownership of the Logic Contract immediately upon deployment to a null address (e.g., address(1)). Potentially damaging functions include native functions such as SELFDESTRUCT, as well functions whose code may originate externally such as CALLCODE, and delegatecall(). In the ERC-20 Token example below, a LibraryLock contract is used to prevent destruction of the logic contract.
Examples
Owned
In this example, we show the standard ownership example, and restrict the updateCodeAddress to only the owner.
contractOwnedisProxiable{// ensures no one can manipulate this contract once it is deployedaddresspublicowner=address(1);functionconstructor1()public{// ensures this can be called only once per *proxy* contract deployedrequire(owner==address(0));owner=msg.sender;}functionupdateCode(addressnewCode)onlyOwnerpublic{updateCodeAddress(newCode);}modifieronlyOwner(){require(msg.sender==owner,"Only owner is allowed to perform this action");_;}}
ERC-20 Token
Proxy Contract
pragmasolidity^0.5.1;contractProxy{// Code position in storage is keccak256("PROXIABLE") = "0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7"constructor(bytesmemoryconstructData,addresscontractLogic)public{// save the code addressassembly{// solium-disable-linesstore(0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7,contractLogic)}(boolsuccess,bytesmemory_)=contractLogic.delegatecall(constructData);// solium-disable-linerequire(success,"Construction failed");}function()externalpayable{assembly{// solium-disable-lineletcontractLogic:=sload(0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7)calldatacopy(0x0,0x0,calldatasize)letsuccess:=delegatecall(sub(gas,10000),contractLogic,0x0,calldatasize,0,0)letretSz:=returndatasizereturndatacopy(0,0,retSz)switchsuccesscase0{revert(0,retSz)}default{return(0,retSz)}}}}
Token Logic Contract
contractProxiable{// Code position in storage is keccak256("PROXIABLE") = "0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7"functionupdateCodeAddress(addressnewAddress)internal{require(bytes32(0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7)==Proxiable(newAddress).proxiableUUID(),"Not compatible");assembly{// solium-disable-linesstore(0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7,newAddress)}}functionproxiableUUID()publicpurereturns(bytes32){return0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7;}}contractOwned{addressowner;functionsetOwner(address_owner)internal{owner=_owner;}modifieronlyOwner(){require(msg.sender==owner,"Only owner is allowed to perform this action");_;}}contractLibraryLockDataLayout{boolpublicinitialized=false;}contractLibraryLockisLibraryLockDataLayout{// Ensures no one can manipulate the Logic Contract once it is deployed.// PARITY WALLET HACK PREVENTIONmodifierdelegatedOnly(){require(initialized==true,"The library is locked. No direct 'call' is allowed");_;}functioninitialize()internal{initialized=true;}}contractERC20DataLayoutisLibraryLockDataLayout{uint256publictotalSupply;mapping(address=>uint256)publictokens;}contractERC20{// ...functiontransfer(addressto,uint256amount)public{require(tokens[msg.sender]>=amount,"Not enough funds for transfer");tokens[to]+=amount;tokens[msg.sender]-=amount;}}contractMyTokenisERC20DataLayout,ERC20,Owned,Proxiable,LibraryLock{functionconstructor1(uint256_initialSupply)public{totalSupply=_initialSupply;tokens[msg.sender]=_initialSupply;initialize();setOwner(msg.sender);}functionupdateCode(addressnewCode)publiconlyOwnerdelegatedOnly{updateCodeAddress(newCode);}functiontransfer(addressto,uint256amount)publicdelegatedOnly{ERC20.transfer(to,amount);}}