home
✍🏻

What to Consider and Best Practice for Using Proxy Contract

Article realeased on Sep 20, 2022
Published by Security Researcher Louis

Why We Use Proxy Contracts

Fig. 1 Proxy Pattern Call Process
A proxy pattern is a design pattern that reduces memory usage when multiple identical objects must exist and is used for pre-processing or post-processing of arbitrary requests. A typical example is Proxy Server. A proxy pattern is also applicable to Smart Contract. [Fig. 1] shows how a proxy pattern may be applied in Smart Contract. When a user requests execute() — a function of the Logic Contract intended to be used in the Proxy Contract — to the Proxy, the Proxy Contract executes the Logic Contract’s execute function by making a delegate call to the registered Logic Contract address.
A proxy pattern in Smart Contract solves two major problems. First, a proxy pattern can help reduce the gas required to distribute the contract. Second, the proxy pattern allows writing a contract that is upgradable post-deployment. In this section, we identify how a proxy pattern can reduce the gas required for distribution and check the saved gas by looking at the actual test script. This section also covers how the proxy pattern is applied to writing upgradable contracts.

Saving gas consumption for distribution

Fig. 2 myToken.sol
Fig. 3 ProxyTest.sol
Fig. 4 Test Script for gas comparison required for distribution by applying the proxy pattern
Fig. 5 Used Gas Comparison
Deploying code on a blockchain requires gas. The bytecode determines the amount of gas required for distribution. As shown in [Fig. 4, 5], 511828 is the amount of gas used when distributing one ERC20. If the intended amount of distribution is 1000 ERC20, the amount of gas actually used is 511828*1000. As of this writing, the gas price is 10gwei. Hence, the total ETH gas fee for distribution is 511828*1000 * 10gwei = 511828000*10*(10⁹)*(10^-18) ETH = 511828000*(10^-8) =5.11828 ETH. As of this writing, the USD price of ETH is $1650. Hence, the total cost of distribution is $1650*5.11828=about $8445.
If the above is changed to the distribution of one implementation code distribution and 1000 proxy contracts, the cost will be the distribution costs for 1000 proxy contracts (144739*1000) + one ERC20 (511828 gas). Thus, when this is converted into cost, it is (144739*10000 + 511828) * 10gwei = 145250828*10gwei = 1.45250828 ETH, meaning that the distribution cost can be saved by about 1/3.5.

Designing upgradeable contract

Fig. 6 Upgradeable Contract Overview
The second reason to apply a proxy pattern is to write upgradable contracts. The proxy contract in [Fig. 3] uses Openzeppelin ERC1967 which is written to upgrade the Logic Contract. The proxy contract uses updateTo to register the address of the newly upgraded Logic Contract.
Above [Fig. 6] shows how Logic Contract v1 is upgraded to Logic Contract v2. Following the distribution of Logic Contract v1, the Proxy Contract is distributed with the address of Logic Contract v1 as a parameter. If an upgrade is required for some reason, the newly created Logic Contract v2 can be distributed then it can be pointed to Logic Contract v2 with upgradeTo. Since _implementation has been changed to the Logic Contract v2 address, when calling the execute function, you must call execute of Logic Contract v2 instead of calling Logic Contract v1. To note, however, when writing Logic Contract v2, the Storage Layout must be considered to prevent Storage Collision with the existing v1 version. This will be discussed in the following section.

Notes on using Proxy Contract

