Deep Dive into Smart Contract Proxies: Variants, CREATE vs. CREATE2, and Security Considerations
Table of Contents
- Introduction
- Advantages of Using Proxies
- Proxy Patterns
- Deployment Opcodes: CREATE vs. CREATE2
- Common Vulnerabilities
- Deploying a Proxy Using OpenZeppelin
- Final Words
Introduction
In the realm of Ethereum smart contracts, proxies play a crucial role in achieving upgradability, address consistency, and separation of state and data from contract logic. In this article, we will explore the inner workings of smart contract proxies, their deployment methods using CREATE
and CREATE2
opcodes, various proxy patterns, and potential security risks associated with their implementation.
A smart contract proxy consists of two main components: the proxy contract and the implementation (logic) contract. The proxy contract is responsible for maintaining the contract’s state, data storage, and consistent address. It forwards incoming function calls to the implementation contract, which houses the actual business logic and bytecode. The key to this forwarding mechanism is the delegatecall
opcode, which allows the proxy contract to delegate the execution of a function to the implementation contract while retaining its own state and data context.
Advantages of Using Proxies
Smart contract proxies offer several advantages, making them an attractive choice for developers working in the Ethereum ecosystem. Here are some key benefits of using proxies:
Gas Efficiency
Deploying contracts with implementation code can be quite expensive in terms of gas costs. For instance, creating a pair on UniswapV2 requires approximately 2.5 million gas, translating to over $140 at the time of writing (30 Gwei and $1900/ETH). However, using an EIP-1167 Minimal Proxy Contract (Clone) significantly reduces gas consumption. At around 150,000 gas, the cost comes down to a little over $8 (at the same gas price and Ether value). This efficiency makes proxies a more economical choice for developers.
Upgradability
One of the primary advantages of using proxies is their upgradability. This feature allows developers to modify or upgrade a contract’s logic without changing its address, thereby maintaining consistency in the contract’s state and data throughout the upgrade process. Upgradability is crucial in ensuring that smart contracts can adapt to changing requirements or fix potential issues without compromising the user experience or causing disruptions to existing functionalities.
Drawbacks and Considerations
While proxies offer several benefits, there are some drawbacks to be aware of. One notable disadvantage is the increased gas cost for each call made through a proxy. This is because the implementation address of the underlying logic must be read, and the location of this address may vary among different proxy types. Consequently, when performing a transaction through the proxy, the proxy must read the address and then delegate the call, which increases the gas cost.
Despite this drawback, the advantages of using proxies in the Ethereum ecosystem often outweigh the potential downsides. Developers can leverage gas efficiency and upgradability to create more flexible and cost-effective smart contracts, ultimately improving the overall user experience.
Initializer
In the world of smart contract proxies, initializers play a critical role in the setup process. Most modern-day proxies are initialized rather than using constructors, as constructors are not compatible with proxy contracts. To avoid any confusion, it is recommended to disable the initializers in the implementation contract, since it is typically not for general public use.
Using the Initializable
library from OpenZeppelin, this is easily achievable. Code example below:
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
Proxy Patterns
When working with smart contract proxies, it’s important to understand the different types of proxy patterns available and the tradeoffs between them. The most common types of proxies are:
- Clones [EIP-1167]
- Universal Upgradeable Proxy Standard (UUPS) [EIP-1967]
- Transparent Upgradeable Proxies
Minimal Proxy Contract (Clones) [EIP-1167]
The Clone pattern, as defined in EIP-1167, offers certain advantages in smart contract deployment. In contrast to typical proxies, Clones remove several features, such as upgradability, which results in lower deployment costs. Clones do not have constructors and are deployed with the address of the underlying logic embedded within the bytecode.
This streamlined approach eliminates the need to read from storage during each call, as it directly delegates the call. Consequently, this efficient process conserves gas and provides a more cost-effective solution.
Universal Upgradeable Proxy Standard (UUPS) [EIP-1967]
The UUPS pattern, formally introduced as part of EIP-1822 and incorporating EIP-1967, is a sophisticated proxy model that optimizes deployment costs and gas usage. UUPS relies on EIP-1967 to store the implementation address in a unique storage slot within the proxy contract. By storing the upgrade logic on the implementation contract, which is a key aspect of the UUPS pattern, it effectively reduces the overall deployment costs of the proxies.
This approach eliminates the need to check if the caller is an admin on the Proxy, which conserves gas for each call. Additionally, it prevents function collisions between the implementation contract and the upgrade logic in the proxy. However, this design increases the complexity of the implementation side, as each version of the implementation contract must include the upgrade function.
Developers must exercise caution to ensure the implementation contract does not selfdestruct
or end up in an undesirable state, as doing so will result in an unusable proxy. UUPS also offers the flexibility to customize the authorization mechanism for upgrades, such as incorporating voting systems or timelocks, providing greater control over the upgrade process.
Transparent Upgradeable Proxies
Transparent Upgradeable Proxies, like UUPS, also incorporate EIP-1967, providing a streamlined and efficient proxy pattern for smart contract deployments. The primary distinction between Transparent Upgradeable Proxies and UUPS lies in the location of the upgrade logic. In Transparent Upgradeable Proxies, the upgrade logic is stored within the proxy itself, ensuring that only the admin of the proxy can initiate an upgrade.
This design offers a level of security, as the proxy will only delegate calls if the caller is not the admin. If the caller is any other address, the proxy will always delegate the call, thereby mitigating the risk of function clashing for admins.
While Transparent Upgradeable Proxies offer a robust solution for smart contract upgrades, developers should consider the implications of having the upgrade logic within the proxy, as it may present challenges in terms of gas usage and storage management. However, the pattern remains a popular choice for its simplicity and ease of implementation.
Gas Usage Comparison between Proxy Types
In order to better understand the efficiency of the different proxy types, let’s examine their gas usage for function calls and contract deployments. The table below presents the gas usage comparison for each type of proxy and a direct contract implementation.
The following demo was done with an ERC20 as the base implementation. Gas usage data was obtained through using a combination of hardhat-gas-reporter
and transaction receipts. Source code can be found here.
Note: UUPS needed some minimal additional implementation for the ERC20. Its implementation can be found at contracts\FactoryUUPS.sol
in the source code.
As depicted in the table, the gas usage varies significantly between the three types of proxies and native implementation. Clone proxies demonstrate lower gas usage for function calls compared to TUP and UUPS proxies. However, they have a slightly higher gas usage for contract deployments than direct implementation approach.
UUPS proxies have relatively lower gas usage for function calls and proxy deployments (createToken
) compared to TUP proxies. Transparent Upgradeable Proxies have the highest gas usage for both function calls and proxy deployments among the three types of proxies, with slightly lower initial implementation contract deployment costs compared to UUPS.
Developers should consider these gas usage differences when selecting a proxy pattern, as they can have a significant impact on the overall cost and efficiency of smart contract deployments and interactions.
Deployment Opcodes: CREATE vs. CREATE2
In the context of smart contract deployment, there are two main methods to consider: CREATE
and CREATE2
. Each method has its unique characteristics that affect the resulting contract address and deployment process. Additionally, the choice of deployment method can have significant security implications, as demonstrated by the Wintermute hack on Optimism.
CREATE
The CREATE method determines the contract address based on the factory contract’s nonce. Each time CREATE
is called in the factory, the nonce is increased by 1. The formula for determining the contract address is keccak256(sender, nonce)
.
CREATE2
Introduced in EIP-1014 with the Constantinople fork, CREATE2
offers an alternative deployment method that allows for more control and flexibility over the resulting contract address. In contrast to CREATE
, CREATE2
uses a salt and the keccak hashed bytecode of the contract as part of its address calculation. The formula for determining the contract address is keccak256(0xFF, sender, salt, keccak256(bytecode))
.
As a side note, the 0xff
constant used in the formula ensures that the resulting contract addresses generated by CREATE2
cannot collide with those generated by the CREATE
opcode. This is because the 0xff
byte, when used as a starting byte in Recursive-length prefix (RLP) encoding, would only be applicable to data structures many petabytes long, which Ethereum contracts and data structures do not reach.
Warning: With CREATE2
, it is possible for a contract to deploy at the same address with different bytecode through a Metamorphosis Smart Contract Pattern. To learn more about this pattern and its implications, refer to this resource.
Wintermute Hack on Optimism
The Wintermute hack on Optimism serves as an example of how the choice of deployment method can have serious security consequences. In this case, Optimism sent 19 million OP tokens to Wintermute’s Gnosis Safe proxy on Mainnet. However, the Gnosis Safe proxy was not deployed on Optimism. The Gnosis Safe on Mainnet was created with an older version of the ProxyFactory
contract, which used the CREATE
opcode. An exploiter was able to replay the Gnosis Safe MasterCopy deployment from Mainnet and then deploy batches of vaults until they reached Wintermute’s vault.
Read more about the Wintermute hack on Optimism here.
In summary, developers must weigh the benefits and drawbacks of each deployment method when working with smart contracts. While CREATE provides a simple, incremental approach to contract addresses, it also carries potential risks associated with security vulnerabilities like the Wintermute hack on Optimism. On the other hand, CREATE2 offers more control and flexibility, which can be beneficial in certain use cases.
Common Vulnerabilities
When working with proxy patterns, it is essential to be aware of common vulnerabilities and potential security risks. The following are some common vulnerabilities and the steps to mitigate them.
Uninitialized Proxy
An uninitialized proxy vulnerability occurs when a proxy is deployed without being initialized or can be re-initialized. This vulnerability allows an exploiter to initialize the proxy with unintended parameters, potentially leading to unintended consequences or loss of funds.
Mitigation Steps:
- Initialize the proxy in the same transaction as the deployment.
- Ensure that the initialize function can only be called once. One approach to achieve this is by using OpenZeppelin’s
initializer
modifier from theirInitializable
library.
Vulnerabilities with Selfdestruct
Several vulnerabilities can arise when a contract uses the selfdestruct
functionality:
- If the implementation contract contains
selfdestruct
and is activated by the proxy, the proxy will be destroyed. - If the implementation contract is destroyed, all clones created with that implementation will stop working. Moreover, if the implementation contract was deployed using CREATE2, there is a possibility that the implementation contract could be replaced with a malicious contract.
Mitigation Steps:
- Ensure that the implementation contract does not contain
selfdestruct
functionality, unless it is safeguarded and the destruction of the proxy is intended. - Make sure that the implementation contract does not
delegatecall
to an external contract containingselfdestruct
. If adelegatecall
is found in the external contract, continue the checking process until no such functionality is found.
It is important to note that this vulnerability may not be an issue in the future due to the deactivation of selfdestruct
in EIP-4758. Additionally, an exception to this vulnerability exists when using a Metamorphic contract. To learn more about Metamorphic contracts, read more about it here.
Deploying a Proxy Using OpenZeppelin
Proxies are often deployed with a contract factory. In this section, multiple demonstrations show how to deploy proxies non-deterministically using the CREATE
opcode and deterministically using the CREATE2
opcode with OpenZeppelin, using an ERC-20 as the implementation. All of the source code can be found here.
Deploying a Proxy Non-deterministically (CREATE opcode)
Typically, deploying a proxy non-deterministically is done by simply creating a new contract.
Sample code for deploying a TransparentUpgradeableProxy
with the CREATE
opcode:
function createToken(
string calldata name,
string calldata symbol,
uint256 initialSupply,
address owner
) external returns (address) {
TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
tokenImplementation,
msg.sender,
""
);
ERC20PresetFixedSupplyUpgradeable(address(proxy)).initialize(
name,
symbol,
initialSupply,
owner
);
return address(proxy);
}
When deploying a proxy with the UUPS pattern, the implementation contract must be modified to include the upgrade logic. In the example below, we create a UUPS-compatible ERC20 token by extending the ERC20PresetFixedSupplyUpgradeable
contract with UUPSUpgradeable
and OwnableUpgradeable
. The _authorizeUpgrade
function is overridden to restrict upgrades to the contract owner.
contract UUPSCompatibleERC20 is
ERC20PresetFixedSupplyUpgradeable,
UUPSUpgradeable,
OwnableUpgradeable
{
function initialize(
string memory name,
string memory symbol,
uint256 initialSupply,
address owner
) public virtual override initializer {
__ERC20PresetFixedSupply_init(name, symbol, initialSupply, owner);
__Ownable_init();
}
function _authorizeUpgrade(address) internal override onlyOwner {}
}
Additionally, it is possible to verify that the new implementation of the ERC20 is UUPS compatible using the @openzeppelin/hardhat-upgrades
plugin. In the example code below, we demonstrate how to deploy a UUPS-compatible proxy using Hardhat:
const UUPSCompatibleERC20 = await hre.ethers.getContractFactory("UUPSCompatibleERC20");
const proxy = await hre.upgrades.deployProxy(
UUPSCompatibleERC20,
["Name", "Symbol", 1000, this.accounts[0].address],
{ kind: "uups" }, // <- This line is necessary for the compatibility check
);
Using OpenZeppelin’s clone method for deploying a proxy with the CREATE
opcode:
function createToken(
string calldata name,
string calldata symbol,
uint256 initialSupply
) external returns (address) {
address clone = Clones.clone(tokenImplementation);
ERC20PresetFixedSupplyUpgradeable(clone).initialize(
name,
symbol,
initialSupply,
msg.sender
);
return clone;
}
Deploying a Proxy Deterministically (CREATE2 opcode)
When deploying a proxy deterministically, the process usually entails generating a new contract that includes a salt parameter. The deployment code for this approach is quite similar to the non-deterministic method, with the addition of {salt: _salt}
. An example of this would be:
Proxy newProxy = new Proxy{salt: _salt}(tokenImplementation);
Using OpenZeppelin’s clone method for deploying a proxy with the CREATE2
opcode:
address clone = Clones.cloneDeterministic(implementationAddress, salt);
To prevent replay attacks on another chain, it is advisable to use a new salt derived from the old salt and unique data, such as the inputs or a unique data hash. For example:
bytes20 newSalt = bytes20(keccak256(abi.encodePacked(_initializerData, _salt)));
Final Words
Thank you for taking the time to explore the various proxy patterns, deployment methods, and associated vulnerabilities in the world of smart contract development with me. I hope this article has provided valuable insights and a deeper understanding of the subject matter. As the blockchain ecosystem continues to evolve, staying informed and knowledgeable about these patterns will be crucial. I appreciate your time and interest, and I would be grateful for any feedback you may have. Additionally, please feel free to suggest topics you’d like me to cover in future articles.
Note: All source code for this article can be found here: https://github.com/ljz3/proxies-gas-usage
Further reading:
- Diamond Proxy [https://proxies.yacademy.dev/pages/proxies-list/#diamond-proxy]
- Metamorphic Contracts [https://ethereum-blockchain-developer.com/110-upgrade-smart-contracts/12-metamorphosis-create2/]
- Beacon Proxy [https://proxies.yacademy.dev/pages/proxies-list/#beacon-proxy]
- Some other smart contract vulnerabilities [https://proxies.yacademy.dev/pages/security-guide]
References
- “Proxies.” OpenZeppelin Docs. Accessed April 17, 2023. https://docs.openzeppelin.com/contracts/4.x/api/proxy.
- Peter Murray (@yarrumretep), Nate Welch (@flygoing). “ERC-1167: Minimal Proxy Contract.” Ethereum Improvement Proposals, June 22, 2018. https://eips.ethereum.org/EIPS/eip-1167.
- Santiago Palladino (@spalladino), Francisco Giordano (@frangio). “ERC-1967: Proxy Storage Slots.” Ethereum Improvement Proposals, April 24, 2019. https://eips.ethereum.org/EIPS/eip-1967.
- Gabriel Barros, Patrick Gallagher. “ERC-1822: Universal Upgradeable Proxy Standard (UUPS) [Draft].” Ethereum Improvement Proposals, March 4, 2019. https://eips.ethereum.org/EIPS/eip-1822.
- @luu-alex, @gabrocheleau, @wackerow, @minimalsm, @kuzdogan, and @jmcook1186. “Recursive-Length Prefix (RLP) Serialization.” ethereum.org. Accessed April 17, 2023. https://ethereum.org/en/developers/docs/data-structures-and-encoding/rlp/.
- (@vbuterin), Vitalik Buterin. “EIP-1014: Skinny Create2.” Ethereum Improvement Proposals, April 20, 2018. https://eips.ethereum.org/EIPS/eip-1014.
- “Wintermute.” rekt. Accessed April 17, 2023. https://rekt.news/wintermute-rekt/.
- “Proxies Deep Dive.” yAcademy Proxies Research. Accessed April 17, 2023. https://proxies.yacademy.dev/pages/proxies-list/.
- “Security Guide to Proxies.” yAcademy Proxies Research. Accessed April 17, 2023. https://proxies.yacademy.dev/pages/security-guide.
- Croubois, Hadrien. “Workshop Recap: Cheap Contract Deployment Through Clones.” OpenZeppelin, March 3, 2021. https://blog.openzeppelin.com/workshop-recap-cheap-contract-deployment-through-clones/.
- Giordano, Francisco. “Deploying More Efficient Upgradeable Contracts.” OpenZeppelin, June 17, 2021. https://blog.openzeppelin.com/workshop-recap-deploying-more-efficient-upgradeable-contracts/.