ZKsync Era runs Solidity, uses Ethereum addresses and speaks the Ethereum JSON-RPC, so it's easy to assume it's the EVM. For contracts compiled the usual ZKsync way, it isn't. They run on EraVM, a different virtual machine designed for proving, and a contract that is correct on Ethereum can compute wrong addresses, fail to deploy children, reject users or lock funds there. Most of the differences fall into a handful of categories.
zksolc and EraVM, not solc and the EVM
When you build for ZKsync with forge build --zksync or hardhat-zksync, zksolc uses solc as a front end, takes its Yul or EVM assembly output, and compiles that through LLVM into EraVM bytecode. EraVM is register-based, has its own gas schedule, and represents some EVM features as calls to system contracts (the deployer, nonce holder, ETH balance and so on).
A few EVM features therefore don't exist or behave differently. SELFDESTRUCT, CALLCODE and EXTCODECOPY are not supported and fail at compile time, and assembly that inspects or copies bytecode generally won't work.
ZKsync has also shipped an EVM bytecode interpreter that lets unmodified EVM bytecode run on Era, at a higher gas cost than native EraVM code, and its newer ZKsync OS stack is designed around EVM execution. Which of these your target chain supports, and how completely, depends on its protocol version, so check the current docs. Everything below applies to contracts compiled to EraVM with zksolc, which is still what the ZKsync-specific toolchains produce.
CREATE and CREATE2 addresses are different
On Ethereum, CREATE2 gives keccak256(0xff ++ deployer ++ salt ++ keccak256(initCode)). On EraVM the formula uses a different prefix, a ZKsync-specific bytecode hash (a versioned hash of the EraVM bytecode, not keccak256 of init code) and the hash of the constructor input separately:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
library ZkCreate2 {
bytes32 private constant CREATE2_PREFIX = keccak256("zksyncCreate2");
function computeAddress(
address deployer,
bytes32 salt,
bytes32 bytecodeHash,
bytes memory constructorInput
) internal pure returns (address) {
bytes32 h = keccak256(
bytes.concat(
CREATE2_PREFIX,
bytes32(uint256(uint160(deployer))),
salt,
bytecodeHash,
keccak256(constructorInput)
)
);
return address(uint160(uint256(h)));
}
}