As it is confirmed in the above section, a proxy contract is hands-down more advantageous in terms of gas saving and upgradability upon distribution. Yet, you should note a few things when writing a proxy contract. In this section, we will go over two things to consider when writing a proxy contract. The first is the storage collision that occurs in a proxy contract. The second is Logic Contract’s Uninitalize and Selfdestruct issues.
Storage Collision is a problem that may arise in a proxy pattern. Since a proxy contract uses a delegate call, the state variables of the logic contract are read and written in the storage of the proxy contract. Hence, if a contract is written without considering the storage layout, the slot to which the state variable of the logic contract and the state variable of the proxy contract are allocated becomes packed, causing a collision to occur. If slot conflict occurs, the packed variables are overwritten with unintended values, which may cause deviations from the contract’s requirements. In this section, the storage slot allocation method will be discussed. Also, actual accidents caused by Storage Collision will be reviewed by analyzing the case where Audius Governance was hacked.
Second, we will examine things to keep in mind when using Initialize in a proxy pattern. Initialize is a function that is executed first after a contract is distributed and is executed only once. Mainly, Initialize designates a contract’s ownership to the caller. In a proxy pattern, there are Proxy Contract and Logic Contract. For both contracts, the ownership must be taken after distribution. However, if the Logic Contract Initialize is not called, the ownership of the Logic Contract may be transferred to an unauthorized third party. Here, a bigger problem occurs when selfdestruct is callable using ownership of Logic Contract. In this section, based on the Parity Multisig Freeze case, we will examine what happens if the Logic Contract is passed to an unauthorized third party and selfdestruct is executed.

Storage Slot Allocation Method [3]

Basic Storage Layout

Fig. 7 Example Contract
Smart Contract sequentially stores and manages variables with a size of 32 bytes (256 bits). Here, the space where the saving occurs is called Slot. Generally, variables are allocated from Slot 0 in the order in which they are declared. The Example Contract in [Fig. 7] has two variables with a size of 256 bits. Since Storage is allocated according to the order of the declaration, num1 is located in Slot 0 and num2 in Slot 1.

Storage Packing

Fig. 8 ExamplePacking Contract
Fig. 9 Storage Layout of Example Packing
Not all variable types are 256 bits. Let’s see theExamplePacking Contract in [Fig. 8]: two bool variables with a size of 8 bits are declared. A number of variables are placed in one slot until the combined size of the variables is 256 bits. Once the size of the next variable exceeds 256 bits, the next variable is placed in a new slot. In ExamplePacking Contract, the combined size of bool n1, bool n2 and uint256 n3 is 8+8+256bits. Therefore, two bool variables are placed in Slot 0. Then, uin256 variables are placed in Slot 1 because they exceed the size of one slot. In doing so, packing occurs in Slot 0. Packing refers to two or more variables being placed in one slot. [Fig. 9] shows the actual output of the Storage Layout of ExamplePacking Contract.

Dynamic Array//Mapping

Fig. 10 exampleAlloc Contract
Fig. 11 exampleAlloc Storage Layout
Fig. 12 exampleAlloc Storage Layout (*Each of the input values as parameters of keccak256 to obtain a slot is 64 bytes. And herein, 0, which must be padded, is omitted for readability)
Fig. 12 exampleAlloc Storage Data Type
If an array is dynamically allocated, the length of the array is entered as a value, not directly into the slot of the declared order [4]. In Slot 0 and Slot 1 of the exampleAlloc contract of [Fig. 10], foo and bar are located, respectively. In Slot 2, the moment the length of items is determined — i.e., when allocate is called — the length of items.length is set to 2. The actual value is recorded in Slot keccak256(slot index)~(Slot keccak256(slot index)+array index).
Mapping is similar to dynamically allocating an array. Still, unlike the dynamic array, the slot number from which the mapping began and the hashed value of the key value are designated as the slot number. For the values of value[0] and value[1], 100 is stored in Slot keccak256(0(key), 3(Slot number)) — i.e., the value obtained by hashing 3, which is the slot number in which value is declared, as well as 0,1, which is each key value — and 200 is stored in Slot keccak256(1,3).
The Storage Layout in [Fig. 12] shows that ‘t_uint256’, which is the value type of the items array, does not enter Slot 2 immediately. Instead, the data type ‘t_array(t_uint256)dyn_storage’ is inside. From this, it can be confirmed that the value of ‘t_mapping(t_uint256, t_uint256)’ sits in the mapping as well, not that the value enters directly.

Inheriting Contract

Fig. 13 Child contract inherited from Parent1 and Parent2 contracts
Fig. 14 Storage Layout when contract inheritance
Let’s see how Slot is designated for receiving the inheritance. In the case of inheritance, the inherited contracts are designated sequentially from Slot 0. If the [Fig. 13] contract Child is Parent1 and Parent2, Parent1’s num sits in Slot 0, Parent2’s num2 in Slot1, and Child’s num3 in Slot2 as in [Fig. 14].

