Fee model
This document will assume that you already know how gas & fees work on Ethereum.
On Ethereum, all the computational, as well as storage costs, are represented via one unit: gas. Each operation costs a certain amount of gas, which is generally constant (though it may change during upgrades).
Main differences from EVM
ZKsync as well as other L2s have the issue that does not allow the adoption of the same model as the one for Ethereum so easily: the main reason is the requirement for publishing the pubdata on Ethereum. This means that prices for L2 transactions will depend on the volatile L1 gas prices and can not be simply hard coded.
Also, ZKsync, being a zkRollup is required to prove every operation with zero-knowledge proofs. That comes with a few nuances.
Different opcode pricing
The operations tend to have different “complexity”/”pricing” in zero-knowledge proof terms than in standard CPU terms.
For instance, keccak256
which was optimized for CPU performance, will cost more to prove.
That’s why you will find the prices for operations on ZKsync a lot different from the ones on Ethereum.
I/O pricing
On Ethereum, whenever a storage slot is read/written to for the first time, a certain amount of gas is charged for the fact that the slot has been accessed for the first time. A similar mechanism is used for accounts: whenever an account is accessed for the first time, a certain amount of gas is charged for reading the account's data. On EVM, an account's data includes its nonce, balance, and code. We use a similar mechanism but with a few differences.
Storage costs
Just like EVM, we also support "warm" and "cold" storage slots. However, the flow is a bit different:
- The user is firstly precharged with the maximum (cold) cost.
- The operator is asked for a refund.
- Then, the refund is given out to the user in place.
In other words, unlike EVM, the user should always have enough gas for the worst case (even if the storage slot is "warm"). Also, the control of the refunds is currently enforced by the operator only and not by the circuits.
Code decommitment and account access costs
Unlike EVM, our storage does not couple accounts' balances, nonces, and bytecodes. Balance, nonce, and code hash are three separate storage variables that use standard storage "warm" and "cold" mechanisms. A different approach is used for accessing bytecodes though.
We call the process of unpacking the bytecode as, code decommitment, since it is a process of transforming a commitment to code (i.e., the versioned code hash) into its preimage. Whenever a contract with a certain code hash is called, the following logic is executed:
- The operator is asked whether this is the first time this bytecode has been decommitted.
- If the operator returns "yes", then the user is charged the full cost. Otherwise, the user does not pay for decommit.
- If needed, the code is decommitted to the code page.
Unlike storage interactions, the correctness of this process is partially enforced by circuits, i.e., if step (3) is reached, i.e., the code is being decommitted, it will be proven that the operator responded correctly on step (1). However, if the program runs out of gas on step (2), the correctness of the first statement won't be proven. The reason for that is it is hard to prove in circuits at the time the decommitment is invoked whether it is indeed the first decommitment or not.
Note that in the case of an honest operator, this approach offers a better UX, since there is no need to be precharged with the full cost beforehand. However, no program should rely on this fact.
Conclusion
As a conclusion, ZKsync Era supports a similar "cold"/"warm" mechanism to EVM, but for now, these are only enforced by the operator, i.e., the users of the applications should not rely on these. The execution is guaranteed to be correct as long as the user has enough gas to pay for the worst, i.e. "cold" scenario.
Memory pricing
ZKsync Era has different memory pricing rules:
- Whenever a user contract is called,
2^12
bytes of memory are given out for free, before starting to charge users linearly according to its length. - Whenever a kernel space (i.e., a system) contract is called,
2^21
bytes of memory are given out for free, before starting to charge users linearly according to the length.
Note that, unlike EVM, we never use a quadratic component of the price for memory expansion.
Different intrinsic costs
Unlike Ethereum, where the intrinsic cost of transactions (21000
gas) is used to cover the price of updating the balances of the users,
the nonce and signature verification, on ZKsync these prices are not included in the intrinsic costs for transactions,
due to the native support of account abstraction, meaning that each account type may have their own transaction cost.
In theory, some may even use more zk-friendly signature schemes or other kinds of optimizations to allow cheaper transactions for their users.
That being said, ZKsync transactions do come with some small intrinsic costs, but they are mostly used to cover costs related to the processing of the transaction by the bootloader which can not be easily measured in code in real-time. These are measured via testing and are hard coded.
Charging for pubdata
An important cost factor for users is the pubdata. ZKsync Era is a state diff-based rollup, meaning that the pubdata is published not for the transaction data, but for the state changes: modified storage slots, deployed bytecodes, L2->L1 messages. This allows for applications that modify the same storage slot multiple times such as oracles, to update the storage slots multiple times while maintaining a constant footprint on L1 pubdata. Correctly a state diff rollups requires a special solution to charging for pubdata. It is explored in the next section.