EVM Instructions
CREATE, CREATE2
CREATE, CREATE2On ZKsync Era, contract deployment is performed using the hash of the bytecode, and the factoryDeps field of EIP712 transactions contains the bytecode. The actual deployment occurs by providing the contract's hash to the ContractDeployer system contract.
To guarantee that create/create2 functions operate correctly, the compiler must be aware of the bytecode of the deployed contract in advance. The compiler interprets the calldata arguments as incomplete input for ContractDeployer, as the remaining part is filled in by the compiler internally. The Yul datasize and dataoffset instructions have been adjusted to return the constant size and bytecode hash rather than the bytecode itself.
The code below should work as expected:
MyContract a = new MyContract();
MyContract a = new MyContract{salt: ...}();In addition, the subsequent code should also work, but it must be explicitly tested to ensure its intended functionality:
bytes memory bytecode = type(MyContract).creationCode;
assembly {
addr := create2(0, add(bytecode, 32), mload(bytecode), salt)
}The following code will not function correctly because the compiler is not aware of the bytecode beforehand:
function myFactory(bytes memory bytecode) public {
assembly {
addr := create(0, add(bytecode, 0x20), mload(bytecode))
}
}Unfortunately, it's impossible to differentiate between the above cases during compile-time. As a result, we strongly recommend including tests for any factory that deploys child contracts using type(T).creationCode.
Since the deploy and runtime code is merged together on ZKsync Era, we do not support type(T).runtimeCode and it always produces a compile-time error.
Address derivation
For zkEVM bytecode, ZKsync Era uses a distinct address derivation method compared to Ethereum. The precise formulas can be found in our SDK, as demonstrated below:
Since the bytecode differs from Ethereum as ZKsync uses a modified version of the EVM, the address derived from the bytecode hash will also differ. This means that the same bytecode deployed on Ethereum and ZKsync will have different addresses and the Ethereum address will still be available and unused on ZKsync. If and when the zkEVM reaches parity with the EVM, the address derivation will be updated to match Ethereum and the same bytecode will have the same address on both chains, deployed bytecodes to different addresses on ZKsync could then be deployed to the same the Ethereum-matching addresses on ZKsync.
CALL, STATICCALL, DELEGATECALL
CALL, STATICCALL, DELEGATECALLFor calls, you specify a memory slice to write the return data to, e.g. out and outsize arguments for call(g, a, v, in, insize, out, outsize). In EVM, if outsize != 0, the allocated memory will grow to out + outsize (rounded up to the words) regardless of the returndatasize.
On ZKsync Era, returndatacopy, similar to calldatacopy, is implemented as a cycle iterating over return data with a few additional checks: a call returndatacopy(destOffset, offset, size) will trigger a panic if offset + size > returndatasize to simulate the same behavior as in EVM. See EIP-211 for more details.
Thus, unlike EVM where memory growth occurs before the call itself, on ZKsync Era, the necessary copying of return data happens only after the call has ended, leading to a difference in msize() and sometimes ZKsync Era not panicking where EVM would panic due to the difference in memory growth.
Additionally, there is no native support for passing Ether on ZKsync Era, so it is handled by a special system contract called MsgValueSimulator. The simulator receives the callee address and Ether amount, performs all necessary balance changes, and then calls the callee.
MSTORE, MLOAD
MSTORE, MLOADUnlike EVM, where the memory growth is in words, on zkEVM the memory growth is counted in bytes. For example, if you write mstore(100, 0) the msize on zkEVM will be 132, but on the EVM it will be 160. Note, that also unlike EVM which has quadratic growth for memory payments, on zkEVM the fees are charged linearly at a rate of 1 erg per byte.
The other thing is that our compiler can sometimes optimize unused memory reads/writes. This can lead to different msize compared to Ethereum since fewer bytes have been allocated, leading to cases where EVM panics, but zkEVM will not due to the difference in memory growth.
CALLDATALOAD, CALLDATACOPY
CALLDATALOAD, CALLDATACOPYIf the offset for calldataload(offset) is greater than 2^32-33 then execution will panic.
Internally on zkEVM, calldatacopy(to, offset, len) there is just a loop with the calldataload and mstore on each iteration. That means that the code will panic if 2^32-32 + offset % 32 < offset + len.
RETURN, STOP
RETURN, STOPConstructors return the array of immutable values. If you use RETURN or STOP in an assembly block in the constructor on ZKsync Era, it will leave the immutable variables uninitialized.
TIMESTAMP, NUMBER
TIMESTAMP, NUMBERFor more information about blocks on ZKsync Era, including the differences between block.timestamp and block.number, check out the blocks on ZKsync Documentation.
Changes From the Previous Protocol Version Modifications were performed on how certain block properties were implemented on ZKsync Era. For details on the changes performed visit the announcement on GitHub.
COINBASE
COINBASEReturns the address of the Bootloader contract, which is 0x8001 on ZKsync Era.
DIFFICULTY, PREVRANDAO
DIFFICULTY, PREVRANDAOReturns a constant value of 2500000000000000 on ZKsync Era.
BASEFEE
BASEFEEThis is not a constant on ZKsync Era and is instead defined by the fee model. Most of the time it is 0.25 gwei, but under very high L1 gas prices it may rise.
SELFDESTRUCT
SELFDESTRUCTConsidered harmful and deprecated in EIP-6049.
Always produces a compile-time error with the zkEVM compiler.
CALLCODE
CALLCODEDeprecated in EIP-2488 in favor of DELEGATECALL.
Always produces a compile-time error with the zkEVM compiler.
PC
PCInaccessible in Yul and Solidity >=0.7.0, but accessible in Solidity 0.6.
Always produces a compile-time error with the zkEVM compiler.
CODESIZE
CODESIZESize of the constructor arguments
Contract size
Yul uses a special instruction datasize to distinguish the contract code and constructor arguments, so we substitute datasize with 0 and codesize with calldatasize in ZKsync Era deployment code. This way when Yul calculates the calldata size as sub(codesize, datasize), the result is the size of the constructor arguments.
CODECOPY
CODECOPYCopies the constructor arguments
Zeroes memory out
Compile-time error
EXTCODECOPY
EXTCODECOPYContract bytecode cannot be accessed on zkEVM architecture. Only its size is accessible with both CODESIZE and EXTCODESIZE.
EXTCODECOPY always produces a compile-time error with the zkEVM compiler.
DATASIZE, DATAOFFSET, DATACOPY
DATASIZE, DATAOFFSET, DATACOPYContract deployment is handled by two parts of the zkEVM protocol: the compiler front end and the system contract called ContractDeployer.
On the compiler front-end the code of the deployed contract is substituted with its hash. The hash is returned by the dataoffset Yul instruction or the PUSH [$] EVM legacy assembly instruction. The hash is then passed to the datacopy Yul instruction or the CODECOPY EVM legacy instruction, which writes the hash to the correct position of the calldata of the call to ContractDeployer.
The deployer calldata consists of several elements:
Deployer method signature
0
4
Salt
4
32
Contract hash
36
32
Constructor calldata offset
68
32
Constructor calldata length
100
32
Constructor calldata
132
N
The data can be logically split into header (first 132 bytes) and constructor calldata (the rest).
The header replaces the contract code in the EVM pipeline, whereas the constructor calldata remains unchanged. For this reason, datasize and PUSH [$] return the header size (132), and the space for constructor arguments is allocated by solc on top of it.
Finally, the CREATE or CREATE2 instructions pass 132+N bytes to the ContractDeployer contract, which makes all the necessary changes to the state and returns the contract address or zero if there has been an error.
If some Ether is passed, the call to the ContractDeployer also goes through the MsgValueSimulator just like ordinary calls.
We do not recommend using CREATE for anything other than creating contracts with the new operator. However, a lot of contracts create contracts in assembly blocks instead, so authors must ensure that the behavior is compatible with the logic described above.
Yul example:
EVM legacy assembly example:
SETIMMUTABLE, LOADIMMUTABLE
SETIMMUTABLE, LOADIMMUTABLEzkEVM does not provide any access to the contract bytecode, so the behavior of immutable values is simulated with the system contracts.
The deploy code, also known as constructor, assembles the array of immutables in the auxiliary heap. Each array element consists of an index and a value. Indexes are allocated sequentially by
zksolcfor each string literal identifier allocated bysolc.The constructor returns the array as the return data to the contract deployer.
The array is passed to a special system contract called
ImmutableSimulator, where it is stored in a mapping with the contract address as the key.In order to access immutables from the runtime code, contracts call the
ImmutableSimulatorto fetch a value using the address and value index. In the deploy code, immutable values are read from the auxiliary heap, where they are still available.
The element of the array of immutable values:
Yul example:
EVM legacy assembly example:
Last updated