Storage Collision

Proxy Anti Pattern Slot Layout[5]

Fig. 15 Logic Contract
Fig. 16 ProxyCollision Contract
Fig. 17 Proxy Anti Pattern Storage Collision
When using a proxy pattern. Storage Collision must be considered. A proxy contract uses a delegate call to use the code of the Logic Contract. Put differently, the state variables of the Logic Contract operate in the Storage of the Proxy Contract. Let’s look at the Storage Layout of ProxyCollision Contract and Logic Contract in [Fig. 15, 16]. The state variable of the Logic Contract is assigned to the Storage of the ProxyCollision Contract. The owner variable of Logic Contract is placed in Slot 0 of ProxyCollision Contract. Similarly, the variable of logicContract of ProxyCollision also sits in Slot 0. Hence, as it can be confirmed from [Fig. 17], Storage Collision occurs in Slot 0. Thus, if an operation is performed to change the owner by making a delegate call in the proxy contract, it uses the storage of the proxy contract, thereby influencing the address logicContract variable as well.

Security Incident Due to Storage Collision — Audius Governance Hack [6]

On July 23, 2022, the Audius Governance Hack incident occurred where the initialize function could be called multiple times due to Storage Collision, allowing 18.5M $AUDIO Token (about $6 million) to be stolen by the hacking of the Governance. Audius is a decentralized protocol for sharing music led by artists. Audius is a platform that helps artists directly publish and monetize their music to distribute it to fans [7].
Fig. 18 AudiusAdminUpgradeabilityProxy[8]
Fig. 19 Initializable[9]
Fig. 20 Audius Governance Hack Storage Collision [10]
The AudiusAdminUpgradeabilityProxy in [Fig. 18] is Audius’ Proxy Contract. The Initializable [Fig. 19] is the first contract inherited from Logic Contract. Hence, as in [Fig. 20], the Storage Layout is formed. That is, Storage Collision has occurred in Slot 0 of Proxy Storage. [Fig. 20] shows the situation where Initializing and Initialized are overwritten.
1. The attacker calls the Initialize() function and changes the owner’s address
2. The attacker changes vote parameters
3. The attacker submits a malicious proposal (id=85)
4. By initializing DelegateManagerv2, the attacker resets the governance address
5. With the attacker’s contract address, delegateStake is performed
6. Submit the vote
The attack occurred in the following order [11]. In [Fig. 21], the attacker called the initialize of Governance Contract again, set _registryAddress to the attacker’s address (0xbdbB5945f252bc3466A319CDcC3EE8056bf2e569), and changed the vote parameter. The changed vote parameters are votePeriod, Delay, and _votingQuorumPercent, and these were changed to 3, 0, 1. This means that the voting period was set to 3 blocks, the delay was not set, and the quorum was set to 1%.
As it can be confirmed from [Fig. 22], the attacker submitted a malicious proposal with the 85th id. This is a proposal to send a token to the attacker’s address. The proposal at this time is shown in [12]. Next, as in [Fig. 23], the attacker registered her address as the governance of DelegateManagerv2 by calling initialize of DelegateManagerv2. The governance of DelegateManagerv2 has the authority to call delegatestake. Accordingly, as in [Fig. 24], the tokens corresponding to the amount of 10³¹ became delegateStakes to the attacker’s address. In [Fig. 24], the attacker increased her vote’s weight due to the previous delegateStake and eventually voted alone based on this, as in [Fig. 25] and [12], the 85th proposal was passed.
Fig. 21 The attacker calls Governance.initialize to her address to change the owner’s address and vote parameters
Fig. 22 The attacker submits a malicious proposal with the 85th id
Fig. 23 The attacker sets governance to the attacker’s address
Fig. 24 The attacker executes delegateStake with the attacker’s address
Fig. 25 The attacker votes [13]

Initialize Issue

Why is Initialize required in a proxy pattern

