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.
The 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.
We use the pre-deployed Safe Singleton Factory to ensure deterministic deployment:
address constant SAFE_SINGLETON_FACTORY = 0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7;
bytes32 constant SALT = keccak256("<custom-salt>");
Note that SAFE_SINGLETON_FACTORY
are 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("<custom-salt>");
address public immutable factoryAddress;
address constant ADMIN = <multichain proxy admin address>;
constructor() {
(bool success, bytes memory result) = SAFE_SINGLETON_FACTORY.call(
abi.encodePacked(
SALT,
type(ProxyFactory).creationCode,
abi.encode(ADMIN)
)
);
if (!success) {
revert AlreadyDeployed();
}
factoryAddress = address(bytes20(result));
}
}
Now ProxyFactory address>
=> Proxy address
will depend only on the ADMIN
and SALT
values.
Then you can deploy the deterministic proxy as follows:
SALT
value.ProxyFactoryDeployer
contract.createProxy
on ProxyFactoryDeployer.factoryAddress
with implementation initializer arguments.NewProxy
event of the createProxy
transaction.
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.
As 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.
Here are several implementations with similar principles that use Safe Singleton Factory:
Pros
TransparentUpgradeableProxy
), allowing the implementation logic to be upgraded without changing the proxy address.NewProxy
events, making it easier to track proxy deployments and their corresponding implementations.ProxyFactoryDeployer
, managing salts, and handling initializer payloads, which can be error-prone.ADMIN
address across multiple chains requires careful management and synchronization to prevent discrepancies.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: