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