L1 Contracts

Useful Addresses

DiamondInit

It is a one-function contract that implements the logic of initializing a diamond proxy. It is called only once on the diamond constructor and is not saved in the diamond as a facet.

Implementation detail - function returns a magic value just like it is designed in EIP-1271, but the magic value is 32 bytes in size.

DiamondProxy

This contract uses the EIP-2535 diamond proxy pattern.

It is an in-house implementation that is inspired by the mudgen reference implementation. It has no external functions, only the fallback that delegates a call to one of the facets (target/implementation contract).

So even an upgrade system is a separate facet that can be replaced.

One of the differences from the reference implementation is the ability to freeze access to the facet.

Each of the facets has an associated parameter that indicates if it is possible to freeze access to the facet.

Privileged actors can freeze the diamond (not a specific facet!) and all facets with the marker isFreezable should be inaccessible until the governor unfreezes the diamond.

Diamond

Technically, this L1 smart contract acts as a connector between Ethereum (L1) and ZKsync (L2). This contract checks the validity proof and data availability, handles L2 <-> L1 communication, finalizes L2 state transition, and more.

There are also important contracts deployed on the L2 that can also execute logic that we refer to as System Contracts. Using L2 <-> L1 communication can affect both the L1 and the L2.

ExecutorFacet

A contract that accepts L2 blocks, enforces data availability and checks the validity of zk-proofs.

The state transition is divided into three stages:

  • commitBlocks - check L2 block timestamp, process the L2 logs, save data for a block, and prepare data for zk-proof.
  • proveBlocks - validate zk-proof.
  • executeBlocks - finalize the state, marking L1 -> L2 communication processing, and saving Merkle tree with L2 logs.

When a block is committed, we process L2 -> L1 logs. Here are the invariants that are expected there:

  • The only one L2 -> L1 log from the L2_SYSTEM_CONTEXT_ADDRESS, with the key == l2BlockTimestamp and value == l2BlockHash.
  • Several (or none) logs from the L2_KNOWN_CODE_STORAGE_ADDRESS with the key == bytecodeHash, where bytecode is marked as a known factory dependency.
  • Several (or none) logs from the L2_BOOTLOADER_ADDRESS with the key == canonicalTxHash where canonicalTxHash is a hash of processed L1 -> L2 transaction.
  • Several (of none) logs from the L2_TO_L1_MESSENGER with the key == hashedMessage where hashedMessage is a hash of an arbitrary-length message that is sent from L2.
  • Several (or none) logs from other addresses with arbitrary parameters.

GettersFacet

Separate facet, whose only function is providing view and pure methods. It also implements diamond loupe which makes managing facets easier.

MailboxFacet

The facet that handles L2 <-> L1 communication, an overview for which can be found in the L1 / L2 Interoperability guide.

The Mailbox only cares about transferring information from L2 to L1 and the other way but does not hold or transfer any assets (ETH, ERC20 tokens, or NFTs).

L1 -> L2 communication is implemented as requesting an L2 transaction on L1 and executing it on L2. This means a user can call the function on the L1 contract to save the data about the transaction in some queue. Later on, a validator can process such transactions on L2 and mark them as processed on the L1 priority queue.

Currently, it is used only for sending information from L1 to L2 or implementing a multi-layer protocol, but it is planned to use a priority queue for the censor-resistance mechanism. Relevant functions for L1 -> L2 communication: requestL2Transaction/l2TransactionBaseCost/serializeL2Transaction.

NOTE: For each executed transaction L1 -> L2, the system program necessarily sends an L2 -> L1 log.

The semantics of such L2 -> L1 log are always:

  • sender = BOOTLOADER_ADDRESS.
  • key = hash(L1ToL2Transaction).
  • value = status of the processing transaction (1 - success & 0 for fail).
  • isService = true (just a conventional value).
  • l2ShardId = 0 (means that L1 -> L2 transaction was processed in a rollup shard, other shards are not available yet anyway).
  • txNumberInBlock = number of transactions in the block.

L2 -> L1 communication, in contrast to L1 -> L2 communication, is based only on transferring the information, and not on the transaction execution on L1.

From the L2 side, there is a special zkEVM opcode that saves l2ToL1Log in the L2 block. A validator will send all l2ToL1Logs when sending an L2 block to the L1 (see ExecutorFacet). Later on, users will be able to both read their l2ToL1logs on L1 and prove that they sent it.

From the L1 side, for each L2 block, a Merkle root with such logs in leaves is calculated. Thus, a user can provide Merkle proof for each l2ToL1Logs.

NOTE: The l2ToL1Log structure consists of fixed-size fields! Because of this, it is inconvenient to send a lot of data from L2 and to prove that they were sent on L1 using only l2ToL1log. To send a variable-length message we use this trick:

  • One of the system contracts accepts an arbitrary-length message and sends a fixed-length message with parameters senderAddress == this, marker == true, key == msg.sender, value == keccak256(message).
  • The contract on L1 accepts all sent messages and if the message came from this system contract it requires that the preimage of value be provided.

ValidatorTimelock

An intermediate smart contract between the validator EOA account and the ZKsync smart contract. Its primary purpose is to provide a trustless means of delaying batch execution without modifying the main ZKsync contract. ZKsync actively monitors the chain activity and reacts to any suspicious activity by freezing the chain. This allows time for investigation and mitigation before resuming normal operations.

It is a temporary solution to prevent any significant impact of the validator hot key leakage, while the network is in the Alpha stage.

This contract consists of four main functions commitBatches, proveBatches, executeBatches, and revertBatches, which can be called only by the validator.

When the validator calls commitBatches, the same calldata will be propagated to the ZKsync contract (DiamondProxy through call where it invokes the ExecutorFacet through delegatecall), and also a timestamp is assigned to these batches to track the time these batches are committed by the validator to enforce a delay between committing and execution of batches. Then, the validator can prove the already committed batches regardless of the mentioned timestamp, and again the same calldata (related to the proveBatches function) will be propagated to the ZKsync contract. After the delay is elapsed, the validator is allowed to call executeBatches to propagate the same calldata to ZKsync contract.

The owner of the ValidatorTimelock contract is the same as the owner of the Governance contract - Matter Labs multisig.

Allowlist

The auxiliary contract controls the permission access list. It is used in bridges and diamond proxies to control which addresses can interact with them in the Alpha release. Currently, it is supposed to set all permissions to public.

The owner of the Allowlist contract is the Governance contract.


Made with ❤️ by the ZKsync Community