Deploying Smart Contracts to zkSync
zkSync Era is a ZK Rollup with EVM compatibility, but not EVM equivalence. Most Solidity contracts compile and deploy without changes, but there is zksolc compiler specifics and a number of opcodes that behave differently. You need to know this before deployment, otherwise the contract will deploy but work incorrectly.
zkSync Era Specifics
zksolc Compiler
zkSync uses its own compiler zksolc, which compiles Solidity to EraVM bytecode (not EVM bytecode). This is important difference:
- Contract deployed on zkSync has different bytecode than the same contract on Ethereum
-
CREATEandCREATE2work differently: before deployment bytecode must be declared in transaction (feature "factory dependencies") -
SELFDESTRUCT— can deploy but opcode doesn't delete contract (changed in EIP-6049, zkSync follows this semantics) -
PUSH0— supported from certain zksolc versions; for old contracts withpragma solidity ^0.8.19may be problem
Gas Model Differences
zkSync Era has two-dimensional gas model: gasLimit (computation) + gasPerPubdata (cost of publishing data to L1). When deploying contracts with large bytecode gasPerPubdata may dominate over computational gas.
Deployment via Hardhat
npm install -D @matterlabs/hardhat-zksync @matterlabs/zksync-contracts
// hardhat.config.ts
import { HardhatUserConfig } from 'hardhat/config'
import '@matterlabs/hardhat-zksync'
const config: HardhatUserConfig = {
zksolc: {
version: 'latest',
settings: {
optimizer: {
enabled: true,
mode: '3', // z (size), s (speed), 3 (balanced)
},
},
},
networks: {
zkSyncMainnet: {
url: 'https://mainnet.era.zksync.io',
ethNetwork: 'mainnet',
zksync: true,
verifyURL: 'https://zksync2-mainnet-explorer.zksync.io/contract_verification',
},
zkSyncTestnet: {
url: 'https://sepolia.era.zksync.dev',
ethNetwork: 'sepolia',
zksync: true,
verifyURL: 'https://explorer.sepolia.era.zksync.dev/contract_verification',
},
},
solidity: '0.8.24',
}
export default config
Deploy script:
import { Wallet, Provider } from 'zksync-ethers'
import { Deployer } from '@matterlabs/hardhat-zksync'
import { HardhatRuntimeEnvironment } from 'hardhat/types'
export default async function (hre: HardhatRuntimeEnvironment) {
const provider = new Provider(hre.network.config.url)
const wallet = new Wallet(process.env.DEPLOYER_PRIVATE_KEY!, provider)
const deployer = new Deployer(hre, wallet)
// Load artifact (zksolc compiles to zk-specific format)
const artifact = await deployer.loadArtifact('MyContract')
// Estimate deployment cost
const deploymentFee = await deployer.estimateDeployFee(artifact, [/* constructor args */])
console.log(`Estimated deploy fee: ${ethers.formatEther(deploymentFee)} ETH`)
const contract = await deployer.deploy(artifact, [/* constructor args */])
await contract.waitForDeployment()
console.log(`Deployed to: ${await contract.getAddress()}`)
}
npx hardhat deploy-zksync --script deploy.ts --network zkSyncMainnet
Deployment via Foundry (zkSync Fork)
Foundry officially supports zkSync via foundry-zksync — a fork adding --zksync flag:
# Install foundry-zksync
curl -L https://raw.githubusercontent.com/matter-labs/foundry-zksync/main/install-foundry-zksync | bash
# Deploy
forge create src/MyContract.sol:MyContract \
--rpc-url https://mainnet.era.zksync.io \
--private-key $PRIVATE_KEY \
--zksync \
--constructor-args "arg1" 123
# Or via script
forge script script/Deploy.s.sol \
--rpc-url https://mainnet.era.zksync.io \
--private-key $PRIVATE_KEY \
--zksync \
--broadcast
Contract Verification
# Via Hardhat
npx hardhat verify --network zkSyncMainnet CONTRACT_ADDRESS "constructor_arg1"
# Via zkSync Explorer API directly
curl -X POST https://zksync2-mainnet-explorer.zksync.io/contract_verification \
-H "Content-Type: application/json" \
-d '{
"contractAddress": "0x...",
"sourceCode": "...",
"contractName": "MyContract",
"compilerZksolcVersion": "v1.4.1",
"compilerSolcVersion": "0.8.24",
"optimizationUsed": true
}'
Native Account Abstraction
Key advantage of zkSync Era — native AA at protocol level (unlike ERC-4337, which works via entrypoint contract). Every account can be a smart contract. To deploy AA account:
// Contract must implement IAccount interface
import "@matterlabs/zksync-contracts/l2/system-contracts/interfaces/IAccount.sol";
contract MyAccount is IAccount {
function validateTransaction(
bytes32 _txHash,
bytes32 _suggestedSignedHash,
Transaction calldata _transaction
) external payable override returns (bytes4 magic) {
// Custom validation logic (multisig, session keys, etc.)
}
function executeTransaction(
bytes32 _txHash,
bytes32 _suggestedSignedHash,
Transaction calldata _transaction
) external payable override {
// Execution
}
// ...
}
Bridges and Depositing ETH
For deployment you need ETH on zkSync. Bridge via official zkSync bridge (~15–30 minutes L1 finalization). Programmatic bridge via L1 contract:
import { Provider, Wallet, utils } from 'zksync-ethers'
import { ethers } from 'ethers'
const l1Provider = new ethers.JsonRpcProvider(L1_RPC_URL)
const l2Provider = new Provider('https://mainnet.era.zksync.io')
const wallet = new Wallet(PRIVATE_KEY, l2Provider, l1Provider)
// Deposit 0.1 ETH from L1 to L2
const depositHandle = await wallet.deposit({
token: utils.ETH_ADDRESS,
amount: ethers.parseEther('0.1'),
})
await depositHandle.waitFinalize()
Deploying simple contract takes several hours (environment preparation + verification). Migrating existing Ethereum project to zkSync accounting for compiler specifics and testing — 1–2 days.







