Our engineers often see developers porting EVM contracts to zkSync without changes — and get surprises: CREATE2 doesn't work, gas goes 3x higher than expected, verification fails due to bytecode incompatibility. In this article, we break down how to correctly deploy zkSync smart contracts on zkSync Era: with zksolc, EraVM, Native Account Abstraction, and the two-dimensional gas model. Deploying smart contracts zkSync requires understanding these differences.
Problems We Solve
Bytecode incompatibility. zksolc generates EraVM bytecode, not EVM. If you don't specify factory dependencies, CREATE (and CREATE2) simply won't work — you need to declare all contracts created from code in advance. Gas model. zkSync uses two-dimensional gas: regular gas for execution and gasPerPubdata for publishing data to L1. When deploying a heavy contract (e.g., with large constructors), the final cost can be 2–3 times higher than on Ethereum Mainnet if you don't optimize the payload. Lack of documentation. Many don't know about Native Account Abstraction zkSync and miss the chance to implement smart accounts with custom validation directly at the protocol level, without a separate entrypoint.
If you've faced these issues, contact us — we'll help.
How We Do It
We use a proven stack: Hardhat zkSync plugin (for complex projects) or Foundry zkSync (for quick prototypes). We configure the optimizer settings (size/speed/balanced) based on project specifics — they affect contract size by up to 40% and gas usage by up to 50%. Below is a tool comparison:
| Tool | Compilation | Verification | Flexibility | Ideal for |
|---|---|---|---|---|
| Hardhat | zksolc, full support | Built-in via plugin | High, many modules | Production contracts with many dependencies |
| Foundry zkSync | forge with --zksync | Via API | Medium, fewer plugins | Quick prototypes and tests |
Foundry zkSync, by our measurements, deploys contracts 2–3 times faster due to direct transaction submission, but Hardhat gives more detailed control over factory dependencies and verification.
| zksolc Optimization Mode | Contract Size | Execution Gas | Where to Apply |
|---|---|---|---|
| size | -40% | +20% | Lightweight contracts with few calls |
| speed | +10% | -30% | Frequently called contracts (tokens, DEX) |
| balanced | baseline | baseline | Universal option |
The size mode reduces contract size by 40% compared to balanced mode, but increases execution gas by 20%, making it ideal for rarely-called contracts.
zksolc and Its Importance
The zksolc compiler converts Solidity into bytecode for the custom EraVM. This means:
- Standard patterns (CREATE2 for deterministic deployment) require explicit factory dependencies. Without them, deployment fails with
ContractDeployedWithoutFactoryDep. - The
SELFDESTRUCTopcode does not destroy the contract — zkSync follows EIP-6049 and returns an error. -
PUSH0may not be supported in older zksolc versions, breaking contracts withpragma ^0.8.19. In such cases, usepragma ^0.8.13or add a fallback.
ZkSync Developer Guide states: 'Factory dependencies must be declared for all contracts created via CREATE.'
Deploying via Hardhat
Install the plugin and configure the config:
// hardhat.config.ts
import { HardhatUserConfig } from 'hardhat/config'
import '@matterlabs/hardhat-zksync'
const config: HardhatUserConfig = {
zksolc: {
version: 'latest',
settings: {
optimizer: {
enabled: true,
mode: '3',
},
},
},
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
Deployment script using Deployer:
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)
const artifact = await deployer.loadArtifact('MyContract')
const deploymentFee = await deployer.estimateDeployFee(artifact, [])
console.log(`Estimated deploy fee: ${ethers.formatEther(deploymentFee)} ETH`)
const contract = await deployer.deploy(artifact, [])
await contract.waitForDeployment()
console.log(`Deployed to: ${await contract.getAddress()}`)
}
Run: npx hardhat deploy-zksync --script deploy.ts --network zkSyncMainnet
Deploying via Foundry
Install Foundry zkSync and run:
curl -L https://raw.githubusercontent.com/matter-labs/foundry-zksync/main/install-foundry-zksync | bash
forge create src/MyContract.sol:MyContract \
--rpc-url https://mainnet.era.zksync.io \
--private-key $PRIVATE_KEY \
--zksync \
--constructor-args "arg1" 123
Verification via API is done by sending a POST request to https://zksync2-mainnet-explorer.zksync.io/contract_verification with a JSON body.
Native Account Abstraction in zkSync
Native AA works at the protocol level — every account can be a smart contract. To implement it, you need to implement the 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
}
function executeTransaction(
bytes32 _txHash,
bytes32 _suggestedSignedHash,
Transaction calldata _transaction
) external payable override {
// execution
}
}
This saves up to 30% gas compared to ERC-4337 due to the absence of a separate entrypoint contract.
Why zkSync Requires Special Gas Optimization
Two-dimensional gas is a key feature. gasPerPubdata can account for up to 40% of transaction cost. Optimize calldata: avoid long string arguments and arrays in constructors. Use the 'size' optimization mode to save space if the contract is deployed once and rarely called. Understanding the gas model zkSync is crucial for cost optimization.
Typical Mistakes When Deploying on zkSync
- Missing factory dependencies — the most common cause of deployment failure. For every contract created via CREATE/CREATE2, you must add its address to the dependencies array during deployment.
- Ignoring two-dimensional gas — gas for publishing data (gasPerPubdata) can account for up to 40% of transaction cost.
- Using unsupported opcodes — SELFDESTRUCT, PUSH0, some versions of DELEGATECALL may not work or work differently. Always test on testnet.
Checklist for zkSync compatibility
- Check factory dependencies for all dynamic contracts
- Ensure unsupported opcodes (SELFDESTRUCT, PUSH0) are not used
- Configure zksolc optimizer (size/speed/balanced mode)
- Test on zkSync Sepolia testnet
- Verify the contract after deployment
What's Included in Deployment Work?
Step 1. Analysis and audit of existing contracts for zkSync compatibility. We check on-chain and off-chain security, identify problematic opcodes. Step 2. Setting up the zksolc compiler and optimizing the gas model. We select the optimization mode and configure factory dependencies. Step 3. Preparing deployment scripts (Hardhat/Foundry). Includes gas estimation, factory dependencies, network configuration. Step 4. Contract verification zkSync on the block explorer. We use the hardhat-zksync-verify plugin or direct API calls. Step 5. Writing deployment documentation and operational recommendations. We record all parameters and dependencies. Step 6. Technical support during launch (2 weeks). We help integrate the contract into interfaces.
With 7+ years of blockchain experience and over 200 smart contracts deployed, our team ensures a 100% success rate on over 50 L2 deployments. For complex projects, we add formal verification using solc-verify or Certora.
Deployment cost for a typical ERC-20 contract starts at $199. Our optimization can save you up to $300 per month on gas fees.
Timeline: 1 to 5 days depending on complexity.
Get a consultation — contact us for a free audit of your contract's zkSync compatibility. Turnkey contract deployment with compatibility guarantee.







