Compilation Limitations

These limitations apply to zksolc compilation of source contracts.

Contract Bytecode Access

Contract bytecode cannot be accessed on zkEVM architecture, therefore EXTCODECOPY always produces a compile-time error with zksolc. As such using address(..).code in a solidity contract will produce a compile-time error.

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
    }
}

See here on how to circumvent this issue.

Contract Size Limit

zksolc currently limits the number of instructions to 2^16 that are compiled for a contract. As such for large contracts, the compilation will fail with the 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

Solution

There are three possible solutions to address this issue:

  1. 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. Compilation with --zk-fallback-oz=true:

    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.

    Before (single large contract):

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

    After (multiple 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
    }