paint-brush
A Deterministic Address for an EVM Multi-Chain Proxyby@antonk

A Deterministic Address for an EVM Multi-Chain Proxy

by Anton KozlovOctober 28th, 2024
Read on Terminal Reader
tldt arrow

Too Long; Didn't Read

Developers often face the challenge of deploying the same smart contracts across multiple EVM-compatible networks. Traditional deployment methods fall short when the contract's address depends on the deployer's account state. This inconsistency can lead to complications in cross-chain interactions, user experience, and overall system architecture.
featured image - A Deterministic Address for an EVM Multi-Chain Proxy
Anton Kozlov HackerNoon profile picture

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.

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.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 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_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:

  • 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:


  1. Choose and set the future proxy admin address.
  2. Set the SALT value.
  3. Deploy the ProxyFactoryDeployer contract.
  4. Call createProxy on ProxyFactoryDeployer.factoryAddress with implementation initializer arguments.
  5. Now, you'll be able to grab the proxy address from the 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.

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: Emits NewProxy 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 deploying ProxyFactoryDeployer, 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 same ADMIN 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:

  1. Cross-chain bridges: Simplifying address mapping and verification across different networks.
  2. Multi-chain DApps: Ensuring consistent contract addresses for seamless user experiences across networks.
  3. Soul-bound tokens (SBTs): Maintaining identity consistency in cross-chain environments.
  4. Interoperable protocols: Facilitating easier integration and interaction between different blockchain ecosystems.