Security and best practices
Before developing on a ZKsync chain, review these best practices. Following these recommendations will help you optimize performance, maintain security, and align your contracts with the design of the EraVM.
Use call instead of .send or .transfer
Avoid using payable(addr).send(x) or payable(addr).transfer(x).
These functions only provide a 2300 gas stipend, which may not be enough if the call triggers state changes or other operations that require more L2 gas.
Use call instead.
Instead of:
payable(addr).send(x); // or
payable(addr).transfer(x);
Use:
(bool s, ) = addr.call{value: x}("");
require(s);
This approach replicates the behavior of .send or .transfer while providing more flexibility and avoiding gas-limit issues.
See Consensys’ explanation of .transfer risks for additional context.
While .call is safer in terms of gas, it does not provide built-in reentrancy protection. To prevent vulnerabilities:
- Follow the checks-effects-interactions pattern.
- Use a reentrancy guard when interacting with external contracts.
These practices help secure your contracts against reentrancy attacks and unexpected external behavior.
Use the Proxy Pattern
ZKsync chains running EraVM support Solidity and Vyper code compilation into EraVM bytecode using custom compilers. Although the compilers undergoes extensive testing to ensure EVM compatibility, minor issues can occasionally appear.
solc compiler.To handle potential compiler patches or updates, use the Proxy pattern when deploying new contracts. This allows you to recompile and upgrade your contract logic without requiring a full redeployment.
After your contracts have proven stable, you can migrate to an immutable version if desired.
Using the Proxy pattern during the initial deployment phase reduces maintenance effort and ensures flexibility as the EraVM continues to evolve.
Do Not Rely on Ethereum Gas Logic
The gas model on ZKsync chains differs from Ethereum’s. Two key factors influence gas costs:
- State-diff-based data availability – transaction costs depend on L1 gas prices.
- Distinct computational model – the EraVM uses a different opcode pricing model and computational trade-offs compared to the EVM.
Because of these differences, gas usage on a ZKsync chain cannot be directly compared to Ethereum. Avoid hardcoding gas constants in your contracts, as future changes to ZKsync’s fee model could make these assumptions invalid.
Account for gasPerPubdataByte
Each transaction on ZKsync includes a constant called gasPerPubdataByte.
This value is set by the operator, although users define an upper bound for it in EIP-712 transactions.
Since gasPerPubdataByte fluctuates with L1 gas prices, relying solely on gas limits can lead to unpredictable behavior.
For example, in Gnosis Safe’s execTransaction method:
// We require some gas to emit the events (at least 2500) after the execution and some to perform code until the execution (500)
// We also include the 1/64 in the check that is not send along with a call to counteract potential shortcomings because of EIP-150
require(gasleft() >= ((safeTxGas * 64) / 63).max(safeTxGas + 2500) + 500, "GS010");
// Use scope here to limit variable lifetime and prevent `stack too deep` errors
{
uint256 gasUsed = gasleft();
// If the gasPrice is 0 we assume that nearly all available gas can be used (it is always more than safeTxGas)
// We only subtract 2500 (compared to the 3000 before) to ensure that the amount passed is still higher than safeTxGas
success = execute(to, value, data, operation, gasPrice == 0 ? (gasleft() - 2500) : safeTxGas);
gasUsed = gasUsed.sub(gasleft());
// ...
}
This logic enforces a minimum gasleft() but does not account for gasPerPubdata.
A malicious user could exploit a high gasPerPubdata value to increase gas costs or cause transaction failures.
When porting logic from Ethereum:
- Include
gasPerPubdatain your gas calculations. - Add a safety margin to handle price fluctuations.
- Avoid assumptions based on Ethereum’s static gas model.
Currently, ZKsync operators use accurate and fair values for ETH L1 pricing and gasPerPubdata,
but preparing your contracts for future decentralization ensures long-term stability and reliability.
Use Native Account Abstraction for Validation
Do not rely on ecrecover for signature verification.
ZKsync chains running EraVM use native account abstraction, which supports flexible authentication
mechanisms such as multisignature wallets or custom signature schemes.
Assuming all accounts use ECDSA private keys may cause compatibility issues. Instead, rely on the built-in account abstraction mechanisms for validation to ensure forward compatibility.
Learn more in the Account Abstraction guide.
Test in a Local Environment
Always test your contracts locally before deploying to mainnet. Local testing allows you to verify contract behavior in a controlled environment, reduce latency, and avoid transaction costs.
ZKsync provides local testing environments that simulate mainnet behavior for development purposes.
To test locally:
- Set up the local environment using one of the available tools.
- Run your tests with frameworks such as Foundry and Hardhat to verify expected behavior.
- Review results and confirm that events, gas usage, and outputs match expectations.
- Iterate and refine before deploying to testnet or mainnet.
Local testing helps you identify issues early, ensuring a more reliable and predictable deployment on ZKsync chains.
By following these best practices, you can improve the security, reliability, and maintainability of your smart contracts across all ZKsync chains powered by the EraVM.