Initialize, like Constructor, is a function that is first executed in a contract. Initialize is used to designate the owner of the contract and set initial values of important state variables. Constructor is executed immediately after the contract is distributed. But Initialize must be called manually. Then, why is Initialize required, which must be called manually? Initialize is needed to take ownership using delegatecall in a proxy pattern. If a proxy pattern is used, the Logic Contract literally provides only Logic — the Storage uses the Proxy Contract’s storage. That is, Constructor is a function that operates at the moment when it is finally distributed — it changes the storage of the Logic Contract but does not change the Storage of the Proxy Contract. Yet, if Initialize is called using the delegate call, it changes the storage of the proxy contract.

Problems of no Initialize- Parity Multisig Freeze

Fig. 26 Example when the user calls the execute function in the wallet contract
As described above, Initialize designates the owner of a contract. Sometimes, however, a problem arises if the developer or management team fails to call Initialize. More specifically, a problem arises if selfdestruct is included in a logic contract [17]. selfdestruct is an operation that deletes a contract existing in the blockchain and moves ETH in the contract to a designated address [18]. The selfdestruct operation was adopted to lighten the network by deleting data (or code) stored in the blockchain [19]. In the past, selfdestruct (suicide) was implemented in Logic Contract in many cases.
If selfdestruct is not implemented in Logic Contract, storages of Logic Contract and Proxy Contract are separated. Hence, even if a third party takes ownership of Logic Contract, it is not a significant issue. Yet, it is no longer so if selfdestruct is implemented in Logic Contract. Since the attacker has ownership of the Logic Contract, she can call selfdestruct which is callable only by the contract owner, and as a result, the code of the Logic Contract address becomes deleted. If the code of the logic contract is deleted, even if a function is called from the proxy contract with the address of the logic contract, the code does not exist in the address, so normal operation cannot be performed.
Concerning the above, the Parity Multisig Freeze incident occurred, and 513,695.801 ETH was locked [20]. Parity Multisig has a Wallet Contract [21] that acts as a proxy and a Wallet Library [22] that acts as a Logic Contract. Of course, there are multiple Wallet Contract addresses other than the addresses in [21]. As in [Fig. 26], Parity Multisig works then takes ownership by running initWallet immediately after distributing the Wallet Contract. However, ownership exists in the Library Contract, and the development team did not call this. By chance, one developer called initWallet directly to the WalletLibrary Contract and took ownership [23]. Then, by calling kill, the developer deleted WalletLibrary. In short, the code to execute the logic by the delegate call in the Wallet Contract became deleted [24]. Hence, due to the deleted Library, the function to withdraw Ether deposited in the WalletLibrary could not be executed and was locked.
To solve this problem, eip999 [25] was proposed but was rejected. eip999 is a proposal to allow the distribution of the new WalletLibrary address to the old WalletLibrary address. At the time, the opposition camp believed that the proposal harmed the immutability of the blockchain and was not in line with the philosophy of the blockchain.

Best Practice

In the earlier section, we examined the issues of Storage Collision and Initialize with the Audius Governance Hack and Parity Multisig Freeze cases. Then, how can the proxy pattern to prevent Storage Collision and Initialize issues be implemented? This section introduces EIP1967 and Storage Gap as best practices to prevent Storage Collision and explains considerations for preventing the issue of Initialize.

Storage Collision

EIP1967

Fig. 27 IMPLEMENTATION_SLOT at ERC1967Proxy
Fig. 28 EIP1967 Storage Layout
The first method to prevent storage collision is to determine the slot number of the variable containing the implementation address according to EIP1967 [15]. EIP1967 is a proposal to predetermine the number of the slot that will contain the address of the Logic Contract. In Proxy Contract, the slot to contain the Logic Contract’s address is calculated as bytes32(uint256(keccak256(“eip1967.proxy.implementation”))-1)). As in [Fig. 27], the hash value is confirmed. Then, as in ERC1967Upgrade, the address of the Logic Contract to be newly upgraded is entered into the slot of _IMPLEMENTATION_SLOT. As a result, the ERC1967 proxy pattern comes into the possession of Storage Layout as shown in Fig. 28]. Hence, there is no collision between the state variable of the proxy contract and the state variable of the logic contract.

Storage Gap Setting

