Deploying EIP-1167 Minimal Proxy Contract with Vyper on zkSync Era

zkApe
3 min readSep 14, 2023

--

If you’re keen on deploying the EIP-1167 minimal proxy contract using Vyper on zkSync, and wish to pre-calculate the contract address, you might face some challenges. The address you expect to get and the one that’s actually deployed can differ due to discrepancies between zkSync and the Ethereum Virtual Machine (EVM) concerning the create2 opcode. So, how can you navigate this issue with a Vyper-written contract? How do you ensure that zkSync’s behavior aligns with your expectations?

If you’re unfamiliar with the differences between zkSync and Ethereum, click here.

Let’s start by looking at zkSync’s ContractDeployer. Here, you’d notice the getNewAddressCreate2 function, which predicts the address for create2 deployments. Use this function to determine your desired address.

/// @notice Calculates the address of a deployed contract via create2
/// @param _sender The account that deploys the contract.
/// @param _bytecodeHash The correctly formatted hash of the bytecode.
/// @param _salt The create2 salt.
/// @param _input The constructor data.
/// @return newAddress The derived address of the account.
function getNewAddressCreate2(
address _sender,
bytes32 _bytecodeHash,
bytes32 _salt,
bytes calldata _input
) public view override returns (address newAddress) {
// No collision is possible with the Ethereum's CREATE2, since
// the prefix begins with 0x20....
bytes32 constructorInputHash = EfficientCall.keccak(_input);

bytes32 hash = keccak256(
bytes.concat(CREATE2_PREFIX, bytes32(uint256(uint160(_sender))), _salt, _bytecodeHash, constructorInputHash)
);

newAddress = address(uint160(uint256(hash)));
}

For Vyper enthusiasts, don’t worry! We’ve prepared a Vyper version of the getNewAddressCreate2 function for you.

# @version 0.3.9

_CREATE2_PREFIX: constant(bytes32) = 0x2020dba91b30cc0006188af794c2fb30dd8520db7e2c088b7fc7c103c00ca494


@external
@pure
def compute_address(salt: bytes32, bytecode_hash: bytes32, deployer: address, input: Bytes[4_096]=b"") -> address:

constructor_input_hash: bytes32 = keccak256(input)
data: bytes32 = keccak256(concat(_CREATE2_PREFIX, empty(bytes12), convert(deployer, bytes20), salt, bytecode_hash, constructor_input_hash))

return convert(convert(data, uint256) & convert(max_value(uint160), uint256), address)

With these functions, you can now derive the address you intend to have.

Since using Vyper in-built create_minimal_proxy_to for contract deployment isn't possible, you'll need to resort to Solidity for creating the create2 deployment contract. If you're well-versed in Solidity, you can opt for an alternative method. Let's begin by penning down a create2 contract using Solidity:

pragma solidity ^0.8.0;

contract CreateZksyncContract {

function create2(bytes32 _salt, bytes memory _bytecode) external returns (address addr) {

assembly {
addr := create2(0, add(_bytecode, 32), mload(_bytecode), _salt)
}

}
}

Next, refer to this contract interface within your Vyper code, such as:

# @version 0.3.9

interface CreateAddress:
def compute_address(salt: bytes32, bytecode_hash: bytes32, deployer: address, input: Bytes[4096]) -> address: pure

interface CreateNewAddress:
def create2(_salt: bytes32, _bytecode_hash: Bytes[266]): nonpayable


_CREATE2_PREFIX: constant(bytes32) = 0x2020dba91b30cc0006188af794c2fb30dd8520db7e2c088b7fc7c103c00ca494
FACTORY: constant(address) = 0x0000000000000000000000000000000000008006


@external
@pure
def compute_address(salt: bytes32, bytecode_hash: bytes32, deployer: address, input: Bytes[4_096]=b"") -> address:

constructor_input_hash: bytes32 = keccak256(input)
data: bytes32 = keccak256(concat(_CREATE2_PREFIX, empty(bytes12), convert(deployer, bytes20), salt, bytecode_hash, constructor_input_hash))

return convert(convert(data, uint256) & convert(max_value(uint160), uint256), address)


@view
@external
def compute_address_self(salt: bytes32, bytecode_hash: bytes32, deployer: address, input: Bytes[4096]) -> address:
return CreateAddress(self).compute_address(salt, bytecode_hash, deployer, input)


@external
def deploy_contract(_salt: bytes32, _bytecode_hash: Bytes[266]):
CreateNewAddress(FACTORY).create2(_salt, _bytecode_hash)

With this method, the address you aim to get should match the one you deploy.

Parameter Explanation:

  • sender: The deployer, which in this instance is the CreateZksyncContract contract address.
  • bytecodehash: The hash of the bytecode of the contract you wish to deploy. If you’re unsure about obtaining this, you can parse the Deploy event during contract deployment. Copy the bytecode and replace the segment in the following example: “0100006dfff1b6fba991ac1d4187f9217fc872cd4de60ea3e735e93a04ee441".
0x0000000000000000000000000000000000000000000000000000000000000000000000000100006dfff1b6fba991ac1d4187f9217fc872cd4de60ea3e735e93a04ee441000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
  • salt: Any information you’d like to input.
  • input: Fill in constructor data if the contract you’re deploying has a constructor; otherwise, simply input “0x”.

This article is proudly supported by @pcaversaccio

Key links:

Official Site: https://zkape.io

Twitter: https://twitter.com/zk_apes

Discord: http://discord.gg/zkape

Guild: https://guild.xyz/zkape

QuestN: https://app.questn.com/zkapes

Medium: https://zkape.medium.com/

Docs: https://docs.zkape.io

--

--

zkApe

First Apes-Thematic #ERC6551 & Smart contract wallet on #zkSyncEra with Account Abstraction 🤖 Join community: http://discord.gg/zkape