As blockchain ecosystems continue to expand, developers often face the challenge of deploying the same smart contracts across multiple EVM-compatible networks. A significant issue arises when maintaining consistent contract addresses across these different chains. This problem is particularly acute when dealing with complex contract systems or when upgradeability is a concern.
\ Traditional deployment methods, including CREATE2, fall short when the contract's address depends on the deployer's account state, which may vary across networks. This inconsistency can lead to complications in cross-chain interactions, user experience, and overall system architecture.
\ The need to deploy contracts with identical addresses on different networks has become increasingly important, especially for projects aiming for seamless multi-chain operations. This article presents a practical approach to address this challenge, combining the benefits of deterministic deployment with proxy patterns for upgradeability.
SolutionThe proposed solution leverages the Safe Singleton Factory in combination with a custom proxy pattern. This approach ensures deterministic contract addresses across multiple chains while maintaining upgradeability. We'll use solidity v0.8.17as a reference tooling; however, code snippets should work well on any versions with small changes.
\ To keep the initialization and proxy creation sequence standardized, we'll use Initializable and TransparentUpgradeableProxypatterns.
\ Always make sure to test your code before deployment.
\ The complete implementation is available via gist. Let's overview its steps.
Utilize the Safe Singleton FactoryWe use the pre-deployed Safe Singleton Factory to ensure deterministic deployment:
\
address constant SAFE_SINGLETON_FACTORY = 0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7; bytes32 constant SALT = keccak256("\ Note that SAFE_SINGLETON_FACTORYare different on zkEVM based networks. The salt value here is used to differentiate between several proxies deployed with the same admin address.
\ For this example, let's assume we have a simple Implementation contract that we’ll use as a first implementation to TransparentUpgradeableProxy
\
contract Implementation is Initializable { IWETH internal weth; constructor() { _disableInitializers(); } function init(IWETH _weth) external initializer { weth = _weth; } }\ For simplification, we use a transparent proxy pattern here, but it can be done with any proxy pattern, depending on a contract's requirements.
\ Next, we use the following logic to create the proxy:
\
contract ProxyFactory { error ZeroAdmin(); error Unauthorized(); event NewProxy(address proxy, address implementation); address immutable admin; constructor(address _admin) { if (_admin == address(0)) revert ZeroAdmin(); admin = _admin; } function createProxy(IWETH _weth, address _proxyAdmin) external { if (msg.sender != admin) revert Unauthorized(); address firstImpl = address(new Implementation()); bytes memory proxyInitCalldata = abi.encodeCall( Implementation.init, (_weth) ); address proxy = address( new TransparentUpgradeableProxy( firstImpl, _proxyAdmin, proxyInitCalldata ) ); emit NewProxy(proxy, firstImpl); } }\ You may be wondering why we even need ProxyFactory and deployment logic splitting.
\ There are two main things to do:
\ By deploying ProxyFactory manually, we can’t break this link. Thus, we use Safe Singleton Factory to make it possible:
\
contract ProxyFactoryDeployer { error AlreadyDeployed(); address constant SAFE_SINGLETON_FACTORY = 0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7; bytes32 constant SALT = keccak256("\ Now ProxyFactory address> => Proxy address will depend only on the ADMIN and SALT values.
\ Then you can deploy the deterministic proxy as follows:
\
\ To simplify the explanation, we prepared an init payload inside of the createProxy function. To conserve gas, this can be done off-chain by passing bytes calldata proxyInitPayload as the createProxy argument.
\ Because the ProxyFactory address is deterministic, any contract addresses deployed from it also become deterministic (in the context of a multi-chain interaction). This allows you to prepare any setup of contracts inside the createProxy method to be the same on most EVM-compatible chains.
SecurityAs you may notice we didn't use any authorization logic on ProxyAdminDeployer. Deployment transaction can also be front-runned. Moreover, after deployment on one chain, anyone can perform a deploy on other chains and occupy the ProxyFactory address. But as long as we preserve the same ADMIN address for every chain, it doesn't matter. Address occupation can be performed only if the ProxyFactory creation code and ADMIN address are the same. Thus, any attack vector aiming to occupy the multi-chain address leads to the correct deployment of the ProxyFactory with the proper configuration of admin permissions.
AlternativesHere are several implementations with similar principles that use Safe Singleton Factory:
Pros & cons==Pros==
This solution provides a powerful method for deploying contracts with consistent addresses across multiple EVM blockchains while maintaining upgradeability. By leveraging the Safe Singleton Factory and implementing a custom proxy pattern, developers can achieve deterministic deployment addresses, crucial for various cross-chain applications.
\ Potential use cases include:
\
All Rights Reserved. Copyright , Central Coast Communications, Inc.