[A Deterministic Address for an EVM Multi-Chain Proxy]
Introduction
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 write-up presents a practical approach to address this challenge, combining the benefits of deterministic deployment with proxy patterns for upgradeability.
Solution
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.17 as 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 TransparentUpgradeableProxy patterns.
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 Factory
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:
- Secure proxy initialization data only to an admin
- Break the link between chain-specific environments and certain proxy addresses.
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:
- Choose and set the future proxy admin address.
- Set the
SALT
value. - Deploy the
ProxyFactoryDeployer
contract. - Call
createProxy
onProxyFactoryDeployer.factoryAddress
with implementation initializer arguments. - Now, you'll be able to grab the proxy address from the
NewProxy
event of thecreateProxy
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.
Security
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.
Alternatives
Here are several implementations with similar principles that use Safe Singleton Factory:
Pros & cons
Pros
- Deterministic deployment across multiple chains:
Ensures that contract addresses remain consistent across different EVM-compatible networks, facilitating seamless cross-chain interactions. - Upgradeability:
Utilizes proxy patterns (TransparentUpgradeableProxy
), allowing the implementation logic to be upgraded without changing the proxy address. - Security through Singleton Factory:
Leverages the Safe Singleton Factory to ensure that the factory address is deterministic and secure across chains. - Flexibility in proxy patterns:
While the example uses the transparent proxy pattern, the architecture supports any proxy pattern based on contract requirements. - Simplified multi-chain management:
By maintaining consistent contract addresses, managing and interacting with contracts across multiple chains becomes more straightforward. - Event logging for transparency:
EmitsNewProxy
events, making it easier to track proxy deployments and their corresponding implementations.
Cons
- Dependency on pre-deployed Singleton Factory:
Relies on the availability and consistency of the Safe Singleton Factory across different networks, which might vary, especially on specialized chains like zkEVM. - Complex deployment process:
The setup involves multiple steps, including deployingProxyFactoryDeployer
, managing salts, and handling initializer payloads, which can be error-prone. - Gas consumption:
Deploying contracts through factories and proxies can be more gas-intensive compared to direct deployments, potentially increasing operational costs. - Administrative overhead:
Maintaining the sameADMIN
address across multiple chains requires careful management and synchronization to prevent discrepancies. - Limited customizability for each chain:
While deterministic addresses are beneficial, different chains might have unique requirements or optimizations that this solution doesn't inherently address. - Upgradability constraints:
While proxies allow for upgradeability, ensuring compatibility and preventing storage clashes across implementations requires meticulous planning and audits.
Conclusion
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:
- Cross-chain bridges: Simplifying address mapping and verification across different networks.
- Multi-chain dApps: Ensuring consistent contract addresses for seamless user experiences across networks.
- Soul-bound tokens (SBTs): Maintaining identity consistency in cross-chain environments.
- Interoperable protocols: Facilitating easier integration and interaction between different blockchain ecosystems.