Fig. 29 Proxy pattern without __gap applied — Parent inheritance
Fig. 30 Storage Layout of Child Contract that is inherited from Parent Contract
However, let’s assume that a new variable, n5, is added to Parentv2 and is upgraded for some reason as in [Fig. 31]. As a result, because the Proxy Contract has the context of the existing Logic Contract, it forms the Storage Layout as in [Fig. 32], and the previous uint256 n4 and uint256 n5 collide in slot 3.
Fig. 31 Proxy pattern without __gap applied — Inherit Parentv2
Fig. 32 Storage Layout of Child Contract that inherited Parentv2 Contract
To prevent storage collision in this situation, a slot can be secured by setting __gap in advance. [Fig. 33] is an example where the gap is applied to the parent contract. By leaving __gap, it is possible to secure a slot for the Parent Contract from slot 0 to slot 49 in advance as it can be confirmed from [Fig. 34].
Fig. 33 The contract including __gap
Fig. 34 Storage Layout of Contract with __gap applied
If the Parent needs to be upgraded by adding one variable, as in [Fig. 35], it can be done by shortening the length of __gap by one and adding a new variable. Since we have already allocated a slot for the Parent Contract, storage does not collide and shows a stable layout as in [Fig. 36].
Fig. 35 Inheriting Parentv2 with __gap applied
Fig. 36 Storage Layout of Child Contract that inherited Parentv2 with __gap applied

Initialize Issue

To solve the issues related to Initialize, the first thing to do is do Initialize also in the Logic Contract to bring ownership. In this regard, there is also a method of applying _disableInitializers [26]. _disableInitializers prevent directly calling of Initialize by locking when the Logic Contract is deployed. Also, it prevents re-doing Initialize. If one calls this in the contract constructor, it can prevent the contract from being initialized or re-initialized in any version.
The second method is not using selfdestruct. As mentioned by Vitalik Buterin in [19], it helps reduce the weight of the network but is an operation that harms the immutability of the blockchain. As stated above, not doing Initialize itself is not a great threat, but the most dangerous point is that selfdestruct is implemented in Logic Contract. Hence, it is better for security to not use selfdestruct unless it is absolutely necessary. You can receive a partial refund of the gas used during distribution by selfdestruct, but one must carefully consider the trade-off between getting a gas refund and security.
If, nonetheless, selfdestruct must be used, it is recommended to use it in library [27]. Since version 0.4.21 and thereafter, the library does not allow functions that directly change the status. Call protection is provided so that revert occurs if library code is executed using direct call instead of delegate call [28]. Therefore, if selfdestruct is implemented in the library, the library code cannot be deleted because it cannot be called directly.
Lastly, we recommend not to designate the code that can do delegate call as implementation. If the Logic Contract can delegate call to any contract, the Logic Contract can be deleted. Of course, any contract should have selfdestruct. Hence, it is recommended not to designate a logic contract capable of delegate call as the implementation address.

Conclusion

Using a proxy pattern has several advantages and provides development convenience. Still, before applying a proxy pattern, the above Storage Collision and Initialize issues must be recognized for safe development. To prevent Storage Collision, consider the storage layout between Proxy Contract and Logic Contract. Also, sufficient storage should be allocated considering future upgrades of the Logic Contract. Finally, after distributing the contract, Initialize must absolutely be executed separately. Above all, using selfdestruct in the Logic Contract should be avoided.

Reference

About KALOS
KALOS is a flagship service of HAECHI LABS, the leader of the global blockchain industry. We bring together the best Web2 and Web3 experts. Security Researchers with expertise in cryptography, leaders of the global best hacker team, and blockchain/smart contract experts are responsible for securing your Web3 service.
We have secured over $60b worth of crypto assets across 400+ global crypto projects — L1/L2 projects, defi protocols, P2E games, and bridges — notably 1inch, SushiSwap, Badger DAO, SuperRare, Klaytn and Chainsafe. KALOS is the only blockchain technology company selected for the Samsung Electronics Startup Incubation Program in recognition of our expertise. We have also received technology grants from the Ethereum Foundation and Ethereum Community Fund.
Secure your smart contracts with KALOS.
Email: audit@kalos.xyz
Official website: https://kalos.xyz