Compilation

Understand the key differences when compiling contracts with foundry-zksync.

zksolc is the compiler used by ZKsync to convert solidity code to zkEVM-compatible bytecode. It uses the same input format as solc but the output bytecodes and their respective hashes. Internally it uses a custom-compiled solc.

Below are the common compilation differences development teams will need to address when making use of Foundry ZKsync.

Contract Bytecode Access

Contract bytecode cannot be accessed on the ZKsync architecture due to the way zkEVM handles certain instructions like EXTCODECOPY. As a result, using address(..).code in a Solidity contract will produce a compile-time error with zksolc.

Issue

The EXTCODECOPY instruction is not supported in ZKsync's architecture. Any attempt to access a contract's bytecode using address(..).code will fail to compile.

Problematic Code

This code will fail during compilation because address(..).code is unsupported in ZKsync:

contract FooBar {
    function number() return (uint8) {
        return 10;
    }
}

contract FooTest is Test {
    function testFoo() public {
        FooBar target = new FooBar();
        address(target).code;   // will fail at compile-time
    }
}

Compilation Error

When you attempt to compile this code, you will see the following error message:

Error
Error: LLVM IR generator definition pass: 11508:17 The `EXTCODECOPY` instruction is not supported
--> test/FooTest.t.sol:FooTest

Solution

To work around this limitation, you can use Foundry's FFI (Foreign Function Interface) functionality to access contract bytecode and hash it externally, and then pass it back into the contract. This approach leverages cheatcodes to read the bytecode from a file.

Fixed Code Example

Here's how you can modify the code to avoid the use of address(..).code:

contract Calculator {
    function add(uint8 a, uint8 b) return (uint8) {
        return a + b;
    }
}

contract FooTest is Test {
    function testFoo() public {
        string memory artifact = vm.readFile(
            "zkout/FooTest.sol/Calculator.json"
        );
        bytes32 bytecodeHash = vm.parseJsonBytes32(artifact, ".hash");
        bytes32 salt = 0x0000000000000000000000000000000000000001;

        ISystemContractDeployer deployer = ISystemContractDeployer(
            address(0x0000000000000000000000000000000000008006)
        );
        address addr = deployer.getNewAddressCreate2(
            address(this),
            salt,
            bytecodeHash,
            ""
        );
    }
}

Additional Configuration

This solution requires enabling read permissions in the foundry.toml file for the file access during testing:

[profile.default]
fs_permissions = [{ access = "read", path = "./zkout/FooTest.sol/Calculator.json"}]

Contract Size Limit

The zksolc compiler enforces a limit on the number of instructions a contract can have, capped at 2^16 instructions. If a contract exceeds this limit, the compilation will fail.

Issue

Contracts with a large number of instructions will not compile on ZKsync due to the 65535 addressable space limitation imposed by zksolc.

Problematic Scenario

If you attempt to compile a large contract that exceeds this instruction limit, the compilation will fail with an error.

Example of Compilation Error

Error: assembly-to-bytecode conversion: assembly parse error Label DEFAULT_UNWIND was tried to be used
for either PC or constant at offset 65947 that is more than 65535 addressable space

This error indicates that the contract's instruction count has exceeded the maximum allowed limit.

Solution

There are three possible solutions to address this issue:

  1. Attempt Compilation with --zk-force-evmla=true: You can attempt to compile the contract using ZKsync's EVM legacy architecture by adding the --zk-force-evmla=true flag. This can sometimes bypass the contract size limit by compiling in a different mode.
    Example command:
    forge build --zk-force-evmla=true --zksync
    
  2. Use the --zk-fallback-oz=true flag: If the contract size still exceeds the limit, try compiling with optimization level -Oz by using the --zk-fallback-oz=true flag. This tells the compiler to fall back to -Oz optimization when the bytecode is too large, potentially reducing the contract size further.
    Example command:
    forge build --zk-fallback-oz=true --zksync
    
  3. Split the Contract into Smaller Units: If neither of the above flags resolves the issue, the contract must be refactored into smaller, modular contracts. This involves separating your logic into different contracts and using contract inheritance or external contract calls to maintain functionality.

Example of Refactoring

Here’s an example of splitting a large contract into smaller units:

Before (single large contract):

contract LargeContract {
    function largeFunction1() public { /* complex logic */ }
    function largeFunction2() public { /* complex logic */ }
    // Additional large functions and state variables...
}

After (split into smaller contracts):

contract ContractPart1 {
    function part1Function() public { /* logic from largeFunction1 */ }
}

contract ContractPart2 {
    function part2Function() public { /* logic from largeFunction2 */ }
}

contract MainContract is ContractPart1, ContractPart2 {
    // Logic to combine the functionalities of both parts
}

Non-inlineable Libraries

Libraries that contain public or external methods cannot be inlined when compiling to ZKsync VM bytecode. Although Solidity can inline libraries during EVM compilation, this inlining is not supported in the Yul intermediate representation, which is used by zksolc to compile for ZKsync.

Issue

On ZKsync, if your project contains non-inlineable libraries, the compilation will fail because these libraries cannot be inlined during the Yul to ZKsync bytecode compilation step.

Problematic Scenario

Consider the following library that calculates the square of a number:

pragma solidity ^0.8.0;

library MiniMath {
    function square(uint256 x) public pure returns (uint256) {
         return x * x;
    }
}

Now, assume you have a smart contract that uses this library:

pragma solidity ^0.8.0;

import "./MiniMath.sol";

contract Main {
    uint256 public lastNumber;

    function storeSquare(uint256 x) public {
        uint256 square = MiniMath.square(x);
        lastNumber = square;
    }
}

When you try to compile this project, the compiler will fail because the MiniMath library is not inlined due to the public method.

Example of Compilation Error

Error:
Missing libraries detected [MiniMath { contract_name: "Main", contract_path: "contracts/Main.sol", missing_libraries: [] }]

This error indicates that the MiniMath library is missing because it cannot be inlined and must be deployed separately.

Solution

Libraries with public or external methods must be deployed separately, and their addresses must be passed to the compiler. The library methods will be called through their deployed address, replacing inlining.

Steps to Fix

  1. Deploy the Missing Libraries: Run the following command to deploy the missing library:
    forge create --deploy-missing-libraries --private-key <PRIVATE_KEY> --rpc-url <RPC_URL> --chain <CHAIN_ID> --zksync
    

    This command will deploy the library (MiniMath in this case) to the specified network.
  2. Compile the Main Contract: After deploying the library, proceed with compiling the main contract by specifying the deployed library addresses during compilation:
    forge build --zksync
    

    The deployed address will be linked to the main contract, and library methods will be invoked via external calls rather than inline.

Made with ❤️ by the ZKsync Community