Deep Dive into Smart Contract Proxies: Variants, CREATE vs. CREATE2, and Security Considerations

scourgedev.eth
11 min readApr 17, 2023

Table of Contents

  1. Introduction
  2. Advantages of Using Proxies
  3. Proxy Patterns
  4. Deployment Opcodes: CREATE vs. CREATE2
  5. Common Vulnerabilities
  6. Deploying a Proxy Using OpenZeppelin
  7. 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 CREATE2opcodes, 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:

  1. Clones [EIP-1167]
  2. Universal Upgradeable Proxy Standard (UUPS) [EIP-1967]
  3. 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-reporterand 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 their Initializable 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 containing selfdestruct. If a delegatecall 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:

References

  1. “Proxies.” OpenZeppelin Docs. Accessed April 17, 2023. https://docs.openzeppelin.com/contracts/4.x/api/proxy.
  2. Peter Murray (@yarrumretep), Nate Welch (@flygoing). “ERC-1167: Minimal Proxy Contract.” Ethereum Improvement Proposals, June 22, 2018. https://eips.ethereum.org/EIPS/eip-1167.
  3. Santiago Palladino (@spalladino), Francisco Giordano (@frangio). “ERC-1967: Proxy Storage Slots.” Ethereum Improvement Proposals, April 24, 2019. https://eips.ethereum.org/EIPS/eip-1967.
  4. 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.
  5. @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/.
  6. (@vbuterin), Vitalik Buterin. “EIP-1014: Skinny Create2.” Ethereum Improvement Proposals, April 20, 2018. https://eips.ethereum.org/EIPS/eip-1014.
  7. “Wintermute.” rekt. Accessed April 17, 2023. https://rekt.news/wintermute-rekt/.
  8. “Proxies Deep Dive.” yAcademy Proxies Research. Accessed April 17, 2023. https://proxies.yacademy.dev/pages/proxies-list/.
  9. “Security Guide to Proxies.” yAcademy Proxies Research. Accessed April 17, 2023. https://proxies.yacademy.dev/pages/security-guide.
  10. Croubois, Hadrien. “Workshop Recap: Cheap Contract Deployment Through Clones.” OpenZeppelin, March 3, 2021. https://blog.openzeppelin.com/workshop-recap-cheap-contract-deployment-through-clones/.
  11. Giordano, Francisco. “Deploying More Efficient Upgradeable Contracts.” OpenZeppelin, June 17, 2021. https://blog.openzeppelin.com/workshop-recap-deploying-more-efficient-upgradeable-contracts/.

--

--

scourgedev.eth

Team Lead | Blockchain Full Stack Developer | Smart Contract Developer | Currently Diving Deep Into Smart Contract Security