Skip to content

Commit

Permalink
Merge pull request #8 from matter-labs/vb-opensoursing
Browse files Browse the repository at this point in the history
Prepare the repo for open sourcing
  • Loading branch information
shahar4 authored Jan 25, 2023
2 parents 056aca4 + c1814c5 commit a4fafa8
Show file tree
Hide file tree
Showing 6 changed files with 238 additions and 215 deletions.
221 changes: 8 additions & 213 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,220 +1,15 @@
# zkSync 2.0 Smart Contracts
# zkSync 2.0: Smart Contracts

## Overview
zkSync 2.0 is a ZK rollup, a trustless protocol that uses cryptographic validity proofs to provide scalable and low-cost transactions on Ethereum. As all transactions are proven on the Ethereum mainchain, users enjoy the same security level as in Ethereum.

zkSync 2.0 is a permissionless general-purpose ZK rollup. Similar to many L1 blockchains and sidechains it enables
deployment and interaction with Turing-complete smart contracts.
This repository contains both L1 and L2 zkSync smart contracts. For their description see the [system overview](docs/Overview.md).

- L2 smart contracts are executed on a zkEVM.
- zkEVM bytecode is different from the L1 EVM.
- There is a Solidity and Vyper compilers for L2 smart contracts.
- There is a standard way to pass messages between L1 and L2. That is a part of the protocol.
- There is no escape hatch mechanism yet, but there will be one.
## Disclaimer

All data that is needed to restore the L2 state are also pushed on-chain. There are two approaches, publishing inputs of
L2 transactions on-chain and publishing the state transition diff. zkSync follows the second option.
It is used as a submodule of a private repo. Compilation and test scripts should work without additional tooling, but others may not.

See the [documentation](https://v2-docs.zksync.io/dev/fundamentals/rollups.html) to read more!
## License

## Glossary
zkSync 2.0 contracts are distributed under the terms of the MIT license.

- **Governor** - privileged address that controls the upgradability of the network and sets other privileged addresses.
- **Validator/Operator** - a privileged address that can commit/verify/execute L2 blocks.
- **Facet** - implementation contract. The word comes from the EIP-2535.
- **Security council** - set of trusted addresses that can decrease upgrade timelock.
- **Ergs** - a unit that measures the amount of computational effort required to execute specific operations on the
zkSync v2 network. Analog of the gas on Ethereum.

### L1 Smart contracts

#### 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 called _system contracts_. Using L2
<-> L1 communication, they can affect both the L1 and the L2.

#### DiamondProxy

The main contract uses [EIP-2535](https://eips.ethereum.org/EIPS/eip-2535) diamond proxy pattern. It is an in-house
implementation that is inspired by the [mudgen reference implementation](https://github.com/mudgen/Diamond). 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 access freezability. 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. Note that it is a very dangerous thing since the diamond proxy can freeze the upgrade system and then the
diamond will be frozen forever.

#### 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](https://eips.ethereum.org/EIPS/eip-1271), but the magic value is 32 bytes in size.

#### DiamondCutFacet

These smart contracts manage the freezing/unfreezing and upgrades of the diamond proxy. That being said, the contract
must never be frozen.

Currently, freezing and unfreezing are implemented as access control functions. It is fully controlled by the governor
but can be changed later. The governor can call `emergencyFreezeDiamond` to freeze the diamond and `unfreezeDiamond` to
restore it.

Another purpose of `DiamondCutFacet` is to upgrade the facets. The upgrading is split into 2-3 phases:

- `proposeDiamondCut` - propose an upgrade by the governor.
- `approveEmergencyDiamondCutAsSecurityCouncilMember` - approve the upgrade by the security council.
- `executeDiamondCutProposal` - finalize the upgrade.

The upgrade itself characterizes by three variables:

- `facetCuts` - a set of changes to the facets (adding new facets, removing facets, and replacing them).
- pair `(address _initAddress, bytes _calldata)` for initializing the upgrade by making a delegate call to
`_initAddress` with `_calldata` inputs.

NOTE: `proposeDiamondCut` - commits data associated with an upgrade but does not execute it. While the upgrade is
associated with `facetCuts` and `(address _initAddress, bytes _calldata)` the upgrade will be committed to the
`facetCuts` and `_initAddress`. This is done on purpose, to leave some freedom to the governor to change calldata for
the upgrade between proposing and executing it.

#### GettersFacet

Separate facet, whose only function is providing `view` and `pure` methods. It also implements
[diamond loupe](https://eips.ethereum.org/EIPS/eip-2535#diamond-loupe) which makes managing facets easier.

#### GovernanceFacet

Controls changing the privileged addresses such as governor and validators or one of the system parameters (L2
bootloader bytecode hash, verifier address, verifier parameters, etc).

#### MailboxFacet

The facet that handles L2 <-> L1 communication, an overview for which can be found in
[docs](https://v2-docs.zksync.io/dev/developer-guides/bridging/l1-l2-interop.html).

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.

#### 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.

#### Bridges

Bridges are completely separate contracts from the Diamond. They are a wrapper for L1 <-> L2 communication on contracts
on both L1 and L2. Upon locking assets on one layer, a request is sent to mint these bridged assets on the other layer.
Upon burning assets on one layer, a request is sent to unlock them on the other.

The Ether bridge is special because it is the only place where native L2 Ether can be minted. Other than that, it is
just a smart contract without any special system preferences.

##### L1Bridge

- `deposit` - lock funds inside the contract and send a request to mint bridged assets on L2.
- `claimFailedDeposit` - unlock funds if the deposit was initiated but then failed on L2.
- `finalizeWithdrawal` - unlock funds for the valid withdrawal request from L2.

##### L2Bridge

- `withdraw` - initiate a withdrawal by burning funds on the contract and sending a corresponding message to L1.
- `finalizeDeposit` - finalize the deposit and mint funds on L2.

#### 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.

### L2 specifics

#### Deployment

The L2 deployment process is different from Ethereum.

In L1, the deployment always goes through two opcodes `create` and `create2`, each of which provides its address
derivation. The parameter of these opcodes is the so-called "init bytecode" - a bytecode that returns the bytecode to be
deployed. This works well in L1 but is suboptimal for L2.

In the case of L2, there are also two ways to deploy contracts - `create` and `create2`. However, the expected input
parameters for `create` and `create2` are different. It accepts the hash of the bytecode, rather than the full bytecode.
Therefore, users pay less for contract creation and don't need to send the full contract code by the network upon
deploys.

A good question could be, _how does the validator know the preimage of the bytecode hashes to execute the code?_ Here
comes the concept of factory dependencies! Factory dependencies are a list of bytecode hashes whose preimages were shown
on L1 (data is always available). Such bytecode hashes can be deployed, others - no. Note that they can be added to the
system by either L2 transaction or L1 -> L2 communication, where you can specify the full bytecode and the system will
mark it as known and allow you to deploy it.

Besides that, due to the bytecode differences for L1 and L2 contracts, address derivation is different. This applies to
both `create` and `create2` and means that contracts deployed on the L1 cannot have a collision with contracts deployed
on the L2. Please note that EOA address derivation is the same as on Ethereum.

Thus:

- L2 contracts are deployed by bytecode hash, not by full bytecode
- Factory dependencies - list of bytecode hashes that can be deployed on L2
- Address derivation for `create`/`create2` on L1 and L2 is different

See the [documentation](https://v2-docs.zksync.io/dev/developer-guides/contracts/contracts.html#solidity-vyper-support)
to read more!
See [LICENSE-MIT](LICENSE-MIT) for details.
Loading

0 comments on commit a4fafa8

Please sign in to comment.