diff --git a/contracts/l1-contracts/bridge/L1ERC20Bridge.sol b/contracts/l1-contracts/bridge/L1ERC20Bridge.sol new file mode 100644 index 0000000..ccd4a2a --- /dev/null +++ b/contracts/l1-contracts/bridge/L1ERC20Bridge.sol @@ -0,0 +1,258 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {IERC20} from "@openzeppelin/contracts-v4/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts-v4/token/ERC20/utils/SafeERC20.sol"; + +import {IL1ERC20Bridge} from "./interfaces/IL1ERC20Bridge.sol"; +import {IL1SharedBridge} from "./interfaces/IL1SharedBridge.sol"; + +import {L2ContractHelper} from "../common/libraries/L2ContractHelper.sol"; +import {ReentrancyGuard} from "../common/ReentrancyGuard.sol"; + +import {Unauthorized, EmptyDeposit, TokensWithFeesNotSupported, WithdrawalAlreadyFinalized} from "../common/L1ContractErrors.sol"; + +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +/// @notice Smart contract that allows depositing ERC20 tokens from Ethereum to hyperchains +/// @dev It is a legacy bridge from ZKsync Era, that was deprecated in favour of shared bridge. +/// It is needed for backward compatibility with already integrated projects. +contract L1ERC20Bridge is IL1ERC20Bridge, ReentrancyGuard { + using SafeERC20 for IERC20; + + /// @dev The shared bridge that is now used for all bridging, replacing the legacy contract. + IL1SharedBridge public immutable override SHARED_BRIDGE; + + /// @dev A mapping L2 batch number => message number => flag. + /// @dev Used to indicate that L2 -> L1 message was already processed for ZKsync Era withdrawals. + // slither-disable-next-line uninitialized-state + mapping(uint256 l2BatchNumber => mapping(uint256 l2ToL1MessageNumber => bool isFinalized)) + public isWithdrawalFinalized; + + /// @dev A mapping account => L1 token address => L2 deposit transaction hash => amount. + /// @dev Used for saving the number of deposited funds, to claim them in case the deposit transaction will fail in ZKsync Era. + mapping(address account => mapping(address l1Token => mapping(bytes32 depositL2TxHash => uint256 amount))) + public depositAmount; + + /// @dev The address that is used as a L2 bridge counterpart in ZKsync Era. + // slither-disable-next-line uninitialized-state + address public l2Bridge; + + /// @dev The address that is used as a beacon for L2 tokens in ZKsync Era. + // slither-disable-next-line uninitialized-state + address public l2TokenBeacon; + + /// @dev Stores the hash of the L2 token proxy contract's bytecode on ZKsync Era. + // slither-disable-next-line uninitialized-state + bytes32 public l2TokenProxyBytecodeHash; + + /// @dev Deprecated storage variable related to withdrawal limitations. + mapping(address => uint256) private __DEPRECATED_lastWithdrawalLimitReset; + + /// @dev Deprecated storage variable related to withdrawal limitations. + mapping(address => uint256) private __DEPRECATED_withdrawnAmountInWindow; + + /// @dev Deprecated storage variable related to deposit limitations. + mapping(address => mapping(address => uint256)) private __DEPRECATED_totalDepositedAmountPerUser; + + /// @dev Contract is expected to be used as proxy implementation. + /// @dev Initialize the implementation to prevent Parity hack. + constructor(IL1SharedBridge _sharedBridge) reentrancyGuardInitializer { + SHARED_BRIDGE = _sharedBridge; + } + + /// @dev Initializes the reentrancy guard. Expected to be used in the proxy. + function initialize() external reentrancyGuardInitializer {} + + /// @dev transfer token to shared bridge as part of upgrade + function transferTokenToSharedBridge(address _token) external { + if (msg.sender != address(SHARED_BRIDGE)) { + revert Unauthorized(msg.sender); + } + uint256 amount = IERC20(_token).balanceOf(address(this)); + IERC20(_token).safeTransfer(address(SHARED_BRIDGE), amount); + } + + /*////////////////////////////////////////////////////////////// + ERA LEGACY GETTERS + //////////////////////////////////////////////////////////////*/ + + /// @return The L2 token address that would be minted for deposit of the given L1 token on ZKsync Era. + function l2TokenAddress(address _l1Token) external view returns (address) { + bytes32 constructorInputHash = keccak256(abi.encode(l2TokenBeacon, "")); + bytes32 salt = bytes32(uint256(uint160(_l1Token))); + + return L2ContractHelper.computeCreate2Address(l2Bridge, salt, l2TokenProxyBytecodeHash, constructorInputHash); + } + + /*////////////////////////////////////////////////////////////// + ERA LEGACY FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /// @notice Legacy deposit method with refunding the fee to the caller, use another `deposit` method instead. + /// @dev Initiates a deposit by locking funds on the contract and sending the request + /// of processing an L2 transaction where tokens would be minted. + /// @dev If the token is bridged for the first time, the L2 token contract will be deployed. Note however, that the + /// newly-deployed token does not support any custom logic, i.e. rebase tokens' functionality is not supported. + /// @param _l2Receiver The account address that should receive funds on L2 + /// @param _l1Token The L1 token address which is deposited + /// @param _amount The total amount of tokens to be bridged + /// @param _l2TxGasLimit The L2 gas limit to be used in the corresponding L2 transaction + /// @param _l2TxGasPerPubdataByte The gasPerPubdataByteLimit to be used in the corresponding L2 transaction + /// @return l2TxHash The L2 transaction hash of deposit finalization + /// NOTE: the function doesn't use `nonreentrant` modifier, because the inner method does. + function deposit( + address _l2Receiver, + address _l1Token, + uint256 _amount, + uint256 _l2TxGasLimit, + uint256 _l2TxGasPerPubdataByte + ) external payable returns (bytes32 l2TxHash) { + l2TxHash = deposit({ + _l2Receiver: _l2Receiver, + _l1Token: _l1Token, + _amount: _amount, + _l2TxGasLimit: _l2TxGasLimit, + _l2TxGasPerPubdataByte: _l2TxGasPerPubdataByte, + _refundRecipient: address(0) + }); + } + + /// @notice Initiates a deposit by locking funds on the contract and sending the request + /// @dev Initiates a deposit by locking funds on the contract and sending the request + /// of processing an L2 transaction where tokens would be minted + /// @dev If the token is bridged for the first time, the L2 token contract will be deployed. Note however, that the + /// newly-deployed token does not support any custom logic, i.e. rebase tokens' functionality is not supported. + /// @param _l2Receiver The account address that should receive funds on L2 + /// @param _l1Token The L1 token address which is deposited + /// @param _amount The total amount of tokens to be bridged + /// @param _l2TxGasLimit The L2 gas limit to be used in the corresponding L2 transaction + /// @param _l2TxGasPerPubdataByte The gasPerPubdataByteLimit to be used in the corresponding L2 transaction + /// @param _refundRecipient The address on L2 that will receive the refund for the transaction. + /// @dev If the L2 deposit finalization transaction fails, the `_refundRecipient` will receive the `_l2Value`. + /// Please note, the contract may change the refund recipient's address to eliminate sending funds to addresses + /// out of control. + /// - If `_refundRecipient` is a contract on L1, the refund will be sent to the aliased `_refundRecipient`. + /// - If `_refundRecipient` is set to `address(0)` and the sender has NO deployed bytecode on L1, the refund will + /// be sent to the `msg.sender` address. + /// - If `_refundRecipient` is set to `address(0)` and the sender has deployed bytecode on L1, the refund will be + /// sent to the aliased `msg.sender` address. + /// @dev The address aliasing of L1 contracts as refund recipient on L2 is necessary to guarantee that the funds + /// are controllable through the Mailbox, since the Mailbox applies address aliasing to the from address for the + /// L2 tx if the L1 msg.sender is a contract. Without address aliasing for L1 contracts as refund recipients they + /// would not be able to make proper L2 tx requests through the Mailbox to use or withdraw the funds from L2, and + /// the funds would be lost. + /// @return l2TxHash The L2 transaction hash of deposit finalization + function deposit( + address _l2Receiver, + address _l1Token, + uint256 _amount, + uint256 _l2TxGasLimit, + uint256 _l2TxGasPerPubdataByte, + address _refundRecipient + ) public payable nonReentrant returns (bytes32 l2TxHash) { + // empty deposit + if (_amount == 0) { + revert EmptyDeposit(); + } + uint256 amount = _depositFundsToSharedBridge(msg.sender, IERC20(_l1Token), _amount); + // The token has non-standard transfer logic + if (amount != _amount) { + revert TokensWithFeesNotSupported(); + } + + l2TxHash = SHARED_BRIDGE.depositLegacyErc20Bridge{value: msg.value}({ + _msgSender: msg.sender, + _l2Receiver: _l2Receiver, + _l1Token: _l1Token, + _amount: _amount, + _l2TxGasLimit: _l2TxGasLimit, + _l2TxGasPerPubdataByte: _l2TxGasPerPubdataByte, + _refundRecipient: _refundRecipient + }); + depositAmount[msg.sender][_l1Token][l2TxHash] = _amount; + emit DepositInitiated({ + l2DepositTxHash: l2TxHash, + from: msg.sender, + to: _l2Receiver, + l1Token: _l1Token, + amount: _amount + }); + } + + /// @dev Transfers tokens from the depositor address to the shared bridge address. + /// @return The difference between the contract balance before and after the transferring of funds. + function _depositFundsToSharedBridge(address _from, IERC20 _token, uint256 _amount) internal returns (uint256) { + uint256 balanceBefore = _token.balanceOf(address(SHARED_BRIDGE)); + _token.safeTransferFrom(_from, address(SHARED_BRIDGE), _amount); + uint256 balanceAfter = _token.balanceOf(address(SHARED_BRIDGE)); + + return balanceAfter - balanceBefore; + } + + /// @dev Withdraw funds from the initiated deposit, that failed when finalizing on L2. + /// @param _depositSender The address of the deposit initiator + /// @param _l1Token The address of the deposited L1 ERC20 token + /// @param _l2TxHash The L2 transaction hash of the failed deposit finalization + /// @param _l2BatchNumber The L2 batch number where the deposit finalization was processed + /// @param _l2MessageIndex The position in the L2 logs Merkle tree of the l2Log that was sent with the message + /// @param _l2TxNumberInBatch The L2 transaction number in a batch, in which the log was sent + /// @param _merkleProof The Merkle proof of the processing L1 -> L2 transaction with deposit finalization + function claimFailedDeposit( + address _depositSender, + address _l1Token, + bytes32 _l2TxHash, + uint256 _l2BatchNumber, + uint256 _l2MessageIndex, + uint16 _l2TxNumberInBatch, + bytes32[] calldata _merkleProof + ) external nonReentrant { + uint256 amount = depositAmount[_depositSender][_l1Token][_l2TxHash]; + // empty deposit + if (amount == 0) { + revert EmptyDeposit(); + } + delete depositAmount[_depositSender][_l1Token][_l2TxHash]; + + SHARED_BRIDGE.claimFailedDepositLegacyErc20Bridge({ + _depositSender: _depositSender, + _l1Token: _l1Token, + _amount: amount, + _l2TxHash: _l2TxHash, + _l2BatchNumber: _l2BatchNumber, + _l2MessageIndex: _l2MessageIndex, + _l2TxNumberInBatch: _l2TxNumberInBatch, + _merkleProof: _merkleProof + }); + emit ClaimedFailedDeposit(_depositSender, _l1Token, amount); + } + + /// @notice Finalize the withdrawal and release funds + /// @param _l2BatchNumber The L2 batch number where the withdrawal was processed + /// @param _l2MessageIndex The position in the L2 logs Merkle tree of the l2Log that was sent with the message + /// @param _l2TxNumberInBatch The L2 transaction number in the batch, in which the log was sent + /// @param _message The L2 withdraw data, stored in an L2 -> L1 message + /// @param _merkleProof The Merkle proof of the inclusion L2 -> L1 message about withdrawal initialization + function finalizeWithdrawal( + uint256 _l2BatchNumber, + uint256 _l2MessageIndex, + uint16 _l2TxNumberInBatch, + bytes calldata _message, + bytes32[] calldata _merkleProof + ) external nonReentrant { + if (isWithdrawalFinalized[_l2BatchNumber][_l2MessageIndex]) { + revert WithdrawalAlreadyFinalized(); + } + // We don't need to set finalizeWithdrawal here, as we set it in the shared bridge + + (address l1Receiver, address l1Token, uint256 amount) = SHARED_BRIDGE.finalizeWithdrawalLegacyErc20Bridge({ + _l2BatchNumber: _l2BatchNumber, + _l2MessageIndex: _l2MessageIndex, + _l2TxNumberInBatch: _l2TxNumberInBatch, + _message: _message, + _merkleProof: _merkleProof + }); + emit WithdrawalFinalized(l1Receiver, l1Token, amount); + } +} diff --git a/contracts/l1-contracts/bridge/L1SharedBridge.sol b/contracts/l1-contracts/bridge/L1SharedBridge.sol new file mode 100644 index 0000000..eec3c2e --- /dev/null +++ b/contracts/l1-contracts/bridge/L1SharedBridge.sol @@ -0,0 +1,918 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable-v4/access/Ownable2StepUpgradeable.sol"; +import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable-v4/security/PausableUpgradeable.sol"; + +import {IERC20Metadata} from "@openzeppelin/contracts-v4/token/ERC20/extensions/IERC20Metadata.sol"; +import {IERC20} from "@openzeppelin/contracts-v4/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts-v4/token/ERC20/utils/SafeERC20.sol"; + +import {IL1ERC20Bridge} from "./interfaces/IL1ERC20Bridge.sol"; +import {IL1SharedBridge} from "./interfaces/IL1SharedBridge.sol"; +import {IL2Bridge} from "./interfaces/IL2Bridge.sol"; + +import {IMailbox} from "../state-transition/chain-interfaces/IMailbox.sol"; +import {L2Message, TxStatus} from "../common/Messaging.sol"; +import {UnsafeBytes} from "../common/libraries/UnsafeBytes.sol"; +import {ReentrancyGuard} from "../common/ReentrancyGuard.sol"; +import {AddressAliasHelper} from "../vendor/AddressAliasHelper.sol"; +import {ETH_TOKEN_ADDRESS, TWO_BRIDGES_MAGIC_VALUE} from "../common/Config.sol"; +import {IBridgehub, L2TransactionRequestTwoBridgesInner, L2TransactionRequestDirect} from "../bridgehub/IBridgehub.sol"; +import {IGetters} from "../state-transition/chain-interfaces/IGetters.sol"; +import {L2_BASE_TOKEN_SYSTEM_CONTRACT_ADDR} from "../common/L2ContractAddresses.sol"; +import {Unauthorized, ZeroAddress, SharedBridgeValueAlreadySet, SharedBridgeKey, NoFundsTransferred, ZeroBalance, ValueMismatch, TokensWithFeesNotSupported, NonEmptyMsgValue, L2BridgeNotSet, TokenNotSupported, DepositIncorrectAmount, EmptyDeposit, DepositExists, AddressAlreadyUsed, InvalidProof, DepositDoesNotExist, InsufficientChainBalance, SharedBridgeValueNotSet, WithdrawalAlreadyFinalized, WithdrawFailed, L2WithdrawalMessageWrongLength, InvalidSelector, SharedBridgeBalanceMismatch, SharedBridgeValueNotSet} from "../common/L1ContractErrors.sol"; + +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +/// @dev Bridges assets between L1 and hyperchains, supporting both ETH and ERC20 tokens. +/// @dev Designed for use with a proxy for upgradability. +contract L1SharedBridge is IL1SharedBridge, ReentrancyGuard, Ownable2StepUpgradeable, PausableUpgradeable { + using SafeERC20 for IERC20; + + /// @dev The address of the WETH token on L1. + address public immutable override L1_WETH_TOKEN; + + /// @dev Bridgehub smart contract that is used to operate with L2 via asynchronous L2 <-> L1 communication. + IBridgehub public immutable override BRIDGE_HUB; + + /// @dev Era's chainID + uint256 internal immutable ERA_CHAIN_ID; + + /// @dev The address of ZKsync Era diamond proxy contract. + address internal immutable ERA_DIAMOND_PROXY; + + /// @dev Stores the first batch number on the ZKsync Era Diamond Proxy that was settled after Diamond proxy upgrade. + /// This variable is used to differentiate between pre-upgrade and post-upgrade Eth withdrawals. Withdrawals from batches older + /// than this value are considered to have been finalized prior to the upgrade and handled separately. + uint256 internal eraPostDiamondUpgradeFirstBatch; + + /// @dev Stores the first batch number on the ZKsync Era Diamond Proxy that was settled after L1ERC20 Bridge upgrade. + /// This variable is used to differentiate between pre-upgrade and post-upgrade ERC20 withdrawals. Withdrawals from batches older + /// than this value are considered to have been finalized prior to the upgrade and handled separately. + uint256 internal eraPostLegacyBridgeUpgradeFirstBatch; + + /// @dev Stores the ZKsync Era batch number that processes the last deposit tx initiated by the legacy bridge + /// This variable (together with eraLegacyBridgeLastDepositTxNumber) is used to differentiate between pre-upgrade and post-upgrade deposits. Deposits processed in older batches + /// than this value are considered to have been processed prior to the upgrade and handled separately. + /// We use this both for Eth and erc20 token deposits, so we need to update the diamond and bridge simultaneously. + uint256 internal eraLegacyBridgeLastDepositBatch; + + /// @dev The tx number in the _eraLegacyBridgeLastDepositBatch of the last deposit tx initiated by the legacy bridge + /// This variable (together with eraLegacyBridgeLastDepositBatch) is used to differentiate between pre-upgrade and post-upgrade deposits. Deposits processed in older txs + /// than this value are considered to have been processed prior to the upgrade and handled separately. + /// We use this both for Eth and erc20 token deposits, so we need to update the diamond and bridge simultaneously. + uint256 internal eraLegacyBridgeLastDepositTxNumber; + + /// @dev Legacy bridge smart contract that used to hold ERC20 tokens. + IL1ERC20Bridge public override legacyBridge; + + /// @dev A mapping chainId => bridgeProxy. Used to store the bridge proxy's address, and to see if it has been deployed yet. + mapping(uint256 chainId => address l2Bridge) public override l2BridgeAddress; + + /// @dev A mapping chainId => L2 deposit transaction hash => keccak256(abi.encode(account, tokenAddress, amount)) + /// @dev Tracks deposit transactions from L2 to enable users to claim their funds if a deposit fails. + mapping(uint256 chainId => mapping(bytes32 l2DepositTxHash => bytes32 depositDataHash)) + public + override depositHappened; + + /// @dev Tracks the processing status of L2 to L1 messages, indicating whether a message has already been finalized. + mapping(uint256 chainId => mapping(uint256 l2BatchNumber => mapping(uint256 l2ToL1MessageNumber => bool isFinalized))) + public isWithdrawalFinalized; + + /// @dev Indicates whether the hyperbridging is enabled for a given chain. + // slither-disable-next-line uninitialized-state + mapping(uint256 chainId => bool enabled) internal hyperbridgingEnabled; + + /// @dev Maps token balances for each chain to prevent unauthorized spending across hyperchains. + /// This serves as a security measure until hyperbridging is implemented. + /// NOTE: this function may be removed in the future, don't rely on it! + mapping(uint256 chainId => mapping(address l1Token => uint256 balance)) public chainBalance; + + /// @notice Checks that the message sender is the bridgehub. + modifier onlyBridgehub() { + if (msg.sender != address(BRIDGE_HUB)) { + revert Unauthorized(msg.sender); + } + _; + } + + /// @notice Checks that the message sender is the bridgehub or ZKsync Era Diamond Proxy. + modifier onlyBridgehubOrEra(uint256 _chainId) { + if (msg.sender != address(BRIDGE_HUB) && (_chainId != ERA_CHAIN_ID || msg.sender != ERA_DIAMOND_PROXY)) { + revert Unauthorized(msg.sender); + } + _; + } + + /// @notice Checks that the message sender is the legacy bridge. + modifier onlyLegacyBridge() { + if (msg.sender != address(legacyBridge)) { + revert Unauthorized(msg.sender); + } + _; + } + + /// @notice Checks that the message sender is the shared bridge itself. + modifier onlySelf() { + if (msg.sender != address(this)) { + revert Unauthorized(msg.sender); + } + _; + } + + /// @dev Contract is expected to be used as proxy implementation. + /// @dev Initialize the implementation to prevent Parity hack. + constructor( + address _l1WethAddress, + IBridgehub _bridgehub, + uint256 _eraChainId, + address _eraDiamondProxy + ) reentrancyGuardInitializer { + _disableInitializers(); + L1_WETH_TOKEN = _l1WethAddress; + BRIDGE_HUB = _bridgehub; + ERA_CHAIN_ID = _eraChainId; + ERA_DIAMOND_PROXY = _eraDiamondProxy; + } + + /// @dev Initializes a contract bridge for later use. Expected to be used in the proxy + /// @param _owner Address which can change L2 token implementation and upgrade the bridge + /// implementation. The owner is the Governor and separate from the ProxyAdmin from now on, so that the Governor can call the bridge. + function initialize(address _owner) external reentrancyGuardInitializer initializer { + if (_owner == address(0)) { + revert ZeroAddress(); + } + _transferOwnership(_owner); + } + + /// @dev This sets the first post diamond upgrade batch for era, used to check old eth withdrawals + /// @param _eraPostDiamondUpgradeFirstBatch The first batch number on the ZKsync Era Diamond Proxy that was settled after diamond proxy upgrade. + function setEraPostDiamondUpgradeFirstBatch(uint256 _eraPostDiamondUpgradeFirstBatch) external onlyOwner { + if (eraPostDiamondUpgradeFirstBatch != 0) { + revert SharedBridgeValueAlreadySet(SharedBridgeKey.PostUpgradeFirstBatch); + } + eraPostDiamondUpgradeFirstBatch = _eraPostDiamondUpgradeFirstBatch; + } + + /// @dev This sets the first post upgrade batch for era, used to check old token withdrawals + /// @param _eraPostLegacyBridgeUpgradeFirstBatch The first batch number on the ZKsync Era Diamond Proxy that was settled after legacy bridge upgrade. + function setEraPostLegacyBridgeUpgradeFirstBatch(uint256 _eraPostLegacyBridgeUpgradeFirstBatch) external onlyOwner { + if (eraPostLegacyBridgeUpgradeFirstBatch != 0) { + revert SharedBridgeValueAlreadySet(SharedBridgeKey.LegacyBridgeFirstBatch); + } + eraPostLegacyBridgeUpgradeFirstBatch = _eraPostLegacyBridgeUpgradeFirstBatch; + } + + /// @dev This sets the first post upgrade batch for era, used to check old withdrawals + /// @param _eraLegacyBridgeLastDepositBatch The the ZKsync Era batch number that processes the last deposit tx initiated by the legacy bridge + /// @param _eraLegacyBridgeLastDepositTxNumber The tx number in the _eraLegacyBridgeLastDepositBatch of the last deposit tx initiated by the legacy bridge + function setEraLegacyBridgeLastDepositTime( + uint256 _eraLegacyBridgeLastDepositBatch, + uint256 _eraLegacyBridgeLastDepositTxNumber + ) external onlyOwner { + if (eraLegacyBridgeLastDepositBatch != 0) { + revert SharedBridgeValueAlreadySet(SharedBridgeKey.LegacyBridgeLastDepositBatch); + } + if (eraLegacyBridgeLastDepositTxNumber != 0) { + revert SharedBridgeValueAlreadySet(SharedBridgeKey.LegacyBridgeLastDepositTxn); + } + eraLegacyBridgeLastDepositBatch = _eraLegacyBridgeLastDepositBatch; + eraLegacyBridgeLastDepositTxNumber = _eraLegacyBridgeLastDepositTxNumber; + } + + /// @dev Transfer tokens from legacy erc20 bridge or mailbox and set chainBalance as part of migration process. + /// @param _token The address of token to be transferred (address(1) for ether and contract address for ERC20). + /// @param _target The hyperchain or bridge contract address from where to transfer funds. + /// @param _targetChainId The chain ID of the corresponding hyperchain. + function transferFundsFromLegacy(address _token, address _target, uint256 _targetChainId) external onlySelf { + if (_token == ETH_TOKEN_ADDRESS) { + uint256 balanceBefore = address(this).balance; + IMailbox(_target).transferEthToSharedBridge(); + uint256 balanceAfter = address(this).balance; + if (balanceAfter <= balanceBefore) { + revert NoFundsTransferred(); + } + chainBalance[_targetChainId][ETH_TOKEN_ADDRESS] = + chainBalance[_targetChainId][ETH_TOKEN_ADDRESS] + + balanceAfter - + balanceBefore; + } else { + uint256 balanceBefore = IERC20(_token).balanceOf(address(this)); + uint256 legacyBridgeBalance = IERC20(_token).balanceOf(address(legacyBridge)); + if (legacyBridgeBalance == 0) { + revert ZeroBalance(); + } + IL1ERC20Bridge(_target).transferTokenToSharedBridge(_token); + uint256 balanceAfter = IERC20(_token).balanceOf(address(this)); + if (balanceAfter - balanceBefore < legacyBridgeBalance) { + revert SharedBridgeBalanceMismatch(); + } + chainBalance[_targetChainId][_token] = chainBalance[_targetChainId][_token] + legacyBridgeBalance; + } + } + + /// @dev transfer tokens from legacy erc20 bridge or mailbox and set chainBalance as part of migration process. + /// @dev Unlike `transferFundsFromLegacy` is provides a concrete limit on the gas used for the transfer and even if it will fail, it will not revert the whole transaction. + function safeTransferFundsFromLegacy( + address _token, + address _target, + uint256 _targetChainId, + uint256 _gasPerToken + ) external onlyOwner { + try this.transferFundsFromLegacy{gas: _gasPerToken}(_token, _target, _targetChainId) {} catch { + // A reasonable amount of gas will be provided to transfer the token. + // If the transfer fails, we don't want to revert the whole transaction. + } + } + + /// @dev Accepts ether only from the hyperchain associated with the specified chain ID. + /// @param _chainId The chain ID corresponding to the hyperchain allowed to send ether. + function receiveEth(uint256 _chainId) external payable { + if (BRIDGE_HUB.getHyperchain(_chainId) != msg.sender) { + revert Unauthorized(msg.sender); + } + } + + /// @dev Initializes the l2Bridge address by governance for a specific chain. + function initializeChainGovernance(uint256 _chainId, address _l2BridgeAddress) external onlyOwner { + l2BridgeAddress[_chainId] = _l2BridgeAddress; + } + + /// @notice Allows bridgehub to acquire mintValue for L1->L2 transactions. + /// @dev If the corresponding L2 transaction fails, refunds are issued to a refund recipient on L2. + /// @param _chainId The chain ID of the hyperchain to which deposit. + /// @param _prevMsgSender The `msg.sender` address from the external call that initiated current one. + /// @param _l1Token The L1 token address which is deposited. + /// @param _amount The total amount of tokens to be bridged. + function bridgehubDepositBaseToken( + uint256 _chainId, + address _prevMsgSender, + address _l1Token, + uint256 _amount + ) external payable virtual onlyBridgehubOrEra(_chainId) whenNotPaused { + if (_l1Token == ETH_TOKEN_ADDRESS) { + if (msg.value != _amount) { + revert ValueMismatch(_amount, msg.value); + } + } else { + // The Bridgehub also checks this, but we want to be sure + if (msg.value != 0) { + revert NonEmptyMsgValue(); + } + + uint256 amount = _depositFunds(_prevMsgSender, IERC20(_l1Token), _amount); // note if _prevMsgSender is this contract, this will return 0. This does not happen. + // The token has non-standard transfer logic + if (amount != _amount) { + revert TokensWithFeesNotSupported(); + } + } + + if (!hyperbridgingEnabled[_chainId]) { + chainBalance[_chainId][_l1Token] += _amount; + } + // Note that we don't save the deposited amount, as this is for the base token, which gets sent to the refundRecipient if the tx fails + emit BridgehubDepositBaseTokenInitiated(_chainId, _prevMsgSender, _l1Token, _amount); + } + + /// @dev Transfers tokens from the depositor address to the smart contract address. + /// @return The difference between the contract balance before and after the transferring of funds. + function _depositFunds(address _from, IERC20 _token, uint256 _amount) internal returns (uint256) { + uint256 balanceBefore = _token.balanceOf(address(this)); + // slither-disable-next-line arbitrary-send-erc20 + _token.safeTransferFrom(_from, address(this), _amount); + uint256 balanceAfter = _token.balanceOf(address(this)); + + return balanceAfter - balanceBefore; + } + + /// @notice Initiates a deposit transaction within Bridgehub, used by `requestL2TransactionTwoBridges`. + /// @param _chainId The chain ID of the hyperchain to which deposit. + /// @param _prevMsgSender The `msg.sender` address from the external call that initiated current one. + /// @param _l2Value The L2 `msg.value` from the L1 -> L2 deposit transaction. + /// @param _data The calldata for the second bridge deposit. + function bridgehubDeposit( + uint256 _chainId, + address _prevMsgSender, + // solhint-disable-next-line no-unused-vars + uint256 _l2Value, + bytes calldata _data + ) + external + payable + override + onlyBridgehub + whenNotPaused + returns (L2TransactionRequestTwoBridgesInner memory request) + { + if (l2BridgeAddress[_chainId] == address(0)) { + revert L2BridgeNotSet(_chainId); + } + + (address _l1Token, uint256 _depositAmount, address _l2Receiver) = abi.decode( + _data, + (address, uint256, address) + ); + if (_l1Token == L1_WETH_TOKEN) { + revert TokenNotSupported(L1_WETH_TOKEN); + } + if (BRIDGE_HUB.baseToken(_chainId) == _l1Token) { + revert TokenNotSupported(_l1Token); + } + + uint256 amount; + if (_l1Token == ETH_TOKEN_ADDRESS) { + amount = msg.value; + if (_depositAmount != 0) { + revert DepositIncorrectAmount(0, _depositAmount); + } + } else { + if (msg.value != 0) { + revert NonEmptyMsgValue(); + } + amount = _depositAmount; + + uint256 depAmount = _depositFunds(_prevMsgSender, IERC20(_l1Token), _depositAmount); + // The token has non-standard transfer logic + if (depAmount != _depositAmount) { + revert DepositIncorrectAmount(depAmount, _depositAmount); + } + } + // empty deposit amount + if (amount == 0) { + revert EmptyDeposit(); + } + + bytes32 txDataHash = keccak256(abi.encode(_prevMsgSender, _l1Token, amount)); + if (!hyperbridgingEnabled[_chainId]) { + chainBalance[_chainId][_l1Token] += amount; + } + + { + // Request the finalization of the deposit on the L2 side + bytes memory l2TxCalldata = _getDepositL2Calldata(_prevMsgSender, _l2Receiver, _l1Token, amount); + + request = L2TransactionRequestTwoBridgesInner({ + magicValue: TWO_BRIDGES_MAGIC_VALUE, + l2Contract: l2BridgeAddress[_chainId], + l2Calldata: l2TxCalldata, + factoryDeps: new bytes[](0), + txDataHash: txDataHash + }); + } + emit BridgehubDepositInitiated({ + chainId: _chainId, + txDataHash: txDataHash, + from: _prevMsgSender, + to: _l2Receiver, + l1Token: _l1Token, + amount: amount + }); + } + + /// @notice Confirms the acceptance of a transaction by the Mailbox, as part of the L2 transaction process within Bridgehub. + /// This function is utilized by `requestL2TransactionTwoBridges` to validate the execution of a transaction. + /// @param _chainId The chain ID of the hyperchain to which confirm the deposit. + /// @param _txDataHash The keccak256 hash of abi.encode(msgSender, l1Token, amount) + /// @param _txHash The hash of the L1->L2 transaction to confirm the deposit. + function bridgehubConfirmL2Transaction( + uint256 _chainId, + bytes32 _txDataHash, + bytes32 _txHash + ) external override onlyBridgehub whenNotPaused { + if (depositHappened[_chainId][_txHash] != 0x00) { + revert DepositExists(); + } + depositHappened[_chainId][_txHash] = _txDataHash; + emit BridgehubDepositFinalized(_chainId, _txDataHash, _txHash); + } + + /// @dev Sets the L1ERC20Bridge contract address. Should be called only once. + function setL1Erc20Bridge(address _legacyBridge) external onlyOwner { + if (address(legacyBridge) != address(0)) { + revert AddressAlreadyUsed(address(legacyBridge)); + } + if (_legacyBridge == address(0)) { + revert ZeroAddress(); + } + legacyBridge = IL1ERC20Bridge(_legacyBridge); + } + + /// @dev Generate a calldata for calling the deposit finalization on the L2 bridge contract + function _getDepositL2Calldata( + address _l1Sender, + address _l2Receiver, + address _l1Token, + uint256 _amount + ) internal view returns (bytes memory) { + bytes memory gettersData = _getERC20Getters(_l1Token); + return abi.encodeCall(IL2Bridge.finalizeDeposit, (_l1Sender, _l2Receiver, _l1Token, _amount, gettersData)); + } + + /// @dev Receives and parses (name, symbol, decimals) from the token contract + function _getERC20Getters(address _token) internal view returns (bytes memory) { + if (_token == ETH_TOKEN_ADDRESS) { + bytes memory name = bytes("Ether"); + bytes memory symbol = bytes("ETH"); + bytes memory decimals = abi.encode(uint8(18)); + return abi.encode(name, symbol, decimals); // when depositing eth to a non-eth based chain it is an ERC20 + } + + (, bytes memory data1) = _token.staticcall(abi.encodeCall(IERC20Metadata.name, ())); + (, bytes memory data2) = _token.staticcall(abi.encodeCall(IERC20Metadata.symbol, ())); + (, bytes memory data3) = _token.staticcall(abi.encodeCall(IERC20Metadata.decimals, ())); + return abi.encode(data1, data2, data3); + } + + /// @dev Withdraw funds from the initiated deposit, that failed when finalizing on L2 + /// @param _depositSender The address of the deposit initiator + /// @param _l1Token The address of the deposited L1 ERC20 token + /// @param _amount The amount of the deposit that failed. + /// @param _l2TxHash The L2 transaction hash of the failed deposit finalization + /// @param _l2BatchNumber The L2 batch number where the deposit finalization was processed + /// @param _l2MessageIndex The position in the L2 logs Merkle tree of the l2Log that was sent with the message + /// @param _l2TxNumberInBatch The L2 transaction number in a batch, in which the log was sent + /// @param _merkleProof The Merkle proof of the processing L1 -> L2 transaction with deposit finalization + function claimFailedDeposit( + uint256 _chainId, + address _depositSender, + address _l1Token, + uint256 _amount, + bytes32 _l2TxHash, + uint256 _l2BatchNumber, + uint256 _l2MessageIndex, + uint16 _l2TxNumberInBatch, + bytes32[] calldata _merkleProof + ) external override { + _claimFailedDeposit({ + _checkedInLegacyBridge: false, + _chainId: _chainId, + _depositSender: _depositSender, + _l1Token: _l1Token, + _amount: _amount, + _l2TxHash: _l2TxHash, + _l2BatchNumber: _l2BatchNumber, + _l2MessageIndex: _l2MessageIndex, + _l2TxNumberInBatch: _l2TxNumberInBatch, + _merkleProof: _merkleProof + }); + } + + /// @dev Processes claims of failed deposit, whether they originated from the legacy bridge or the current system. + function _claimFailedDeposit( + bool _checkedInLegacyBridge, + uint256 _chainId, + address _depositSender, + address _l1Token, + uint256 _amount, + bytes32 _l2TxHash, + uint256 _l2BatchNumber, + uint256 _l2MessageIndex, + uint16 _l2TxNumberInBatch, + bytes32[] calldata _merkleProof + ) internal nonReentrant whenNotPaused { + { + bool proofValid = BRIDGE_HUB.proveL1ToL2TransactionStatus({ + _chainId: _chainId, + _l2TxHash: _l2TxHash, + _l2BatchNumber: _l2BatchNumber, + _l2MessageIndex: _l2MessageIndex, + _l2TxNumberInBatch: _l2TxNumberInBatch, + _merkleProof: _merkleProof, + _status: TxStatus.Failure + }); + if (!proofValid) { + revert InvalidProof(); + } + } + if (_amount == 0) { + revert NoFundsTransferred(); + } + + { + bool notCheckedInLegacyBridgeOrWeCanCheckDeposit; + { + // Deposits that happened before the upgrade cannot be checked here, they have to be claimed and checked in the legacyBridge + bool weCanCheckDepositHere = !_isEraLegacyDeposit(_chainId, _l2BatchNumber, _l2TxNumberInBatch); + // Double claims are not possible, as depositHappened is checked here for all except legacy deposits (which have to happen through the legacy bridge) + // Funds claimed before the update will still be recorded in the legacy bridge + // Note we double check NEW deposits if they are called from the legacy bridge + notCheckedInLegacyBridgeOrWeCanCheckDeposit = (!_checkedInLegacyBridge) || weCanCheckDepositHere; + } + if (notCheckedInLegacyBridgeOrWeCanCheckDeposit) { + bytes32 dataHash = depositHappened[_chainId][_l2TxHash]; + bytes32 txDataHash = keccak256(abi.encode(_depositSender, _l1Token, _amount)); + if (dataHash != txDataHash) { + revert DepositDoesNotExist(); + } + delete depositHappened[_chainId][_l2TxHash]; + } + } + + if (!hyperbridgingEnabled[_chainId]) { + // check that the chain has sufficient balance + if (chainBalance[_chainId][_l1Token] < _amount) { + revert InsufficientChainBalance(); + } + chainBalance[_chainId][_l1Token] -= _amount; + } + + // Withdraw funds + if (_l1Token == ETH_TOKEN_ADDRESS) { + bool callSuccess; + // Low-level assembly call, to avoid any memory copying (save gas) + assembly { + callSuccess := call(gas(), _depositSender, _amount, 0, 0, 0, 0) + } + if (!callSuccess) { + revert WithdrawFailed(); + } + } else { + IERC20(_l1Token).safeTransfer(_depositSender, _amount); + // Note we don't allow weth deposits anymore, but there might be legacy weth deposits. + // until we add Weth bridging capabilities, we don't wrap/unwrap weth to ether. + } + + emit ClaimedFailedDepositSharedBridge(_chainId, _depositSender, _l1Token, _amount); + } + + /// @dev Determines if an eth withdrawal was initiated on ZKsync Era before the upgrade to the Shared Bridge. + /// @param _chainId The chain ID of the transaction to check. + /// @param _l2BatchNumber The L2 batch number for the withdrawal. + /// @return Whether withdrawal was initiated on ZKsync Era before diamond proxy upgrade. + function _isEraLegacyEthWithdrawal(uint256 _chainId, uint256 _l2BatchNumber) internal view returns (bool) { + if ((_chainId == ERA_CHAIN_ID) && eraPostDiamondUpgradeFirstBatch == 0) { + revert SharedBridgeValueNotSet(SharedBridgeKey.PostUpgradeFirstBatch); + } + return (_chainId == ERA_CHAIN_ID) && (_l2BatchNumber < eraPostDiamondUpgradeFirstBatch); + } + + /// @dev Determines if a token withdrawal was initiated on ZKsync Era before the upgrade to the Shared Bridge. + /// @param _chainId The chain ID of the transaction to check. + /// @param _l2BatchNumber The L2 batch number for the withdrawal. + /// @return Whether withdrawal was initiated on ZKsync Era before Legacy Bridge upgrade. + function _isEraLegacyTokenWithdrawal(uint256 _chainId, uint256 _l2BatchNumber) internal view returns (bool) { + if ((_chainId == ERA_CHAIN_ID) && eraPostLegacyBridgeUpgradeFirstBatch == 0) { + revert SharedBridgeValueNotSet(SharedBridgeKey.LegacyBridgeFirstBatch); + } + return (_chainId == ERA_CHAIN_ID) && (_l2BatchNumber < eraPostLegacyBridgeUpgradeFirstBatch); + } + + /// @dev Determines if a deposit was initiated on ZKsync Era before the upgrade to the Shared Bridge. + /// @param _chainId The chain ID of the transaction to check. + /// @param _l2BatchNumber The L2 batch number for the deposit where it was processed. + /// @param _l2TxNumberInBatch The L2 transaction number in the batch, in which the deposit was processed. + /// @return Whether deposit was initiated on ZKsync Era before Shared Bridge upgrade. + function _isEraLegacyDeposit( + uint256 _chainId, + uint256 _l2BatchNumber, + uint256 _l2TxNumberInBatch + ) internal view returns (bool) { + if ((_chainId == ERA_CHAIN_ID) && (eraLegacyBridgeLastDepositBatch == 0)) { + revert SharedBridgeValueNotSet(SharedBridgeKey.LegacyBridgeLastDepositBatch); + } + return + (_chainId == ERA_CHAIN_ID) && + (_l2BatchNumber < eraLegacyBridgeLastDepositBatch || + (_l2TxNumberInBatch < eraLegacyBridgeLastDepositTxNumber && + _l2BatchNumber == eraLegacyBridgeLastDepositBatch)); + } + + /// @notice Finalize the withdrawal and release funds + /// @param _chainId The chain ID of the transaction to check + /// @param _l2BatchNumber The L2 batch number where the withdrawal was processed + /// @param _l2MessageIndex The position in the L2 logs Merkle tree of the l2Log that was sent with the message + /// @param _l2TxNumberInBatch The L2 transaction number in the batch, in which the log was sent + /// @param _message The L2 withdraw data, stored in an L2 -> L1 message + /// @param _merkleProof The Merkle proof of the inclusion L2 -> L1 message about withdrawal initialization + function finalizeWithdrawal( + uint256 _chainId, + uint256 _l2BatchNumber, + uint256 _l2MessageIndex, + uint16 _l2TxNumberInBatch, + bytes calldata _message, + bytes32[] calldata _merkleProof + ) external override { + // To avoid rewithdrawing txs that have already happened on the legacy bridge. + // Note: new withdraws are all recorded here, so double withdrawing them is not possible. + if (_isEraLegacyTokenWithdrawal(_chainId, _l2BatchNumber)) { + if (legacyBridge.isWithdrawalFinalized(_l2BatchNumber, _l2MessageIndex)) { + revert WithdrawalAlreadyFinalized(); + } + } + _finalizeWithdrawal({ + _chainId: _chainId, + _l2BatchNumber: _l2BatchNumber, + _l2MessageIndex: _l2MessageIndex, + _l2TxNumberInBatch: _l2TxNumberInBatch, + _message: _message, + _merkleProof: _merkleProof + }); + } + + struct MessageParams { + uint256 l2BatchNumber; + uint256 l2MessageIndex; + uint16 l2TxNumberInBatch; + } + + /// @dev Internal function that handles the logic for finalizing withdrawals, + /// serving both the current bridge system and the legacy ERC20 bridge. + function _finalizeWithdrawal( + uint256 _chainId, + uint256 _l2BatchNumber, + uint256 _l2MessageIndex, + uint16 _l2TxNumberInBatch, + bytes calldata _message, + bytes32[] calldata _merkleProof + ) internal nonReentrant whenNotPaused returns (address l1Receiver, address l1Token, uint256 amount) { + if (isWithdrawalFinalized[_chainId][_l2BatchNumber][_l2MessageIndex]) { + revert WithdrawalAlreadyFinalized(); + } + isWithdrawalFinalized[_chainId][_l2BatchNumber][_l2MessageIndex] = true; + + // Handling special case for withdrawal from ZKsync Era initiated before Shared Bridge. + if (_isEraLegacyEthWithdrawal(_chainId, _l2BatchNumber)) { + // Checks that the withdrawal wasn't finalized already. + bool alreadyFinalized = IGetters(ERA_DIAMOND_PROXY).isEthWithdrawalFinalized( + _l2BatchNumber, + _l2MessageIndex + ); + if (alreadyFinalized) { + revert WithdrawalAlreadyFinalized(); + } + } + + MessageParams memory messageParams = MessageParams({ + l2BatchNumber: _l2BatchNumber, + l2MessageIndex: _l2MessageIndex, + l2TxNumberInBatch: _l2TxNumberInBatch + }); + (l1Receiver, l1Token, amount) = _checkWithdrawal(_chainId, messageParams, _message, _merkleProof); + + if (!hyperbridgingEnabled[_chainId]) { + // Check that the chain has sufficient balance + if (chainBalance[_chainId][l1Token] < amount) { + // not enough funds + revert InsufficientChainBalance(); + } + chainBalance[_chainId][l1Token] -= amount; + } + + if (l1Token == ETH_TOKEN_ADDRESS) { + bool callSuccess; + // Low-level assembly call, to avoid any memory copying (save gas) + assembly { + callSuccess := call(gas(), l1Receiver, amount, 0, 0, 0, 0) + } + if (!callSuccess) { + revert WithdrawFailed(); + } + } else { + // Withdraw funds + IERC20(l1Token).safeTransfer(l1Receiver, amount); + } + emit WithdrawalFinalizedSharedBridge(_chainId, l1Receiver, l1Token, amount); + } + + /// @dev Verifies the validity of a withdrawal message from L2 and returns details of the withdrawal. + function _checkWithdrawal( + uint256 _chainId, + MessageParams memory _messageParams, + bytes calldata _message, + bytes32[] calldata _merkleProof + ) internal view returns (address l1Receiver, address l1Token, uint256 amount) { + (l1Receiver, l1Token, amount) = _parseL2WithdrawalMessage(_chainId, _message); + L2Message memory l2ToL1Message; + { + bool baseTokenWithdrawal = (l1Token == BRIDGE_HUB.baseToken(_chainId)); + address l2Sender = baseTokenWithdrawal ? L2_BASE_TOKEN_SYSTEM_CONTRACT_ADDR : l2BridgeAddress[_chainId]; + + l2ToL1Message = L2Message({ + txNumberInBatch: _messageParams.l2TxNumberInBatch, + sender: l2Sender, + data: _message + }); + } + + bool success = BRIDGE_HUB.proveL2MessageInclusion({ + _chainId: _chainId, + _batchNumber: _messageParams.l2BatchNumber, + _index: _messageParams.l2MessageIndex, + _message: l2ToL1Message, + _proof: _merkleProof + }); + // withdrawal wrong proof + if (!success) { + revert InvalidProof(); + } + } + + function _parseL2WithdrawalMessage( + uint256 _chainId, + bytes memory _l2ToL1message + ) internal view returns (address l1Receiver, address l1Token, uint256 amount) { + // We check that the message is long enough to read the data. + // Please note that there are two versions of the message: + // 1. The message that is sent by `withdraw(address _l1Receiver)` + // It should be equal to the length of the bytes4 function signature + address l1Receiver + uint256 amount = 4 + 20 + 32 = 56 (bytes). + // 2. The message that is sent by `withdrawWithMessage(address _l1Receiver, bytes calldata _additionalData)` + // It should be equal to the length of the following: + // bytes4 function signature + address l1Receiver + uint256 amount + address l2Sender + bytes _additionalData = + // = 4 + 20 + 32 + 32 + _additionalData.length >= 68 (bytes). + + // So the data is expected to be at least 56 bytes long. + // wrong message length + if (_l2ToL1message.length < 56) { + revert L2WithdrawalMessageWrongLength(_l2ToL1message.length); + } + + (uint32 functionSignature, uint256 offset) = UnsafeBytes.readUint32(_l2ToL1message, 0); + if (bytes4(functionSignature) == IMailbox.finalizeEthWithdrawal.selector) { + // this message is a base token withdrawal + (l1Receiver, offset) = UnsafeBytes.readAddress(_l2ToL1message, offset); + (amount, offset) = UnsafeBytes.readUint256(_l2ToL1message, offset); + l1Token = BRIDGE_HUB.baseToken(_chainId); + } else if (bytes4(functionSignature) == IL1ERC20Bridge.finalizeWithdrawal.selector) { + // We use the IL1ERC20Bridge for backward compatibility with old withdrawals. + + // this message is a token withdrawal + + // Check that the message length is correct. + // It should be equal to the length of the function signature + address + address + uint256 = 4 + 20 + 20 + 32 = + // 76 (bytes). + if (_l2ToL1message.length != 76) { + revert L2WithdrawalMessageWrongLength(_l2ToL1message.length); + } + (l1Receiver, offset) = UnsafeBytes.readAddress(_l2ToL1message, offset); + (l1Token, offset) = UnsafeBytes.readAddress(_l2ToL1message, offset); + (amount, offset) = UnsafeBytes.readUint256(_l2ToL1message, offset); + } else { + revert InvalidSelector(bytes4(functionSignature)); + } + } + + /*////////////////////////////////////////////////////////////// + ERA LEGACY FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /// @notice Initiates a deposit by locking funds on the contract and sending the request + /// of processing an L2 transaction where tokens would be minted. + /// @dev If the token is bridged for the first time, the L2 token contract will be deployed. Note however, that the + /// newly-deployed token does not support any custom logic, i.e. rebase tokens' functionality is not supported. + /// @param _prevMsgSender The `msg.sender` address from the external call that initiated current one. + /// @param _l2Receiver The account address that should receive funds on L2 + /// @param _l1Token The L1 token address which is deposited + /// @param _amount The total amount of tokens to be bridged + /// @param _l2TxGasLimit The L2 gas limit to be used in the corresponding L2 transaction + /// @param _l2TxGasPerPubdataByte The gasPerPubdataByteLimit to be used in the corresponding L2 transaction + /// @param _refundRecipient The address on L2 that will receive the refund for the transaction. + /// @dev If the L2 deposit finalization transaction fails, the `_refundRecipient` will receive the `_l2Value`. + /// Please note, the contract may change the refund recipient's address to eliminate sending funds to addresses + /// out of control. + /// - If `_refundRecipient` is a contract on L1, the refund will be sent to the aliased `_refundRecipient`. + /// - If `_refundRecipient` is set to `address(0)` and the sender has NO deployed bytecode on L1, the refund will + /// be sent to the `msg.sender` address. + /// - If `_refundRecipient` is set to `address(0)` and the sender has deployed bytecode on L1, the refund will be + /// sent to the aliased `msg.sender` address. + /// @dev The address aliasing of L1 contracts as refund recipient on L2 is necessary to guarantee that the funds + /// are controllable through the Mailbox, since the Mailbox applies address aliasing to the from address for the + /// L2 tx if the L1 msg.sender is a contract. Without address aliasing for L1 contracts as refund recipients they + /// would not be able to make proper L2 tx requests through the Mailbox to use or withdraw the funds from L2, and + /// the funds would be lost. + /// @return l2TxHash The L2 transaction hash of deposit finalization. + function depositLegacyErc20Bridge( + address _prevMsgSender, + address _l2Receiver, + address _l1Token, + uint256 _amount, + uint256 _l2TxGasLimit, + uint256 _l2TxGasPerPubdataByte, + address _refundRecipient + ) external payable override onlyLegacyBridge nonReentrant whenNotPaused returns (bytes32 l2TxHash) { + if (l2BridgeAddress[ERA_CHAIN_ID] == address(0)) { + revert L2BridgeNotSet(ERA_CHAIN_ID); + } + if (_l1Token == L1_WETH_TOKEN) { + revert TokenNotSupported(L1_WETH_TOKEN); + } + + // Note that funds have been transferred to this contract in the legacy ERC20 bridge. + if (!hyperbridgingEnabled[ERA_CHAIN_ID]) { + chainBalance[ERA_CHAIN_ID][_l1Token] += _amount; + } + + bytes memory l2TxCalldata = _getDepositL2Calldata(_prevMsgSender, _l2Receiver, _l1Token, _amount); + + { + // If the refund recipient is not specified, the refund will be sent to the sender of the transaction. + // Otherwise, the refund will be sent to the specified address. + // If the recipient is a contract on L1, the address alias will be applied. + address refundRecipient = AddressAliasHelper.actualRefundRecipient(_refundRecipient, _prevMsgSender); + + L2TransactionRequestDirect memory request = L2TransactionRequestDirect({ + chainId: ERA_CHAIN_ID, + l2Contract: l2BridgeAddress[ERA_CHAIN_ID], + mintValue: msg.value, // l2 gas + l2 msg.Value the bridgehub will withdraw the mintValue from the base token bridge for gas + l2Value: 0, // L2 msg.value, this contract doesn't support base token deposits or wrapping functionality, for direct deposits use bridgehub + l2Calldata: l2TxCalldata, + l2GasLimit: _l2TxGasLimit, + l2GasPerPubdataByteLimit: _l2TxGasPerPubdataByte, + factoryDeps: new bytes[](0), + refundRecipient: refundRecipient + }); + l2TxHash = BRIDGE_HUB.requestL2TransactionDirect{value: msg.value}(request); + } + + bytes32 txDataHash = keccak256(abi.encode(_prevMsgSender, _l1Token, _amount)); + // Save the deposited amount to claim funds on L1 if the deposit failed on L2 + depositHappened[ERA_CHAIN_ID][l2TxHash] = txDataHash; + + emit LegacyDepositInitiated({ + chainId: ERA_CHAIN_ID, + l2DepositTxHash: l2TxHash, + from: _prevMsgSender, + to: _l2Receiver, + l1Token: _l1Token, + amount: _amount + }); + } + + /// @notice Finalizes the withdrawal for transactions initiated via the legacy ERC20 bridge. + /// @param _l2BatchNumber The L2 batch number where the withdrawal was processed + /// @param _l2MessageIndex The position in the L2 logs Merkle tree of the l2Log that was sent with the message + /// @param _l2TxNumberInBatch The L2 transaction number in the batch, in which the log was sent + /// @param _message The L2 withdraw data, stored in an L2 -> L1 message + /// @param _merkleProof The Merkle proof of the inclusion L2 -> L1 message about withdrawal initialization + /// + /// @return l1Receiver The address on L1 that will receive the withdrawn funds + /// @return l1Token The address of the L1 token being withdrawn + /// @return amount The amount of the token being withdrawn + function finalizeWithdrawalLegacyErc20Bridge( + uint256 _l2BatchNumber, + uint256 _l2MessageIndex, + uint16 _l2TxNumberInBatch, + bytes calldata _message, + bytes32[] calldata _merkleProof + ) external override onlyLegacyBridge returns (address l1Receiver, address l1Token, uint256 amount) { + (l1Receiver, l1Token, amount) = _finalizeWithdrawal({ + _chainId: ERA_CHAIN_ID, + _l2BatchNumber: _l2BatchNumber, + _l2MessageIndex: _l2MessageIndex, + _l2TxNumberInBatch: _l2TxNumberInBatch, + _message: _message, + _merkleProof: _merkleProof + }); + } + + /// @notice Withdraw funds from the initiated deposit, that failed when finalizing on ZKsync Era chain. + /// This function is specifically designed for maintaining backward-compatibility with legacy `claimFailedDeposit` + /// method in `L1ERC20Bridge`. + /// + /// @param _depositSender The address of the deposit initiator + /// @param _l1Token The address of the deposited L1 ERC20 token + /// @param _amount The amount of the deposit that failed. + /// @param _l2TxHash The L2 transaction hash of the failed deposit finalization + /// @param _l2BatchNumber The L2 batch number where the deposit finalization was processed + /// @param _l2MessageIndex The position in the L2 logs Merkle tree of the l2Log that was sent with the message + /// @param _l2TxNumberInBatch The L2 transaction number in a batch, in which the log was sent + /// @param _merkleProof The Merkle proof of the processing L1 -> L2 transaction with deposit finalization + function claimFailedDepositLegacyErc20Bridge( + address _depositSender, + address _l1Token, + uint256 _amount, + bytes32 _l2TxHash, + uint256 _l2BatchNumber, + uint256 _l2MessageIndex, + uint16 _l2TxNumberInBatch, + bytes32[] calldata _merkleProof + ) external override onlyLegacyBridge { + _claimFailedDeposit({ + _checkedInLegacyBridge: true, + _chainId: ERA_CHAIN_ID, + _depositSender: _depositSender, + _l1Token: _l1Token, + _amount: _amount, + _l2TxHash: _l2TxHash, + _l2BatchNumber: _l2BatchNumber, + _l2MessageIndex: _l2MessageIndex, + _l2TxNumberInBatch: _l2TxNumberInBatch, + _merkleProof: _merkleProof + }); + } + + /*////////////////////////////////////////////////////////////// + PAUSE + //////////////////////////////////////////////////////////////*/ + + /// @notice Pauses all functions marked with the `whenNotPaused` modifier. + function pause() external onlyOwner { + _pause(); + } + + /// @notice Unpauses the contract, allowing all functions marked with the `whenNotPaused` modifier to be called again. + function unpause() external onlyOwner { + _unpause(); + } +} diff --git a/contracts/l1-contracts/bridge/interfaces/IL1ERC20Bridge.sol b/contracts/l1-contracts/bridge/interfaces/IL1ERC20Bridge.sol new file mode 100644 index 0000000..722eeed --- /dev/null +++ b/contracts/l1-contracts/bridge/interfaces/IL1ERC20Bridge.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.21; + +import {IL1SharedBridge} from "./IL1SharedBridge.sol"; + +/// @title L1 Bridge contract legacy interface +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +/// @notice Legacy Bridge interface before hyperchain migration, used for backward compatibility with ZKsync Era +interface IL1ERC20Bridge { + event DepositInitiated( + bytes32 indexed l2DepositTxHash, + address indexed from, + address indexed to, + address l1Token, + uint256 amount + ); + + event WithdrawalFinalized(address indexed to, address indexed l1Token, uint256 amount); + + event ClaimedFailedDeposit(address indexed to, address indexed l1Token, uint256 amount); + + function isWithdrawalFinalized(uint256 _l2BatchNumber, uint256 _l2MessageIndex) external view returns (bool); + + function deposit( + address _l2Receiver, + address _l1Token, + uint256 _amount, + uint256 _l2TxGasLimit, + uint256 _l2TxGasPerPubdataByte, + address _refundRecipient + ) external payable returns (bytes32 txHash); + + function deposit( + address _l2Receiver, + address _l1Token, + uint256 _amount, + uint256 _l2TxGasLimit, + uint256 _l2TxGasPerPubdataByte + ) external payable returns (bytes32 txHash); + + function claimFailedDeposit( + address _depositSender, + address _l1Token, + bytes32 _l2TxHash, + uint256 _l2BatchNumber, + uint256 _l2MessageIndex, + uint16 _l2TxNumberInBatch, + bytes32[] calldata _merkleProof + ) external; + + function finalizeWithdrawal( + uint256 _l2BatchNumber, + uint256 _l2MessageIndex, + uint16 _l2TxNumberInBatch, + bytes calldata _message, + bytes32[] calldata _merkleProof + ) external; + + function l2TokenAddress(address _l1Token) external view returns (address); + + function SHARED_BRIDGE() external view returns (IL1SharedBridge); + + function l2TokenBeacon() external view returns (address); + + function l2Bridge() external view returns (address); + + function depositAmount( + address _account, + address _l1Token, + bytes32 _depositL2TxHash + ) external returns (uint256 amount); + + function transferTokenToSharedBridge(address _token) external; +} diff --git a/contracts/l1-contracts/bridge/interfaces/IL1SharedBridge.sol b/contracts/l1-contracts/bridge/interfaces/IL1SharedBridge.sol new file mode 100644 index 0000000..8038d6b --- /dev/null +++ b/contracts/l1-contracts/bridge/interfaces/IL1SharedBridge.sol @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.21; + +import {L2TransactionRequestTwoBridgesInner} from "../../bridgehub/IBridgehub.sol"; +import {IBridgehub} from "../../bridgehub/IBridgehub.sol"; +import {IL1ERC20Bridge} from "./IL1ERC20Bridge.sol"; + +/// @title L1 Bridge contract interface +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +interface IL1SharedBridge { + event LegacyDepositInitiated( + uint256 indexed chainId, + bytes32 indexed l2DepositTxHash, + address indexed from, + address to, + address l1Token, + uint256 amount + ); + + event BridgehubDepositInitiated( + uint256 indexed chainId, + bytes32 indexed txDataHash, + address indexed from, + address to, + address l1Token, + uint256 amount + ); + + event BridgehubDepositBaseTokenInitiated( + uint256 indexed chainId, + address indexed from, + address l1Token, + uint256 amount + ); + + event BridgehubDepositFinalized( + uint256 indexed chainId, + bytes32 indexed txDataHash, + bytes32 indexed l2DepositTxHash + ); + + event WithdrawalFinalizedSharedBridge( + uint256 indexed chainId, + address indexed to, + address indexed l1Token, + uint256 amount + ); + + event ClaimedFailedDepositSharedBridge( + uint256 indexed chainId, + address indexed to, + address indexed l1Token, + uint256 amount + ); + + function isWithdrawalFinalized( + uint256 _chainId, + uint256 _l2BatchNumber, + uint256 _l2MessageIndex + ) external view returns (bool); + + function depositLegacyErc20Bridge( + address _msgSender, + address _l2Receiver, + address _l1Token, + uint256 _amount, + uint256 _l2TxGasLimit, + uint256 _l2TxGasPerPubdataByte, + address _refundRecipient + ) external payable returns (bytes32 txHash); + + function claimFailedDepositLegacyErc20Bridge( + address _depositSender, + address _l1Token, + uint256 _amount, + bytes32 _l2TxHash, + uint256 _l2BatchNumber, + uint256 _l2MessageIndex, + uint16 _l2TxNumberInBatch, + bytes32[] calldata _merkleProof + ) external; + + function claimFailedDeposit( + uint256 _chainId, + address _depositSender, + address _l1Token, + uint256 _amount, + bytes32 _l2TxHash, + uint256 _l2BatchNumber, + uint256 _l2MessageIndex, + uint16 _l2TxNumberInBatch, + bytes32[] calldata _merkleProof + ) external; + + function finalizeWithdrawalLegacyErc20Bridge( + uint256 _l2BatchNumber, + uint256 _l2MessageIndex, + uint16 _l2TxNumberInBatch, + bytes calldata _message, + bytes32[] calldata _merkleProof + ) external returns (address l1Receiver, address l1Token, uint256 amount); + + function finalizeWithdrawal( + uint256 _chainId, + uint256 _l2BatchNumber, + uint256 _l2MessageIndex, + uint16 _l2TxNumberInBatch, + bytes calldata _message, + bytes32[] calldata _merkleProof + ) external; + + function setEraPostDiamondUpgradeFirstBatch(uint256 _eraPostDiamondUpgradeFirstBatch) external; + + function setEraPostLegacyBridgeUpgradeFirstBatch(uint256 _eraPostLegacyBridgeUpgradeFirstBatch) external; + + function setEraLegacyBridgeLastDepositTime( + uint256 _eraLegacyBridgeLastDepositBatch, + uint256 _eraLegacyBridgeLastDepositTxNumber + ) external; + + function L1_WETH_TOKEN() external view returns (address); + + function BRIDGE_HUB() external view returns (IBridgehub); + + function legacyBridge() external view returns (IL1ERC20Bridge); + + function l2BridgeAddress(uint256 _chainId) external view returns (address); + + function depositHappened(uint256 _chainId, bytes32 _l2TxHash) external view returns (bytes32); + + /// data is abi encoded : + /// address _l1Token, + /// uint256 _amount, + /// address _l2Receiver + function bridgehubDeposit( + uint256 _chainId, + address _prevMsgSender, + uint256 _l2Value, + bytes calldata _data + ) external payable returns (L2TransactionRequestTwoBridgesInner memory request); + + function bridgehubDepositBaseToken( + uint256 _chainId, + address _prevMsgSender, + address _l1Token, + uint256 _amount + ) external payable; + + function bridgehubConfirmL2Transaction(uint256 _chainId, bytes32 _txDataHash, bytes32 _txHash) external; + + function receiveEth(uint256 _chainId) external payable; +} diff --git a/contracts/l1-contracts/bridge/interfaces/IL2Bridge.sol b/contracts/l1-contracts/bridge/interfaces/IL2Bridge.sol new file mode 100644 index 0000000..39c6b42 --- /dev/null +++ b/contracts/l1-contracts/bridge/interfaces/IL2Bridge.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.21; + +/// @author Matter Labs +interface IL2Bridge { + function finalizeDeposit( + address _l1Sender, + address _l2Receiver, + address _l1Token, + uint256 _amount, + bytes calldata _data + ) external; + + function withdraw(address _l1Receiver, address _l2Token, uint256 _amount) external; + + function l1TokenAddress(address _l2Token) external view returns (address); + + function l2TokenAddress(address _l1Token) external view returns (address); + + function l1Bridge() external view returns (address); +} diff --git a/contracts/l1-contracts/bridge/interfaces/IWETH9.sol b/contracts/l1-contracts/bridge/interfaces/IWETH9.sol new file mode 100644 index 0000000..e1536f4 --- /dev/null +++ b/contracts/l1-contracts/bridge/interfaces/IWETH9.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: Apache-2.0 +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.21; + +interface IWETH9 { + function deposit() external payable; + + function withdraw(uint256 wad) external; +} diff --git a/contracts/l1-contracts/bridgehub/Bridgehub.sol b/contracts/l1-contracts/bridgehub/Bridgehub.sol new file mode 100644 index 0000000..0f1ccbf --- /dev/null +++ b/contracts/l1-contracts/bridgehub/Bridgehub.sol @@ -0,0 +1,406 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable-v4/access/Ownable2StepUpgradeable.sol"; +import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable-v4/security/PausableUpgradeable.sol"; + +import {L2TransactionRequestDirect, L2TransactionRequestTwoBridgesOuter, L2TransactionRequestTwoBridgesInner} from "./IBridgehub.sol"; +import {IBridgehub, IL1SharedBridge} from "../bridge/interfaces/IL1SharedBridge.sol"; +import {IStateTransitionManager} from "../state-transition/IStateTransitionManager.sol"; +import {ReentrancyGuard} from "../common/ReentrancyGuard.sol"; +import {IZkSyncHyperchain} from "../state-transition/chain-interfaces/IZkSyncHyperchain.sol"; +import {ETH_TOKEN_ADDRESS, TWO_BRIDGES_MAGIC_VALUE, BRIDGEHUB_MIN_SECOND_BRIDGE_ADDRESS} from "../common/Config.sol"; +import {BridgehubL2TransactionRequest, L2Message, L2Log, TxStatus} from "../common/Messaging.sol"; +import {AddressAliasHelper} from "../vendor/AddressAliasHelper.sol"; +import {Unauthorized, STMAlreadyRegistered, STMNotRegistered, TokenAlreadyRegistered, TokenNotRegistered, ZeroChainId, ChainIdTooBig, SharedBridgeNotSet, BridgeHubAlreadyRegistered, AddressTooLow, MsgValueMismatch, WrongMagicValue, ZeroAddress} from "../common/L1ContractErrors.sol"; + +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +/// @dev The Bridgehub contract serves as the primary entry point for L1<->L2 communication, +/// facilitating interactions between end user and bridges. +/// It also manages state transition managers, base tokens, and chain registrations. +contract Bridgehub is IBridgehub, ReentrancyGuard, Ownable2StepUpgradeable, PausableUpgradeable { + /// @notice all the ether is held by the weth bridge + IL1SharedBridge public sharedBridge; + + /// @notice we store registered stateTransitionManagers + mapping(address _stateTransitionManager => bool) public stateTransitionManagerIsRegistered; + /// @notice we store registered tokens (for arbitrary base token) + mapping(address _token => bool) public tokenIsRegistered; + + /// @notice chainID => StateTransitionManager contract address, storing StateTransitionManager + mapping(uint256 _chainId => address) public stateTransitionManager; + + /// @notice chainID => baseToken contract address, storing baseToken + mapping(uint256 _chainId => address) public baseToken; + + /// @dev used to manage non critical updates + address public admin; + + /// @dev used to accept the admin role + address private pendingAdmin; + + /// @notice to avoid parity hack + constructor() reentrancyGuardInitializer {} + + /// @notice used to initialize the contract + function initialize(address _owner) external reentrancyGuardInitializer { + _transferOwnership(_owner); + } + + modifier onlyOwnerOrAdmin() { + if (msg.sender != admin && msg.sender != owner()) { + revert Unauthorized(msg.sender); + } + _; + } + + /// @inheritdoc IBridgehub + /// @dev Please note, if the owner wants to enforce the admin change it must execute both `setPendingAdmin` and + /// `acceptAdmin` atomically. Otherwise `admin` can set different pending admin and so fail to accept the admin rights. + function setPendingAdmin(address _newPendingAdmin) external onlyOwnerOrAdmin { + if (_newPendingAdmin == address(0)) { + revert ZeroAddress(); + } + // Save previous value into the stack to put it into the event later + address oldPendingAdmin = pendingAdmin; + // Change pending admin + pendingAdmin = _newPendingAdmin; + emit NewPendingAdmin(oldPendingAdmin, _newPendingAdmin); + } + + /// @inheritdoc IBridgehub + function acceptAdmin() external { + address currentPendingAdmin = pendingAdmin; + // Only proposed by current admin address can claim the admin rights + if (msg.sender != currentPendingAdmin) { + revert Unauthorized(msg.sender); + } + + address previousAdmin = admin; + admin = currentPendingAdmin; + delete pendingAdmin; + + emit NewPendingAdmin(currentPendingAdmin, address(0)); + emit NewAdmin(previousAdmin, currentPendingAdmin); + } + + ///// Getters + + /// @notice return the state transition chain contract for a chainId + function getHyperchain(uint256 _chainId) public view returns (address) { + return IStateTransitionManager(stateTransitionManager[_chainId]).getHyperchain(_chainId); + } + + //// Registry + + /// @notice State Transition can be any contract with the appropriate interface/functionality + function addStateTransitionManager(address _stateTransitionManager) external onlyOwner { + if (_stateTransitionManager == address(0)) { + revert ZeroAddress(); + } + if (stateTransitionManagerIsRegistered[_stateTransitionManager]) { + revert STMAlreadyRegistered(); + } + stateTransitionManagerIsRegistered[_stateTransitionManager] = true; + } + + /// @notice State Transition can be any contract with the appropriate interface/functionality + /// @notice this stops new Chains from using the STF, old chains are not affected + function removeStateTransitionManager(address _stateTransitionManager) external onlyOwner { + if (_stateTransitionManager == address(0)) { + revert ZeroAddress(); + } + if (!stateTransitionManagerIsRegistered[_stateTransitionManager]) { + revert STMNotRegistered(); + } + stateTransitionManagerIsRegistered[_stateTransitionManager] = false; + } + + /// @notice token can be any contract with the appropriate interface/functionality + function addToken(address _token) external onlyOwner { + if (tokenIsRegistered[_token]) { + revert TokenAlreadyRegistered(_token); + } + tokenIsRegistered[_token] = true; + } + + /// @notice To set shared bridge, only Owner. Not done in initialize, as + /// the order of deployment is Bridgehub, Shared bridge, and then we call this + function setSharedBridge(address _sharedBridge) external onlyOwner { + if (_sharedBridge == address(0)) { + revert ZeroAddress(); + } + sharedBridge = IL1SharedBridge(_sharedBridge); + } + + /// @notice register new chain + /// @notice for Eth the baseToken address is 1 + function createNewChain( + uint256 _chainId, + address _stateTransitionManager, + address _baseToken, + // solhint-disable-next-line no-unused-vars + uint256 _salt, + address _admin, + bytes calldata _initData + ) external onlyOwnerOrAdmin nonReentrant whenNotPaused returns (uint256) { + if (_chainId == 0) { + revert ZeroChainId(); + } + if (_chainId > type(uint48).max) { + revert ChainIdTooBig(); + } + if (_stateTransitionManager == address(0)) { + revert ZeroAddress(); + } + if (_baseToken == address(0)) { + revert ZeroAddress(); + } + + if (!stateTransitionManagerIsRegistered[_stateTransitionManager]) { + revert STMNotRegistered(); + } + if (!tokenIsRegistered[_baseToken]) { + revert TokenNotRegistered(_baseToken); + } + if (address(sharedBridge) == address(0)) { + revert SharedBridgeNotSet(); + } + + if (stateTransitionManager[_chainId] != address(0)) { + revert BridgeHubAlreadyRegistered(); + } + + stateTransitionManager[_chainId] = _stateTransitionManager; + baseToken[_chainId] = _baseToken; + + IStateTransitionManager(_stateTransitionManager).createNewChain({ + _chainId: _chainId, + _baseToken: _baseToken, + _sharedBridge: address(sharedBridge), + _admin: _admin, + _diamondCut: _initData + }); + + emit NewChain(_chainId, _stateTransitionManager, _admin); + return _chainId; + } + + //// Mailbox forwarder + + /// @notice forwards function call to Mailbox based on ChainId + /// @param _chainId The chain ID of the hyperchain where to prove L2 message inclusion. + /// @param _batchNumber The executed L2 batch number in which the message appeared + /// @param _index The position in the L2 logs Merkle tree of the l2Log that was sent with the message + /// @param _message Information about the sent message: sender address, the message itself, tx index in the L2 batch where the message was sent + /// @param _proof Merkle proof for inclusion of L2 log that was sent with the message + /// @return Whether the proof is valid + function proveL2MessageInclusion( + uint256 _chainId, + uint256 _batchNumber, + uint256 _index, + L2Message calldata _message, + bytes32[] calldata _proof + ) external view override returns (bool) { + address hyperchain = getHyperchain(_chainId); + return IZkSyncHyperchain(hyperchain).proveL2MessageInclusion(_batchNumber, _index, _message, _proof); + } + + /// @notice forwards function call to Mailbox based on ChainId + /// @param _chainId The chain ID of the hyperchain where to prove L2 log inclusion. + /// @param _batchNumber The executed L2 batch number in which the log appeared + /// @param _index The position of the l2log in the L2 logs Merkle tree + /// @param _log Information about the sent log + /// @param _proof Merkle proof for inclusion of the L2 log + /// @return Whether the proof is correct and L2 log is included in batch + function proveL2LogInclusion( + uint256 _chainId, + uint256 _batchNumber, + uint256 _index, + L2Log calldata _log, + bytes32[] calldata _proof + ) external view override returns (bool) { + address hyperchain = getHyperchain(_chainId); + return IZkSyncHyperchain(hyperchain).proveL2LogInclusion(_batchNumber, _index, _log, _proof); + } + + /// @notice forwards function call to Mailbox based on ChainId + /// @param _chainId The chain ID of the hyperchain where to prove L1->L2 tx status. + /// @param _l2TxHash The L2 canonical transaction hash + /// @param _l2BatchNumber The L2 batch number where the transaction was processed + /// @param _l2MessageIndex The position in the L2 logs Merkle tree of the l2Log that was sent with the message + /// @param _l2TxNumberInBatch The L2 transaction number in the batch, in which the log was sent + /// @param _merkleProof The Merkle proof of the processing L1 -> L2 transaction + /// @param _status The execution status of the L1 -> L2 transaction (true - success & 0 - fail) + /// @return Whether the proof is correct and the transaction was actually executed with provided status + /// NOTE: It may return `false` for incorrect proof, but it doesn't mean that the L1 -> L2 transaction has an opposite status! + function proveL1ToL2TransactionStatus( + uint256 _chainId, + bytes32 _l2TxHash, + uint256 _l2BatchNumber, + uint256 _l2MessageIndex, + uint16 _l2TxNumberInBatch, + bytes32[] calldata _merkleProof, + TxStatus _status + ) external view override returns (bool) { + address hyperchain = getHyperchain(_chainId); + return + IZkSyncHyperchain(hyperchain).proveL1ToL2TransactionStatus({ + _l2TxHash: _l2TxHash, + _l2BatchNumber: _l2BatchNumber, + _l2MessageIndex: _l2MessageIndex, + _l2TxNumberInBatch: _l2TxNumberInBatch, + _merkleProof: _merkleProof, + _status: _status + }); + } + + /// @notice forwards function call to Mailbox based on ChainId + function l2TransactionBaseCost( + uint256 _chainId, + uint256 _gasPrice, + uint256 _l2GasLimit, + uint256 _l2GasPerPubdataByteLimit + ) external view returns (uint256) { + address hyperchain = getHyperchain(_chainId); + return IZkSyncHyperchain(hyperchain).l2TransactionBaseCost(_gasPrice, _l2GasLimit, _l2GasPerPubdataByteLimit); + } + + /// @notice the mailbox is called directly after the sharedBridge received the deposit + /// this assumes that either ether is the base token or + /// the msg.sender has approved mintValue allowance for the sharedBridge. + /// This means this is not ideal for contract calls, as the contract would have to handle token allowance of the base Token + function requestL2TransactionDirect( + L2TransactionRequestDirect calldata _request + ) external payable override nonReentrant whenNotPaused returns (bytes32 canonicalTxHash) { + { + address token = baseToken[_request.chainId]; + if (token == ETH_TOKEN_ADDRESS) { + if (msg.value != _request.mintValue) { + revert MsgValueMismatch(_request.mintValue, msg.value); + } + } else { + if (msg.value != 0) { + revert MsgValueMismatch(0, msg.value); + } + } + + // slither-disable-next-line arbitrary-send-eth + sharedBridge.bridgehubDepositBaseToken{value: msg.value}( + _request.chainId, + msg.sender, + token, + _request.mintValue + ); + } + + address hyperchain = getHyperchain(_request.chainId); + address refundRecipient = AddressAliasHelper.actualRefundRecipient(_request.refundRecipient, msg.sender); + canonicalTxHash = IZkSyncHyperchain(hyperchain).bridgehubRequestL2Transaction( + BridgehubL2TransactionRequest({ + sender: msg.sender, + contractL2: _request.l2Contract, + mintValue: _request.mintValue, + l2Value: _request.l2Value, + l2Calldata: _request.l2Calldata, + l2GasLimit: _request.l2GasLimit, + l2GasPerPubdataByteLimit: _request.l2GasPerPubdataByteLimit, + factoryDeps: _request.factoryDeps, + refundRecipient: refundRecipient + }) + ); + } + + /// @notice After depositing funds to the sharedBridge, the secondBridge is called + /// to return the actual L2 message which is sent to the Mailbox. + /// This assumes that either ether is the base token or + /// the msg.sender has approved the sharedBridge with the mintValue, + /// and also the necessary approvals are given for the second bridge. + /// @notice The logic of this bridge is to allow easy depositing for bridges. + /// Each contract that handles the users ERC20 tokens needs approvals from the user, this contract allows + /// the user to approve for each token only its respective bridge + /// @notice This function is great for contract calls to L2, the secondBridge can be any contract. + function requestL2TransactionTwoBridges( + L2TransactionRequestTwoBridgesOuter calldata _request + ) external payable override nonReentrant whenNotPaused returns (bytes32 canonicalTxHash) { + { + address token = baseToken[_request.chainId]; + uint256 baseTokenMsgValue; + if (token == ETH_TOKEN_ADDRESS) { + if (msg.value != _request.mintValue + _request.secondBridgeValue) { + revert MsgValueMismatch(_request.mintValue + _request.secondBridgeValue, msg.value); + } + baseTokenMsgValue = _request.mintValue; + } else { + if (msg.value != _request.secondBridgeValue) { + revert MsgValueMismatch(_request.secondBridgeValue, msg.value); + } + baseTokenMsgValue = 0; + } + // slither-disable-next-line arbitrary-send-eth + sharedBridge.bridgehubDepositBaseToken{value: baseTokenMsgValue}( + _request.chainId, + msg.sender, + token, + _request.mintValue + ); + } + + address hyperchain = getHyperchain(_request.chainId); + + // slither-disable-next-line arbitrary-send-eth + L2TransactionRequestTwoBridgesInner memory outputRequest = IL1SharedBridge(_request.secondBridgeAddress) + .bridgehubDeposit{value: _request.secondBridgeValue}( + _request.chainId, + msg.sender, + _request.l2Value, + _request.secondBridgeCalldata + ); + + if (outputRequest.magicValue != TWO_BRIDGES_MAGIC_VALUE) { + revert WrongMagicValue(uint256(TWO_BRIDGES_MAGIC_VALUE), uint256(outputRequest.magicValue)); + } + + address refundRecipient = AddressAliasHelper.actualRefundRecipient(_request.refundRecipient, msg.sender); + + if (_request.secondBridgeAddress <= BRIDGEHUB_MIN_SECOND_BRIDGE_ADDRESS) { + revert AddressTooLow(_request.secondBridgeAddress); + } + // to avoid calls to precompiles + canonicalTxHash = IZkSyncHyperchain(hyperchain).bridgehubRequestL2Transaction( + BridgehubL2TransactionRequest({ + sender: _request.secondBridgeAddress, + contractL2: outputRequest.l2Contract, + mintValue: _request.mintValue, + l2Value: _request.l2Value, + l2Calldata: outputRequest.l2Calldata, + l2GasLimit: _request.l2GasLimit, + l2GasPerPubdataByteLimit: _request.l2GasPerPubdataByteLimit, + factoryDeps: outputRequest.factoryDeps, + refundRecipient: refundRecipient + }) + ); + + IL1SharedBridge(_request.secondBridgeAddress).bridgehubConfirmL2Transaction( + _request.chainId, + outputRequest.txDataHash, + canonicalTxHash + ); + } + + /*////////////////////////////////////////////////////////////// + PAUSE + //////////////////////////////////////////////////////////////*/ + + /// @notice Pauses all functions marked with the `whenNotPaused` modifier. + function pause() external onlyOwner { + _pause(); + } + + /// @notice Unpauses the contract, allowing all functions marked with the `whenNotPaused` modifier to be called again. + function unpause() external onlyOwner { + _unpause(); + } +} diff --git a/contracts/l1-contracts/bridgehub/IBridgehub.sol b/contracts/l1-contracts/bridgehub/IBridgehub.sol new file mode 100644 index 0000000..4216a68 --- /dev/null +++ b/contracts/l1-contracts/bridgehub/IBridgehub.sol @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.21; + +import {IL1SharedBridge} from "../bridge/interfaces/IL1SharedBridge.sol"; +import {L2Message, L2Log, TxStatus} from "../common/Messaging.sol"; + +struct L2TransactionRequestDirect { + uint256 chainId; + uint256 mintValue; + address l2Contract; + uint256 l2Value; + bytes l2Calldata; + uint256 l2GasLimit; + uint256 l2GasPerPubdataByteLimit; + bytes[] factoryDeps; + address refundRecipient; +} + +struct L2TransactionRequestTwoBridgesOuter { + uint256 chainId; + uint256 mintValue; + uint256 l2Value; + uint256 l2GasLimit; + uint256 l2GasPerPubdataByteLimit; + address refundRecipient; + address secondBridgeAddress; + uint256 secondBridgeValue; + bytes secondBridgeCalldata; +} + +struct L2TransactionRequestTwoBridgesInner { + bytes32 magicValue; + address l2Contract; + bytes l2Calldata; + bytes[] factoryDeps; + bytes32 txDataHash; +} + +interface IBridgehub { + /// @notice pendingAdmin is changed + /// @dev Also emitted when new admin is accepted and in this case, `newPendingAdmin` would be zero address + event NewPendingAdmin(address indexed oldPendingAdmin, address indexed newPendingAdmin); + + /// @notice Admin changed + event NewAdmin(address indexed oldAdmin, address indexed newAdmin); + + /// @notice Starts the transfer of admin rights. Only the current admin can propose a new pending one. + /// @notice New admin can accept admin rights by calling `acceptAdmin` function. + /// @param _newPendingAdmin Address of the new admin + function setPendingAdmin(address _newPendingAdmin) external; + + /// @notice Accepts transfer of admin rights. Only pending admin can accept the role. + function acceptAdmin() external; + + /// Getters + function stateTransitionManagerIsRegistered(address _stateTransitionManager) external view returns (bool); + + function stateTransitionManager(uint256 _chainId) external view returns (address); + + function tokenIsRegistered(address _baseToken) external view returns (bool); + + function baseToken(uint256 _chainId) external view returns (address); + + function sharedBridge() external view returns (IL1SharedBridge); + + function getHyperchain(uint256 _chainId) external view returns (address); + + /// Mailbox forwarder + + function proveL2MessageInclusion( + uint256 _chainId, + uint256 _batchNumber, + uint256 _index, + L2Message calldata _message, + bytes32[] calldata _proof + ) external view returns (bool); + + function proveL2LogInclusion( + uint256 _chainId, + uint256 _batchNumber, + uint256 _index, + L2Log memory _log, + bytes32[] calldata _proof + ) external view returns (bool); + + function proveL1ToL2TransactionStatus( + uint256 _chainId, + bytes32 _l2TxHash, + uint256 _l2BatchNumber, + uint256 _l2MessageIndex, + uint16 _l2TxNumberInBatch, + bytes32[] calldata _merkleProof, + TxStatus _status + ) external view returns (bool); + + function requestL2TransactionDirect( + L2TransactionRequestDirect calldata _request + ) external payable returns (bytes32 canonicalTxHash); + + function requestL2TransactionTwoBridges( + L2TransactionRequestTwoBridgesOuter calldata _request + ) external payable returns (bytes32 canonicalTxHash); + + function l2TransactionBaseCost( + uint256 _chainId, + uint256 _gasPrice, + uint256 _l2GasLimit, + uint256 _l2GasPerPubdataByteLimit + ) external view returns (uint256); + + //// Registry + + function createNewChain( + uint256 _chainId, + address _stateTransitionManager, + address _baseToken, + uint256 _salt, + address _admin, + bytes calldata _initData + ) external returns (uint256 chainId); + + function addStateTransitionManager(address _stateTransitionManager) external; + + function removeStateTransitionManager(address _stateTransitionManager) external; + + function addToken(address _token) external; + + function setSharedBridge(address _sharedBridge) external; + + event NewChain(uint256 indexed chainId, address stateTransitionManager, address indexed chainGovernance); +} diff --git a/contracts/l1-contracts/common/Config.sol b/contracts/l1-contracts/common/Config.sol new file mode 100644 index 0000000..c0e05f1 --- /dev/null +++ b/contracts/l1-contracts/common/Config.sol @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.21; + +/// @dev `keccak256("")` +bytes32 constant EMPTY_STRING_KECCAK = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470; + +/// @dev Bytes in raw L2 log +/// @dev Equal to the bytes size of the tuple - (uint8 ShardId, bool isService, uint16 txNumberInBatch, address sender, +/// bytes32 key, bytes32 value) +uint256 constant L2_TO_L1_LOG_SERIALIZE_SIZE = 88; + +/// @dev The maximum length of the bytes array with L2 -> L1 logs +uint256 constant MAX_L2_TO_L1_LOGS_COMMITMENT_BYTES = 4 + L2_TO_L1_LOG_SERIALIZE_SIZE * 512; + +/// @dev The value of default leaf hash for L2 -> L1 logs Merkle tree +/// @dev An incomplete fixed-size tree is filled with this value to be a full binary tree +/// @dev Actually equal to the `keccak256(new bytes(L2_TO_L1_LOG_SERIALIZE_SIZE))` +bytes32 constant L2_L1_LOGS_TREE_DEFAULT_LEAF_HASH = 0x72abee45b59e344af8a6e520241c4744aff26ed411f4c4b00f8af09adada43ba; + +// TODO: change constant to the real root hash of empty Merkle tree (SMA-184) +bytes32 constant DEFAULT_L2_LOGS_TREE_ROOT_HASH = bytes32(0); + +/// @dev Denotes the type of the ZKsync transaction that came from L1. +uint256 constant PRIORITY_OPERATION_L2_TX_TYPE = 255; + +/// @dev Denotes the type of the ZKsync transaction that is used for system upgrades. +uint256 constant SYSTEM_UPGRADE_L2_TX_TYPE = 254; + +/// @dev The maximal allowed difference between protocol minor versions in an upgrade. The 100 gap is needed +/// in case a protocol version has been tested on testnet, but then not launched on mainnet, e.g. +/// due to a bug found. +/// We are allowed to jump at most 100 minor versions at a time. The major version is always expected to be 0. +uint256 constant MAX_ALLOWED_MINOR_VERSION_DELTA = 100; + +/// @dev The amount of time in seconds the validator has to process the priority transaction +/// NOTE: The constant is set to zero for the Alpha release period +uint256 constant PRIORITY_EXPIRATION = 0 days; + +/// @dev Timestamp - seconds since unix epoch. +uint256 constant COMMIT_TIMESTAMP_NOT_OLDER = 3 days; + +/// @dev Maximum available error between real commit batch timestamp and analog used in the verifier (in seconds) +/// @dev Must be used cause miner's `block.timestamp` value can differ on some small value (as we know - 12 seconds) +uint256 constant COMMIT_TIMESTAMP_APPROXIMATION_DELTA = 1 hours; + +/// @dev Shift to apply to verify public input before verifying. +uint256 constant PUBLIC_INPUT_SHIFT = 32; + +/// @dev The maximum number of L2 gas that a user can request for an L2 transaction +uint256 constant MAX_GAS_PER_TRANSACTION = 80_000_000; + +/// @dev Even though the price for 1 byte of pubdata is 16 L1 gas, we have a slightly increased +/// value. +uint256 constant L1_GAS_PER_PUBDATA_BYTE = 17; + +/// @dev The intrinsic cost of the L1->l2 transaction in computational L2 gas +uint256 constant L1_TX_INTRINSIC_L2_GAS = 167_157; + +/// @dev The intrinsic cost of the L1->l2 transaction in pubdata +uint256 constant L1_TX_INTRINSIC_PUBDATA = 88; + +/// @dev The minimal base price for L1 transaction +uint256 constant L1_TX_MIN_L2_GAS_BASE = 173_484; + +/// @dev The number of L2 gas the transaction starts costing more with each 544 bytes of encoding +uint256 constant L1_TX_DELTA_544_ENCODING_BYTES = 1656; + +/// @dev The number of L2 gas an L1->L2 transaction gains with each new factory dependency +uint256 constant L1_TX_DELTA_FACTORY_DEPS_L2_GAS = 2473; + +/// @dev The number of L2 gas an L1->L2 transaction gains with each new factory dependency +uint256 constant L1_TX_DELTA_FACTORY_DEPS_PUBDATA = 64; + +/// @dev The number of pubdata an L1->L2 transaction requires with each new factory dependency +uint256 constant MAX_NEW_FACTORY_DEPS = 32; + +/// @dev The L2 gasPricePerPubdata required to be used in bridges. +uint256 constant REQUIRED_L2_GAS_PRICE_PER_PUBDATA = 800; + +/// @dev The mask which should be applied to the packed batch and L2 block timestamp in order +/// to obtain the L2 block timestamp. Applying this mask is equivalent to calculating modulo 2**128 +uint256 constant PACKED_L2_BLOCK_TIMESTAMP_MASK = 0xffffffffffffffffffffffffffffffff; + +/// @dev Address of the point evaluation precompile used for EIP-4844 blob verification. +address constant POINT_EVALUATION_PRECOMPILE_ADDR = address(0x0A); + +/// @dev The overhead for a transaction slot in L2 gas. +/// It is roughly equal to 80kk/MAX_TRANSACTIONS_IN_BATCH, i.e. how many gas would an L1->L2 transaction +/// need to pay to compensate for the batch being closed. +/// @dev It is expected that the L1 contracts will enforce that the L2 gas price will be high enough to compensate +/// the operator in case the batch is closed because of tx slots filling up. +uint256 constant TX_SLOT_OVERHEAD_L2_GAS = 10000; + +/// @dev The overhead for each byte of the bootloader memory that the encoding of the transaction. +/// It is roughly equal to 80kk/BOOTLOADER_MEMORY_FOR_TXS, i.e. how many gas would an L1->L2 transaction +/// need to pay to compensate for the batch being closed. +/// @dev It is expected that the L1 contracts will enforce that the L2 gas price will be high enough to compensate +/// the operator in case the batch is closed because of the memory for transactions being filled up. +uint256 constant MEMORY_OVERHEAD_GAS = 10; + +/// @dev The maximum gas limit for a priority transaction in L2. +uint256 constant PRIORITY_TX_MAX_GAS_LIMIT = 72_000_000; + +address constant ETH_TOKEN_ADDRESS = address(1); + +bytes32 constant TWO_BRIDGES_MAGIC_VALUE = bytes32(uint256(keccak256("TWO_BRIDGES_MAGIC_VALUE")) - 1); + +/// @dev https://eips.ethereum.org/EIPS/eip-1352 +address constant BRIDGEHUB_MIN_SECOND_BRIDGE_ADDRESS = address(uint160(type(uint16).max)); diff --git a/contracts/l1-contracts/common/Dependencies.sol b/contracts/l1-contracts/common/Dependencies.sol new file mode 100644 index 0000000..fceaa77 --- /dev/null +++ b/contracts/l1-contracts/common/Dependencies.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.21; + +/* solhint-disable-next-line no-unused-import */ +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts-v4/proxy/transparent/TransparentUpgradeableProxy.sol"; +/* solhint-disable-next-line no-unused-import */ +import {ProxyAdmin} from "@openzeppelin/contracts-v4/proxy/transparent/ProxyAdmin.sol"; diff --git a/contracts/l1-contracts/common/L1ContractErrors.sol b/contracts/l1-contracts/common/L1ContractErrors.sol new file mode 100644 index 0000000..73ff72c --- /dev/null +++ b/contracts/l1-contracts/common/L1ContractErrors.sol @@ -0,0 +1,337 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +// 0x1ff9d522 +error AddressAlreadyUsed(address addr); +// 0x86bb51b8 +error AddressHasNoCode(address); +// 0x1eee5481 +error AddressTooLow(address); +// 0x6afd6c20 +error BadReturnData(); +// 0x6ef9a972 +error BaseTokenGasPriceDenominatorNotSet(); +// 0x55ad3fd3 +error BatchHashMismatch(bytes32 expected, bytes32 actual); +// 0x2078a6a0 +error BatchNotExecuted(uint256 batchNumber); +// 0xbd4455ff +error BatchNumberMismatch(uint256 expectedBatchNumber, uint256 providedBatchNumber); +// 0xafd53e2f +error BlobHashCommitmentError(uint256 index, bool blobHashEmpty, bool blobCommitmentEmpty); +// 0x6cf12312 +error BridgeHubAlreadyRegistered(); +// 0xcf102c5a +error CalldataLengthTooBig(); +// 0xe85392f9 +error CanOnlyProcessOneBatch(); +// 0x00c6ead2 +error CantExecuteUnprovenBatches(); +// 0xe18cb383 +error CantRevertExecutedBatch(); +// 0x78d2ed02 +error ChainAlreadyLive(); +// 0x8f620a06 +error ChainIdTooBig(); +// 0xf7a01e4d +error DelegateCallFailed(bytes returnData); +// 0x0a8ed92c +error DenominatorIsZero(); +// 0xc7c9660f +error DepositDoesNotExist(); +// 0xad2fa98e +error DepositExists(); +// 0x79cacff1 +error DepositFailed(); +// 0xae08e4af +error DepositIncorrectAmount(uint256 expectedAmt, uint256 providedAmt); +// 0x0e7ee319 +error DiamondAlreadyFrozen(); +// 0x682dabb4 +error DiamondFreezeIncorrectState(); +// 0xa7151b9a +error DiamondNotFrozen(); +// 0xfc7ab1d3 +error EmptyBlobVersionHash(uint256 index); +// 0x95b66fe9 +error EmptyDeposit(); +// 0xac4a3f98 +error FacetExists(bytes4 selector, address); +// 0x79e12cc3 +error FacetIsFrozen(bytes4 func); +// 0xc91cf3b1 +error GasPerPubdataMismatch(); +// 0x6d4a7df8 +error GenesisBatchCommitmentZero(); +// 0x7940c83f +error GenesisBatchHashZero(); +// 0xb4fc6835 +error GenesisIndexStorageZero(); +// 0x3a1a8589 +error GenesisUpgradeZero(); +// 0xd356e6ba +error HashedLogIsDefault(); +// 0x0b08d5be +error HashMismatch(bytes32 expected, bytes32 actual); +// 0xb615c2b1 +error HyperchainLimitReached(); +// 0x826fb11e +error InsufficientChainBalance(); +// 0x356680b7 +error InsufficientFunds(); +// 0x7a47c9a2 +error InvalidChainId(); +// 0x4fbe5dba +error InvalidDelay(); +// 0x0af806e0 +error InvalidHash(); +// 0xc1780bd6 +error InvalidLogSender(address sender, uint256 logKey); +// 0xd8e9405c +error InvalidNumberOfBlobs(uint256 expected, uint256 numCommitments, uint256 numHashes); +// 0x09bde339 +error InvalidProof(); +// 0x5428eae7 +error InvalidProtocolVersion(); +// 0x53e6d04d +error InvalidPubdataCommitmentsSize(); +// 0x5513177c +error InvalidPubdataHash(bytes32 expectedHash, bytes32 provided); +// 0x9094af7e +error InvalidPubdataLength(); +// 0xc5d09071 +error InvalidPubdataMode(); +// 0x6f1cf752 +error InvalidPubdataPricingMode(); +// 0x12ba286f +error InvalidSelector(bytes4 func); +// 0x5cb29523 +error InvalidTxType(uint256 txType); +// 0x5f1aa154 +error InvalidUpgradeTxn(UpgradeTxVerifyParam); +// 0xaa7feadc +error InvalidValue(); +// 0xa4f62e33 +error L2BridgeNotDeployed(uint256 chainId); +// 0xff8811ff +error L2BridgeNotSet(uint256 chainId); +// 0xcb5e4247 +error L2BytecodeHashMismatch(bytes32 expected, bytes32 provided); +// 0xfb5c22e6 +error L2TimestampTooBig(); +// 0xd2c011d6 +error L2UpgradeNonceNotEqualToNewProtocolVersion(uint256 nonce, uint256 protocolVersion); +// 0x97e1359e +error L2WithdrawalMessageWrongLength(uint256 messageLen); +// 0x32eb8b2f +error LegacyMethodIsSupportedOnlyForEra(); +// 0xe37d2c02 +error LengthIsNotDivisibleBy32(uint256 length); +// 0x1b6825bb +error LogAlreadyProcessed(uint8); +// 0x43e266b0 +error MalformedBytecode(BytecodeError); +// 0x59170bf0 +error MalformedCalldata(); +// 0x16509b9a +error MalformedMessage(); +// 0x9bb54c35 +error MerkleIndexOutOfBounds(); +// 0x8e23ac1a +error MerklePathEmpty(); +// 0x1c500385 +error MerklePathOutOfBounds(); +// 0xfa44b527 +error MissingSystemLogs(uint256 expected, uint256 actual); +// 0x4a094431 +error MsgValueMismatch(uint256 expectedMsgValue, uint256 providedMsgValue); +// 0xb385a3da +error MsgValueTooLow(uint256 required, uint256 provided); +// 0x72ea85ad +error NewProtocolMajorVersionNotZero(); +// 0x79cc2d22 +error NoCallsProvided(); +// 0xa6fef710 +error NoFunctionsForDiamondCut(); +// 0xcab098d8 +error NoFundsTransferred(); +// 0x92290acc +error NonEmptyBlobVersionHash(uint256 index); +// 0xc21b1ab7 +error NonEmptyCalldata(); +// 0x536ec84b +error NonEmptyMsgValue(); +// 0xd018e08e +error NonIncreasingTimestamp(); +// 0x0105f9c0 +error NonSequentialBatch(); +// 0x4ef79e5a +error NonZeroAddress(address); +// 0xdd629f86 +error NotEnoughGas(); +// 0xdd7e3621 +error NotInitializedReentrancyGuard(); +// 0xf3ed9dfa +error OnlyEraSupported(); +// 0x1a21feed +error OperationExists(); +// 0xeda2fbb1 +error OperationMustBePending(); +// 0xe1c1ff37 +error OperationMustBeReady(); +// 0xd7f50a9d +error PatchCantSetUpgradeTxn(); +// 0x962fd7d0 +error PatchUpgradeCantSetBootloader(); +// 0x559cc34e +error PatchUpgradeCantSetDefaultAccount(); +// 0x8d5851de +error PointEvalCallFailed(bytes); +// 0x4daa985d +error PointEvalFailed(bytes); +// 0x9b48e060 +error PreviousOperationNotExecuted(); +// 0x5c598b60 +error PreviousProtocolMajorVersionNotZero(); +// 0xa0f47245 +error PreviousUpgradeNotCleaned(); +// 0x101ba748 +error PreviousUpgradeNotFinalized(bytes32 txHash); +// 0xd5a99014 +error PriorityOperationsRollingHashMismatch(); +// 0x1a4d284a +error PriorityTxPubdataExceedsMaxPubDataPerBatch(); +// 0xa461f651 +error ProtocolIdMismatch(uint256 expectedProtocolVersion, uint256 providedProtocolId); +// 0x64f94ec2 +error ProtocolIdNotGreater(); +// 0xd328c12a +error ProtocolVersionMinorDeltaTooBig(uint256 limit, uint256 proposed); +// 0x88d7b498 +error ProtocolVersionTooSmall(); +// 0x53dee67b +error PubdataCommitmentsEmpty(); +// 0x7734c31a +error PubdataCommitmentsTooBig(); +// 0x959f26fb +error PubdataGreaterThanLimit(uint256 limit, uint256 length); +// 0x2a4a14df +error PubdataPerBatchIsLessThanTxn(); +// 0x63c36549 +error QueueIsEmpty(); +// 0xab143c06 +error Reentrancy(); +// 0x667d17de +error RemoveFunctionFacetAddressNotZero(address facet); +// 0xa2d4b16c +error RemoveFunctionFacetAddressZero(); +// 0x3580370c +error ReplaceFunctionFacetAddressZero(); +// 0xdab52f4b +error RevertedBatchBeforeNewBatch(); +// 0x9a67c1cb +error RevertedBatchNotAfterNewLastBatch(); +// 0xd3b6535b +error SelectorsMustAllHaveSameFreezability(); +// 0x7774d2f9 +error SharedBridgeValueNotSet(SharedBridgeKey); +// 0xc1d9246c +error SharedBridgeBalanceMismatch(); +// 0x856d5b77 +error SharedBridgeNotSet(); +// 0xcac5fc40 +error SharedBridgeValueAlreadySet(SharedBridgeKey); +// 0xdf3a8fdd +error SlotOccupied(); +// 0xd0bc70cf +error STMAlreadyRegistered(); +// 0x09865e10 +error STMNotRegistered(); +// 0xae43b424 +error SystemLogsSizeTooBig(); +// 0x08753982 +error TimeNotReached(uint256 expectedTimestamp, uint256 actualTimestamp); +// 0x2d50c33b +error TimestampError(); +// 0x4f4b634e +error TokenAlreadyRegistered(address token); +// 0xddef98d7 +error TokenNotRegistered(address token); +// 0x06439c6b +error TokenNotSupported(address token); +// 0x23830e28 +error TokensWithFeesNotSupported(); +// 0xf640f0e5 +error TooManyBlobs(); +// 0x76da24b9 +error TooManyFactoryDeps(); +// 0xf0b4e88f +error TooMuchGas(); +// 0x00c5a6a9 +error TransactionNotAllowed(); +// 0x4c991078 +error TxHashMismatch(); +// 0x2e311df8 +error TxnBodyGasLimitNotEnoughGas(); +// 0x8e4a23d6 +error Unauthorized(address caller); +// 0xe52478c7 +error UndefinedDiamondCutAction(); +// 0x07218375 +error UnexpectedNumberOfFactoryDeps(); +// 0x6aa39880 +error UnexpectedSystemLog(uint256 logKey); +// 0xf093c2e5 +error UpgradeBatchNumberIsNotZero(); +// 0x47b3b145 +error ValidateTxnNotEnoughGas(); +// 0x626ade30 +error ValueMismatch(uint256 expected, uint256 actual); +// 0xe1022469 +error VerifiedBatchesExceedsCommittedBatches(); +// 0x2dbdba00 +error VerifyProofCommittedVerifiedMismatch(); +// 0xae899454 +error WithdrawalAlreadyFinalized(); +// 0x27fcd9d1 +error WithdrawalFailed(); +// 0x750b219c +error WithdrawFailed(); +// 0x15e8e429 +error WrongMagicValue(uint256 expectedMagicValue, uint256 providedMagicValue); +// 0xd92e233d +error ZeroAddress(); +// 0x669567ea +error ZeroBalance(); +// 0xc84885d4 +error ZeroChainId(); + +enum SharedBridgeKey { + PostUpgradeFirstBatch, + LegacyBridgeFirstBatch, + LegacyBridgeLastDepositBatch, + LegacyBridgeLastDepositTxn +} + +enum BytecodeError { + Version, + NumberOfWords, + Length, + WordsMustBeOdd +} + +enum UpgradeTxVerifyParam { + From, + To, + Paymaster, + Value, + MaxFeePerGas, + MaxPriorityFeePerGas, + Reserved0, + Reserved1, + Reserved2, + Reserved3, + Signature, + PaymasterInput, + ReservedDynamic +} diff --git a/contracts/l1-contracts/common/L2ContractAddresses.sol b/contracts/l1-contracts/common/L2ContractAddresses.sol new file mode 100644 index 0000000..571ef3d --- /dev/null +++ b/contracts/l1-contracts/common/L2ContractAddresses.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.21; + +/// @dev The formal address of the initial program of the system: the bootloader +address constant L2_BOOTLOADER_ADDRESS = address(0x8001); + +/// @dev The address of the known code storage system contract +address constant L2_KNOWN_CODE_STORAGE_SYSTEM_CONTRACT_ADDR = address(0x8004); + +/// @dev The address of the L2 deployer system contract. +address constant L2_DEPLOYER_SYSTEM_CONTRACT_ADDR = address(0x8006); + +/// @dev The special reserved L2 address. It is located in the system contracts space but doesn't have deployed +/// bytecode. +/// @dev The L2 deployer system contract allows changing bytecodes on any address if the `msg.sender` is this address. +/// @dev So, whenever the governor wants to redeploy system contracts, it just initiates the L1 upgrade call deployer +/// system contract +/// via the L1 -> L2 transaction with `sender == L2_FORCE_DEPLOYER_ADDR`. For more details see the +/// `diamond-initializers` contracts. +address constant L2_FORCE_DEPLOYER_ADDR = address(0x8007); + +/// @dev The address of the special smart contract that can send arbitrary length message as an L2 log +address constant L2_TO_L1_MESSENGER_SYSTEM_CONTRACT_ADDR = address(0x8008); + +/// @dev The address of the eth token system contract +address constant L2_BASE_TOKEN_SYSTEM_CONTRACT_ADDR = address(0x800a); + +/// @dev The address of the context system contract +address constant L2_SYSTEM_CONTEXT_SYSTEM_CONTRACT_ADDR = address(0x800b); + +/// @dev The address of the pubdata chunk publisher contract +address constant L2_PUBDATA_CHUNK_PUBLISHER_ADDR = address(0x8011); diff --git a/contracts/l1-contracts/common/Messaging.sol b/contracts/l1-contracts/common/Messaging.sol new file mode 100644 index 0000000..a7a2db9 --- /dev/null +++ b/contracts/l1-contracts/common/Messaging.sol @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.21; + +/// @dev The enum that represents the transaction execution status +/// @param Failure The transaction execution failed +/// @param Success The transaction execution succeeded +enum TxStatus { + Failure, + Success +} + +/// @dev The log passed from L2 +/// @param l2ShardId The shard identifier, 0 - rollup, 1 - porter +/// All other values are not used but are reserved for the future +/// @param isService A boolean flag that is part of the log along with `key`, `value`, and `sender` address. +/// This field is required formally but does not have any special meaning +/// @param txNumberInBatch The L2 transaction number in a Batch, in which the log was sent +/// @param sender The L2 address which sent the log +/// @param key The 32 bytes of information that was sent in the log +/// @param value The 32 bytes of information that was sent in the log +// Both `key` and `value` are arbitrary 32-bytes selected by the log sender +struct L2Log { + uint8 l2ShardId; + bool isService; + uint16 txNumberInBatch; + address sender; + bytes32 key; + bytes32 value; +} + +/// @dev An arbitrary length message passed from L2 +/// @notice Under the hood it is `L2Log` sent from the special system L2 contract +/// @param txNumberInBatch The L2 transaction number in a Batch, in which the message was sent +/// @param sender The address of the L2 account from which the message was passed +/// @param data An arbitrary length message +struct L2Message { + uint16 txNumberInBatch; + address sender; + bytes data; +} + +/// @dev Internal structure that contains the parameters for the writePriorityOp +/// internal function. +/// @param txId The id of the priority transaction. +/// @param l2GasPrice The gas price for the l2 priority operation. +/// @param expirationTimestamp The timestamp by which the priority operation must be processed by the operator. +/// @param request The external calldata request for the priority operation. +struct WritePriorityOpParams { + uint256 txId; + uint256 l2GasPrice; + uint64 expirationTimestamp; + BridgehubL2TransactionRequest request; +} + +/// @dev Structure that includes all fields of the L2 transaction +/// @dev The hash of this structure is the "canonical L2 transaction hash" and can +/// be used as a unique identifier of a tx +/// @param txType The tx type number, depending on which the L2 transaction can be +/// interpreted differently +/// @param from The sender's address. `uint256` type for possible address format changes +/// and maintaining backward compatibility +/// @param to The recipient's address. `uint256` type for possible address format changes +/// and maintaining backward compatibility +/// @param gasLimit The L2 gas limit for L2 transaction. Analog to the `gasLimit` on an +/// L1 transactions +/// @param gasPerPubdataByteLimit Maximum number of L2 gas that will cost one byte of pubdata +/// (every piece of data that will be stored on L1 as calldata) +/// @param maxFeePerGas The absolute maximum sender willing to pay per unit of L2 gas to get +/// the transaction included in a Batch. Analog to the EIP-1559 `maxFeePerGas` on an L1 transactions +/// @param maxPriorityFeePerGas The additional fee that is paid directly to the validator +/// to incentivize them to include the transaction in a Batch. Analog to the EIP-1559 +/// `maxPriorityFeePerGas` on an L1 transactions +/// @param paymaster The address of the EIP-4337 paymaster, that will pay fees for the +/// transaction. `uint256` type for possible address format changes and maintaining backward compatibility +/// @param nonce The nonce of the transaction. For L1->L2 transactions it is the priority +/// operation Id +/// @param value The value to pass with the transaction +/// @param reserved The fixed-length fields for usage in a future extension of transaction +/// formats +/// @param data The calldata that is transmitted for the transaction call +/// @param signature An abstract set of bytes that are used for transaction authorization +/// @param factoryDeps The set of L2 bytecode hashes whose preimages were shown on L1 +/// @param paymasterInput The arbitrary-length data that is used as a calldata to the paymaster pre-call +/// @param reservedDynamic The arbitrary-length field for usage in a future extension of transaction formats +struct L2CanonicalTransaction { + uint256 txType; + uint256 from; + uint256 to; + uint256 gasLimit; + uint256 gasPerPubdataByteLimit; + uint256 maxFeePerGas; + uint256 maxPriorityFeePerGas; + uint256 paymaster; + uint256 nonce; + uint256 value; + // In the future, we might want to add some + // new fields to the struct. The `txData` struct + // is to be passed to account and any changes to its structure + // would mean a breaking change to these accounts. To prevent this, + // we should keep some fields as "reserved" + // It is also recommended that their length is fixed, since + // it would allow easier proof integration (in case we will need + // some special circuit for preprocessing transactions) + uint256[4] reserved; + bytes data; + bytes signature; + uint256[] factoryDeps; + bytes paymasterInput; + // Reserved dynamic type for the future use-case. Using it should be avoided, + // But it is still here, just in case we want to enable some additional functionality + bytes reservedDynamic; +} + +/// @param sender The sender's address. +/// @param contractAddressL2 The address of the contract on L2 to call. +/// @param valueToMint The amount of base token that should be minted on L2 as the result of this transaction. +/// @param l2Value The msg.value of the L2 transaction. +/// @param l2Calldata The calldata for the L2 transaction. +/// @param l2GasLimit The limit of the L2 gas for the L2 transaction +/// @param l2GasPerPubdataByteLimit The price for a single pubdata byte in L2 gas. +/// @param factoryDeps The array of L2 bytecodes that the tx depends on. +/// @param refundRecipient The recipient of the refund for the transaction on L2. If the transaction fails, then +/// this address will receive the `l2Value`. +// solhint-disable-next-line gas-struct-packing +struct BridgehubL2TransactionRequest { + address sender; + address contractL2; + uint256 mintValue; + uint256 l2Value; + bytes l2Calldata; + uint256 l2GasLimit; + uint256 l2GasPerPubdataByteLimit; + bytes[] factoryDeps; + address refundRecipient; +} diff --git a/contracts/l1-contracts/common/ReentrancyGuard.sol b/contracts/l1-contracts/common/ReentrancyGuard.sol new file mode 100644 index 0000000..b1f8e55 --- /dev/null +++ b/contracts/l1-contracts/common/ReentrancyGuard.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.21; + +import {SlotOccupied, NotInitializedReentrancyGuard, Reentrancy} from "./L1ContractErrors.sol"; + +/** + * @custom:security-contact security@matterlabs.dev + * @dev Contract module that helps prevent reentrant calls to a function. + * + * Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier + * available, which can be applied to functions to make sure there are no nested + * (reentrant) calls to them. + * + * Note that because there is a single `nonReentrant` guard, functions marked as + * `nonReentrant` may not call one another. This can be worked around by making + * those functions `private`, and then adding `external` `nonReentrant` entry + * points to them. + * + * TIP: If you would like to learn more about reentrancy and alternative ways + * to protect against it, check out our blog post + * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul]. + * + * _Since v2.5.0:_ this module is now much more gas efficient, given net gas + * metering changes introduced in the Istanbul hardfork. + */ +abstract contract ReentrancyGuard { + /// @dev Address of lock flag variable. + /// @dev Flag is placed at random memory location to not interfere with Storage contract. + // keccak256("ReentrancyGuard") - 1; + uint256 private constant LOCK_FLAG_ADDRESS = 0x8e94fed44239eb2314ab7a406345e6c5a8f0ccedf3b600de3d004e672c33abf4; + + // solhint-disable-next-line max-line-length + // https://github.com/OpenZeppelin/openzeppelin-contracts/blob/566a774222707e424896c0c390a84dc3c13bdcb2/contracts/security/ReentrancyGuard.sol + // The values being non-zero value makes deployment a bit more expensive, + // but in exchange the refund on every call to nonReentrant will be lower in + // amount. Since refunds are capped to a percentage of the total + // transaction's gas, it is best to keep them low in cases like this one, to + // increase the likelihood of the full refund coming into effect. + uint256 private constant _NOT_ENTERED = 1; + uint256 private constant _ENTERED = 2; + + modifier reentrancyGuardInitializer() { + _initializeReentrancyGuard(); + _; + } + + function _initializeReentrancyGuard() private { + uint256 lockSlotOldValue; + + // Storing an initial non-zero value makes deployment a bit more + // expensive but in exchange every call to nonReentrant + // will be cheaper. + assembly { + lockSlotOldValue := sload(LOCK_FLAG_ADDRESS) + sstore(LOCK_FLAG_ADDRESS, _NOT_ENTERED) + } + + // Check that storage slot for reentrancy guard is empty to rule out possibility of slot conflict + if (lockSlotOldValue != 0) { + revert SlotOccupied(); + } + } + + /** + * @dev Prevents a contract from calling itself, directly or indirectly. + * Calling a `nonReentrant` function from another `nonReentrant` + * function is not supported. It is possible to prevent this from happening + * by making the `nonReentrant` function external, and make it call a + * `private` function that does the actual work. + */ + modifier nonReentrant() { + uint256 _status; + assembly { + _status := sload(LOCK_FLAG_ADDRESS) + } + + if (_status == 0) { + revert NotInitializedReentrancyGuard(); + } + // On the first call to nonReentrant, _NOT_ENTERED will be true + if (_status != _NOT_ENTERED) { + revert Reentrancy(); + } + + // Any calls to nonReentrant after this point will fail + assembly { + sstore(LOCK_FLAG_ADDRESS, _ENTERED) + } + + _; + + // By storing the original value once again, a refund is triggered (see + // https://eips.ethereum.org/EIPS/eip-2200) + assembly { + sstore(LOCK_FLAG_ADDRESS, _NOT_ENTERED) + } + } +} diff --git a/contracts/l1-contracts/common/interfaces/IL2ContractDeployer.sol b/contracts/l1-contracts/common/interfaces/IL2ContractDeployer.sol new file mode 100644 index 0000000..3d5b597 --- /dev/null +++ b/contracts/l1-contracts/common/interfaces/IL2ContractDeployer.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.21; + +/** + * @author Matter Labs + * @notice System smart contract that is responsible for deploying other smart contracts on a ZKsync hyperchain. + */ +interface IL2ContractDeployer { + /// @notice A struct that describes a forced deployment on an address. + /// @param bytecodeHash The bytecode hash to put on an address. + /// @param newAddress The address on which to deploy the bytecodehash to. + /// @param callConstructor Whether to run the constructor on the force deployment. + /// @param value The `msg.value` with which to initialize a contract. + /// @param input The constructor calldata. + struct ForceDeployment { + bytes32 bytecodeHash; + address newAddress; + bool callConstructor; + uint256 value; + bytes input; + } + + /// @notice This method is to be used only during an upgrade to set bytecodes on specific addresses. + function forceDeployOnAddresses(ForceDeployment[] calldata _deployParams) external; + + /// @notice Deploys a contract with similar address derivation rules to the EVM's `CREATE2` opcode. + /// @param _salt The create2 salt. + /// @param _bytecodeHash The correctly formatted hash of the bytecode. + /// @param _input The constructor calldata. + function create2(bytes32 _salt, bytes32 _bytecodeHash, bytes calldata _input) external; +} diff --git a/contracts/l1-contracts/common/libraries/L2ContractHelper.sol b/contracts/l1-contracts/common/libraries/L2ContractHelper.sol new file mode 100644 index 0000000..21199f0 --- /dev/null +++ b/contracts/l1-contracts/common/libraries/L2ContractHelper.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.21; + +import {BytecodeError, MalformedBytecode, LengthIsNotDivisibleBy32} from "../L1ContractErrors.sol"; + +/** + * @author Matter Labs + * @custom:security-contact security@matterlabs.dev + * @notice Helper library for working with L2 contracts on L1. + */ +library L2ContractHelper { + /// @dev The prefix used to create CREATE2 addresses. + bytes32 private constant CREATE2_PREFIX = keccak256("zksyncCreate2"); + + /// @notice Validate the bytecode format and calculate its hash. + /// @param _bytecode The bytecode to hash. + /// @return hashedBytecode The 32-byte hash of the bytecode. + /// Note: The function reverts the execution if the bytecode has non expected format: + /// - Bytecode bytes length is not a multiple of 32 + /// - Bytecode bytes length is not less than 2^21 bytes (2^16 words) + /// - Bytecode words length is not odd + function hashL2Bytecode(bytes memory _bytecode) internal pure returns (bytes32 hashedBytecode) { + // Note that the length of the bytecode must be provided in 32-byte words. + if (_bytecode.length % 32 != 0) { + revert LengthIsNotDivisibleBy32(_bytecode.length); + } + + uint256 bytecodeLenInWords = _bytecode.length / 32; + // bytecode length must be less than 2^16 words + if (bytecodeLenInWords >= 2 ** 16) { + revert MalformedBytecode(BytecodeError.NumberOfWords); + } + // bytecode length in words must be odd + if (bytecodeLenInWords % 2 == 0) { + revert MalformedBytecode(BytecodeError.WordsMustBeOdd); + } + hashedBytecode = sha256(_bytecode) & 0x00000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; + // Setting the version of the hash + hashedBytecode = (hashedBytecode | bytes32(uint256(1 << 248))); + // Setting the length + hashedBytecode = hashedBytecode | bytes32(bytecodeLenInWords << 224); + } + + /// @notice Validates the format of the given bytecode hash. + /// @dev Due to the specification of the L2 bytecode hash, not every 32 bytes could be a legit bytecode hash. + /// @dev The function reverts on invalid bytecode hash format. + /// @param _bytecodeHash The hash of the bytecode to validate. + function validateBytecodeHash(bytes32 _bytecodeHash) internal pure { + uint8 version = uint8(_bytecodeHash[0]); + // Incorrectly formatted bytecodeHash + if (version != 1 || _bytecodeHash[1] != bytes1(0)) { + revert MalformedBytecode(BytecodeError.Version); + } + + // Code length in words must be odd + if (bytecodeLen(_bytecodeHash) % 2 == 0) { + revert MalformedBytecode(BytecodeError.WordsMustBeOdd); + } + } + + /// @notice Returns the length of the bytecode associated with the given hash. + /// @param _bytecodeHash The hash of the bytecode. + /// @return codeLengthInWords The length of the bytecode in words. + function bytecodeLen(bytes32 _bytecodeHash) internal pure returns (uint256 codeLengthInWords) { + codeLengthInWords = uint256(uint8(_bytecodeHash[2])) * 256 + uint256(uint8(_bytecodeHash[3])); + } + + /// @notice Computes the create2 address for a Layer 2 contract. + /// @param _sender The address of the sender. + /// @param _salt The salt value to use in the create2 address computation. + /// @param _bytecodeHash The contract bytecode hash. + /// @param _constructorInputHash The hash of the constructor input data. + /// @return The create2 address of the contract. + /// NOTE: L2 create2 derivation is different from L1 derivation! + function computeCreate2Address( + address _sender, + bytes32 _salt, + bytes32 _bytecodeHash, + bytes32 _constructorInputHash + ) internal pure returns (address) { + bytes32 senderBytes = bytes32(uint256(uint160(_sender))); + bytes32 data = keccak256( + // solhint-disable-next-line func-named-parameters + bytes.concat(CREATE2_PREFIX, senderBytes, _salt, _bytecodeHash, _constructorInputHash) + ); + + return address(uint160(uint256(data))); + } +} diff --git a/contracts/l1-contracts/common/libraries/SemVer.sol b/contracts/l1-contracts/common/libraries/SemVer.sol new file mode 100644 index 0000000..c460516 --- /dev/null +++ b/contracts/l1-contracts/common/libraries/SemVer.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.21; + +/// @dev The number of bits dedicated to the "patch" portion of the protocol version. +/// This also defines the bit starting from which the "minor" part is located. +uint256 constant SEMVER_MINOR_OFFSET = 32; + +/// @dev The number of bits dedicated to the "patch" and "minor" portions of the protocol version. +/// This also defines the bit starting from which the "major" part is located. +/// Note, that currently, only major version of "0" is supported. +uint256 constant SEMVER_MAJOR_OFFSET = 64; + +/** + * @author Matter Labs + * @custom:security-contact security@matterlabs.dev + * @notice The library for managing SemVer for the protocol version. + */ +library SemVer { + /// @notice Unpacks the SemVer version from a single uint256 into major, minor and patch components. + /// @param _packedProtocolVersion The packed protocol version. + /// @return major The major version. + /// @return minor The minor version. + /// @return patch The patch version. + function unpackSemVer( + uint96 _packedProtocolVersion + ) internal pure returns (uint32 major, uint32 minor, uint32 patch) { + patch = uint32(_packedProtocolVersion); + minor = uint32(_packedProtocolVersion >> SEMVER_MINOR_OFFSET); + major = uint32(_packedProtocolVersion >> SEMVER_MAJOR_OFFSET); + } + + /// @notice Packs the SemVer version from the major, minor and patch components into a single uint96. + /// @param _major The major version. + /// @param _minor The minor version. + /// @param _patch The patch version. + /// @return packedProtocolVersion The packed protocol version. + function packSemVer( + uint32 _major, + uint32 _minor, + uint32 _patch + ) internal pure returns (uint96 packedProtocolVersion) { + packedProtocolVersion = + uint96(_patch) | + (uint96(_minor) << SEMVER_MINOR_OFFSET) | + (uint96(_major) << SEMVER_MAJOR_OFFSET); + } +} diff --git a/contracts/l1-contracts/common/libraries/UncheckedMath.sol b/contracts/l1-contracts/common/libraries/UncheckedMath.sol new file mode 100644 index 0000000..a41a9c6 --- /dev/null +++ b/contracts/l1-contracts/common/libraries/UncheckedMath.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.21; + +/** + * @author Matter Labs + * @custom:security-contact security@matterlabs.dev + * @notice The library for unchecked math. + */ +library UncheckedMath { + function uncheckedInc(uint256 _number) internal pure returns (uint256) { + unchecked { + return _number + 1; + } + } + + function uncheckedAdd(uint256 _lhs, uint256 _rhs) internal pure returns (uint256) { + unchecked { + return _lhs + _rhs; + } + } +} diff --git a/contracts/l1-contracts/common/libraries/UnsafeBytes.sol b/contracts/l1-contracts/common/libraries/UnsafeBytes.sol new file mode 100644 index 0000000..d20bcba --- /dev/null +++ b/contracts/l1-contracts/common/libraries/UnsafeBytes.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.21; + +/** + * @author Matter Labs + * @custom:security-contact security@matterlabs.dev + * @dev The library provides a set of functions that help read data from an "abi.encodePacked" byte array. + * @dev Each of the functions accepts the `bytes memory` and the offset where data should be read and returns a value of a certain type. + * + * @dev WARNING! + * 1) Functions don't check the length of the bytes array, so it can go out of bounds. + * The user of the library must check for bytes length before using any functions from the library! + * + * 2) Read variables are not cleaned up - https://docs.soliditylang.org/en/v0.8.16/internals/variable_cleanup.html. + * Using data in inline assembly can lead to unexpected behavior! + */ +library UnsafeBytes { + function readUint32(bytes memory _bytes, uint256 _start) internal pure returns (uint32 result, uint256 offset) { + assembly { + offset := add(_start, 4) + result := mload(add(_bytes, offset)) + } + } + + function readAddress(bytes memory _bytes, uint256 _start) internal pure returns (address result, uint256 offset) { + assembly { + offset := add(_start, 20) + result := mload(add(_bytes, offset)) + } + } + + function readUint256(bytes memory _bytes, uint256 _start) internal pure returns (uint256 result, uint256 offset) { + assembly { + offset := add(_start, 32) + result := mload(add(_bytes, offset)) + } + } + + function readBytes32(bytes memory _bytes, uint256 _start) internal pure returns (bytes32 result, uint256 offset) { + assembly { + offset := add(_start, 32) + result := mload(add(_bytes, offset)) + } + } +} diff --git a/contracts/l1-contracts/dev-contracts/ConstructorForwarder.sol b/contracts/l1-contracts/dev-contracts/ConstructorForwarder.sol new file mode 100644 index 0000000..d52f4ff --- /dev/null +++ b/contracts/l1-contracts/dev-contracts/ConstructorForwarder.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +contract ConstructorForwarder { + // add this to be excluded from coverage report + function test() internal virtual {} + + constructor(address to, bytes memory data) payable { + (bool success, ) = payable(to).call{value: msg.value}(data); + require(success); + } +} diff --git a/contracts/l1-contracts/dev-contracts/DummyL1ERC20Bridge.sol b/contracts/l1-contracts/dev-contracts/DummyL1ERC20Bridge.sol new file mode 100644 index 0000000..8155ddf --- /dev/null +++ b/contracts/l1-contracts/dev-contracts/DummyL1ERC20Bridge.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {L1ERC20Bridge} from "../bridge/L1ERC20Bridge.sol"; +import {IL1SharedBridge} from "../bridge/interfaces/IL1SharedBridge.sol"; + +contract DummyL1ERC20Bridge is L1ERC20Bridge { + constructor(IL1SharedBridge _l1SharedBridge) L1ERC20Bridge(_l1SharedBridge) {} + + function setValues(address _l2Bridge, address _l2TokenBeacon, bytes32 _l2TokenProxyBytecodeHash) external { + l2Bridge = _l2Bridge; + l2TokenBeacon = _l2TokenBeacon; + l2TokenProxyBytecodeHash = _l2TokenProxyBytecodeHash; + } +} diff --git a/contracts/l1-contracts/dev-contracts/EventOnFallback.sol b/contracts/l1-contracts/dev-contracts/EventOnFallback.sol new file mode 100644 index 0000000..7d8e67f --- /dev/null +++ b/contracts/l1-contracts/dev-contracts/EventOnFallback.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +contract EventOnFallback { + // add this to be excluded from coverage report + function test() internal virtual {} + + event Called(address msgSender, uint256 value, bytes data); + + fallback() external payable { + emit Called(msg.sender, msg.value, msg.data); + } +} diff --git a/contracts/l1-contracts/dev-contracts/FeeOnTransferToken.sol b/contracts/l1-contracts/dev-contracts/FeeOnTransferToken.sol new file mode 100644 index 0000000..3fbcabb --- /dev/null +++ b/contracts/l1-contracts/dev-contracts/FeeOnTransferToken.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {TestnetERC20Token} from "./TestnetERC20Token.sol"; + +contract FeeOnTransferToken is TestnetERC20Token { + // add this to be excluded from coverage report + function test() internal override {} + + constructor( + string memory name_, + string memory symbol_, + uint8 decimals_ + ) TestnetERC20Token(name_, symbol_, decimals_) {} + + function _transfer(address from, address to, uint256 amount) internal override { + super._transfer(from, to, amount - 1); + super._transfer(from, address(1), 1); + } +} diff --git a/contracts/l1-contracts/dev-contracts/Forwarder.sol b/contracts/l1-contracts/dev-contracts/Forwarder.sol new file mode 100644 index 0000000..6dc0c47 --- /dev/null +++ b/contracts/l1-contracts/dev-contracts/Forwarder.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +contract Forwarder { + // add this to be excluded from coverage report + function test() internal virtual {} + + function forward(address to, bytes calldata data) external payable returns (bytes memory returnValue) { + bool success; + (success, returnValue) = payable(to).call{value: msg.value}(data); + require(success); + } +} diff --git a/contracts/l1-contracts/dev-contracts/Multicall.sol b/contracts/l1-contracts/dev-contracts/Multicall.sol new file mode 100644 index 0000000..242c01e --- /dev/null +++ b/contracts/l1-contracts/dev-contracts/Multicall.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT + +/* + +MIT License + +Copyright (c) 2018 Maker Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +*/ + +pragma solidity 0.8.24; + +/// @title Multicall - Aggregate results from multiple read-only function calls +contract Multicall { + // add this to be excluded from coverage report + function test() internal virtual {} + + struct Call { + address target; + bytes callData; + } + + function aggregate(Call[] memory calls) public returns (uint256 blockNumber, bytes[] memory returnData) { + blockNumber = block.number; + returnData = new bytes[](calls.length); + uint256 callsLength = calls.length; + for (uint256 i = 0; i < callsLength; ++i) { + (bool success, bytes memory ret) = calls[i].target.call(calls[i].callData); + require(success, "multicall 1"); + returnData[i] = ret; + } + } + + // Helper functions + function getEthBalance(address addr) public view returns (uint256 balance) { + balance = addr.balance; + } + + function getBlockHash(uint256 blockNumber) public view returns (bytes32 blockHash) { + blockHash = blockhash(blockNumber); + } + + function getLastBlockHash() public view returns (bytes32 blockHash) { + blockHash = blockhash(block.number - 1); + } + + function getCurrentBlockTimestamp() public view returns (uint256 timestamp) { + timestamp = block.timestamp; + } + + function getCurrentBlockDifficulty() public view returns (uint256 difficulty) { + difficulty = block.prevrandao; + } + + function getCurrentBlockGasLimit() public view returns (uint256 gaslimit) { + gaslimit = block.gaslimit; + } + + function getCurrentBlockCoinbase() public view returns (address coinbase) { + coinbase = block.coinbase; + } +} diff --git a/contracts/l1-contracts/dev-contracts/Multicall3.sol b/contracts/l1-contracts/dev-contracts/Multicall3.sol new file mode 100644 index 0000000..aaa8b80 --- /dev/null +++ b/contracts/l1-contracts/dev-contracts/Multicall3.sol @@ -0,0 +1,237 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +/// @title Multicall3 +/// @notice Aggregate results from multiple function calls +/// @dev Multicall & Multicall2 backwards-compatible +/// @dev Aggregate methods are marked `payable` to save 24 gas per call +/// @author Michael Elliot +/// @author Joshua Levine +/// @author Nick Johnson +/// @author Andreas Bigger +/// @author Matt Solomon +contract Multicall3 { + // add this to be excluded from coverage report + function test() internal virtual {} + + struct Call { + address target; + bytes callData; + } + + struct Call3 { + address target; + bool allowFailure; + bytes callData; + } + + struct Call3Value { + address target; + bool allowFailure; + uint256 value; + bytes callData; + } + + struct Result { + bool success; + bytes returnData; + } + + /// @notice Backwards-compatible call aggregation with Multicall + /// @param calls An array of Call structs + /// @return blockNumber The block number where the calls were executed + /// @return returnData An array of bytes containing the responses + function aggregate(Call[] calldata calls) public payable returns (uint256 blockNumber, bytes[] memory returnData) { + blockNumber = block.number; + uint256 length = calls.length; + returnData = new bytes[](length); + Call calldata call; + for (uint256 i = 0; i < length; ) { + bool success; + call = calls[i]; + (success, returnData[i]) = call.target.call(call.callData); + require(success, "Multicall3: call failed"); + unchecked { + ++i; + } + } + } + + /// @notice Backwards-compatible with Multicall2 + /// @notice Aggregate calls without requiring success + /// @param requireSuccess If true, require all calls to succeed + /// @param calls An array of Call structs + /// @return returnData An array of Result structs + function tryAggregate( + bool requireSuccess, + Call[] calldata calls + ) public payable returns (Result[] memory returnData) { + uint256 length = calls.length; + returnData = new Result[](length); + Call calldata call; + for (uint256 i = 0; i < length; ) { + Result memory result = returnData[i]; + call = calls[i]; + (result.success, result.returnData) = call.target.call(call.callData); + if (requireSuccess) require(result.success, "Multicall3: call failed"); + unchecked { + ++i; + } + } + } + + /// @notice Backwards-compatible with Multicall2 + /// @notice Aggregate calls and allow failures using tryAggregate + /// @param calls An array of Call structs + /// @return blockNumber The block number where the calls were executed + /// @return blockHash The hash of the block where the calls were executed + /// @return returnData An array of Result structs + function tryBlockAndAggregate( + bool requireSuccess, + Call[] calldata calls + ) public payable returns (uint256 blockNumber, bytes32 blockHash, Result[] memory returnData) { + blockNumber = block.number; + blockHash = blockhash(block.number); + returnData = tryAggregate(requireSuccess, calls); + } + + /// @notice Backwards-compatible with Multicall2 + /// @notice Aggregate calls and allow failures using tryAggregate + /// @param calls An array of Call structs + /// @return blockNumber The block number where the calls were executed + /// @return blockHash The hash of the block where the calls were executed + /// @return returnData An array of Result structs + function blockAndAggregate( + Call[] calldata calls + ) public payable returns (uint256 blockNumber, bytes32 blockHash, Result[] memory returnData) { + (blockNumber, blockHash, returnData) = tryBlockAndAggregate(true, calls); + } + + /// @notice Aggregate calls, ensuring each returns success if required + /// @param calls An array of Call3 structs + /// @return returnData An array of Result structs + function aggregate3(Call3[] calldata calls) public payable returns (Result[] memory returnData) { + uint256 length = calls.length; + returnData = new Result[](length); + Call3 calldata calli; + for (uint256 i = 0; i < length; ) { + Result memory result = returnData[i]; + calli = calls[i]; + (result.success, result.returnData) = calli.target.call(calli.callData); + assembly { + // Revert if the call fails and failure is not allowed + // `allowFailure := calldataload(add(calli, 0x20))` and `success := mload(result)` + if iszero(or(calldataload(add(calli, 0x20)), mload(result))) { + // set "Error(string)" signature: bytes32(bytes4(keccak256("Error(string)"))) + mstore(0x00, 0x08c379a000000000000000000000000000000000000000000000000000000000) + // set data offset + mstore(0x04, 0x0000000000000000000000000000000000000000000000000000000000000020) + // set length of revert string + mstore(0x24, 0x0000000000000000000000000000000000000000000000000000000000000017) + // set revert string: bytes32(abi.encodePacked("Multicall3: call failed")) + mstore(0x44, 0x4d756c746963616c6c333a2063616c6c206661696c6564000000000000000000) + revert(0x00, 0x64) + } + } + unchecked { + ++i; + } + } + } + + /// @notice Aggregate calls with a msg value + /// @notice Reverts if msg.value is less than the sum of the call values + /// @param calls An array of Call3Value structs + /// @return returnData An array of Result structs + function aggregate3Value(Call3Value[] calldata calls) public payable returns (Result[] memory returnData) { + uint256 valAccumulator; + uint256 length = calls.length; + returnData = new Result[](length); + Call3Value calldata calli; + for (uint256 i = 0; i < length; ) { + Result memory result = returnData[i]; + calli = calls[i]; + uint256 val = calli.value; + // Humanity will be a Type V Kardashev Civilization before this overflows - andreas + // ~ 10^25 Wei in existence << ~ 10^76 size uint fits in a uint256 + unchecked { + valAccumulator += val; + } + (result.success, result.returnData) = calli.target.call{value: val}(calli.callData); + assembly { + // Revert if the call fails and failure is not allowed + // `allowFailure := calldataload(add(calli, 0x20))` and `success := mload(result)` + if iszero(or(calldataload(add(calli, 0x20)), mload(result))) { + // set "Error(string)" signature: bytes32(bytes4(keccak256("Error(string)"))) + mstore(0x00, 0x08c379a000000000000000000000000000000000000000000000000000000000) + // set data offset + mstore(0x04, 0x0000000000000000000000000000000000000000000000000000000000000020) + // set length of revert string + mstore(0x24, 0x0000000000000000000000000000000000000000000000000000000000000017) + // set revert string: bytes32(abi.encodePacked("Multicall3: call failed")) + mstore(0x44, 0x4d756c746963616c6c333a2063616c6c206661696c6564000000000000000000) + revert(0x00, 0x84) + } + } + unchecked { + ++i; + } + } + // Finally, make sure the msg.value = SUM(call[0...i].value) + require(msg.value == valAccumulator, "Multicall3: value mismatch"); + } + + /// @notice Returns the block hash for the given block number + /// @param blockNumber The block number + function getBlockHash(uint256 blockNumber) public view returns (bytes32 blockHash) { + blockHash = blockhash(blockNumber); + } + + /// @notice Returns the block number + function getBlockNumber() public view returns (uint256 blockNumber) { + blockNumber = block.number; + } + + /// @notice Returns the block coinbase + function getCurrentBlockCoinbase() public view returns (address coinbase) { + coinbase = block.coinbase; + } + + /// @notice Returns the block difficulty + function getCurrentBlockDifficulty() public view returns (uint256 difficulty) { + difficulty = block.prevrandao; + } + + /// @notice Returns the block gas limit + function getCurrentBlockGasLimit() public view returns (uint256 gaslimit) { + gaslimit = block.gaslimit; + } + + /// @notice Returns the block timestamp + function getCurrentBlockTimestamp() public view returns (uint256 timestamp) { + timestamp = block.timestamp; + } + + /// @notice Returns the (ETH) balance of a given address + function getEthBalance(address addr) public view returns (uint256 balance) { + balance = addr.balance; + } + + /// @notice Returns the block hash of the last block + function getLastBlockHash() public view returns (bytes32 blockHash) { + unchecked { + blockHash = blockhash(block.number - 1); + } + } + + /// @notice Gets the base fee of the given block + /// @notice Can revert if the BASEFEE opcode is not implemented by the given chain + function getBasefee() public view returns (uint256 basefee) { + basefee = block.basefee; + } + + /// @notice Returns the chain id + function getChainId() public view returns (uint256 chainid) { + chainid = block.chainid; + } +} diff --git a/contracts/l1-contracts/dev-contracts/ReturnSomething.sol b/contracts/l1-contracts/dev-contracts/ReturnSomething.sol new file mode 100644 index 0000000..1660f87 --- /dev/null +++ b/contracts/l1-contracts/dev-contracts/ReturnSomething.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +contract ReturnSomething { + // add this to be excluded from coverage report + function test() internal virtual {} + + fallback() external payable { + assembly { + return(0, 0x20) + } + } +} diff --git a/contracts/l1-contracts/dev-contracts/RevertFallback.sol b/contracts/l1-contracts/dev-contracts/RevertFallback.sol new file mode 100644 index 0000000..2174a41 --- /dev/null +++ b/contracts/l1-contracts/dev-contracts/RevertFallback.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +contract RevertFallback { + // add this to be excluded from coverage report + function test() internal virtual {} + + fallback() external payable { + revert(); + } +} diff --git a/contracts/l1-contracts/dev-contracts/RevertReceiveAccount.sol b/contracts/l1-contracts/dev-contracts/RevertReceiveAccount.sol new file mode 100644 index 0000000..663afdf --- /dev/null +++ b/contracts/l1-contracts/dev-contracts/RevertReceiveAccount.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +/// @title RevertReceiveAccount - An account which reverts receiving funds depending on the flag +/// @dev Used for testing failed withdrawals from the ZKsync smart contract +contract RevertReceiveAccount { + // add this to be excluded from coverage report + function test() internal virtual {} + + bool public revertReceive; + + constructor() { + revertReceive = false; + } + + function setRevertReceive(bool newValue) public { + revertReceive = newValue; + } + + receive() external payable { + // Assert is used here to also simulate the out-of-gas error, since failed assertion + // consumes up all the remaining gas + assert(!revertReceive); + } +} diff --git a/contracts/l1-contracts/dev-contracts/RevertTransferERC20.sol b/contracts/l1-contracts/dev-contracts/RevertTransferERC20.sol new file mode 100644 index 0000000..dcc1f71 --- /dev/null +++ b/contracts/l1-contracts/dev-contracts/RevertTransferERC20.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {TestnetERC20Token} from "./TestnetERC20Token.sol"; + +/// @title RevertTransferERC20Token - A ERC20 token contract which can revert transfers depending on a flag +/// @dev Used for testing failed ERC-20 withdrawals from the ZKsync smart contract +contract RevertTransferERC20 is TestnetERC20Token { + // add this to be excluded from coverage report + function test() internal override {} + + bool public revertTransfer; + + constructor(string memory name, string memory symbol, uint8 decimals) TestnetERC20Token(name, symbol, decimals) { + revertTransfer = false; + } + + function setRevertTransfer(bool newValue) public { + revertTransfer = newValue; + } + + function transfer(address recipient, uint256 amount) public virtual override returns (bool) { + // Assert is used here to also simulate the out-of-gas error, since failed assertion + // consumes up all the remaining gas + assert(!revertTransfer); + + _transfer(_msgSender(), recipient, amount); + return true; + } +} diff --git a/contracts/l1-contracts/dev-contracts/SingletonFactory.sol b/contracts/l1-contracts/dev-contracts/SingletonFactory.sol new file mode 100644 index 0000000..3e8b9ee --- /dev/null +++ b/contracts/l1-contracts/dev-contracts/SingletonFactory.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +/** + * @title Singleton Factory (EIP-2470) + * @notice Exposes CREATE2 (EIP-1014) to deploy bytecode on deterministic addresses based on initialization code + * and salt. + * @author Ricardo Guilherme Schmidt (Status Research & Development GmbH) + */ +contract SingletonFactory { + // add this to be excluded from coverage report + function test() internal virtual {} + + /** + * @notice Deploys `_initCode` using `_salt` for defining the deterministic address. + * @param _initCode Initialization code. + * @param _salt Arbitrary value to modify resulting address. + * @return createdContract Created contract address. + */ + function deploy(bytes memory _initCode, bytes32 _salt) public returns (address payable createdContract) { + assembly { + createdContract := create2(0, add(_initCode, 0x20), mload(_initCode), _salt) + } + } +} diff --git a/contracts/l1-contracts/dev-contracts/TestnetERC20Token.sol b/contracts/l1-contracts/dev-contracts/TestnetERC20Token.sol new file mode 100644 index 0000000..2b189d1 --- /dev/null +++ b/contracts/l1-contracts/dev-contracts/TestnetERC20Token.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {ERC20} from "@openzeppelin/contracts-v4/token/ERC20/ERC20.sol"; + +contract TestnetERC20Token is ERC20 { + // add this to be excluded from coverage report + function test() internal virtual {} + + uint8 private _decimals; + + constructor(string memory name_, string memory symbol_, uint8 decimals_) ERC20(name_, symbol_) { + _decimals = decimals_; + } + + function mint(address _to, uint256 _amount) public returns (bool) { + _mint(_to, _amount); + return true; + } + + function decimals() public view override returns (uint8) { + return _decimals; + } +} diff --git a/contracts/l1-contracts/dev-contracts/WETH9.sol b/contracts/l1-contracts/dev-contracts/WETH9.sol new file mode 100644 index 0000000..e094ba8 --- /dev/null +++ b/contracts/l1-contracts/dev-contracts/WETH9.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity 0.8.24; + +contract WETH9 { + // add this to be excluded from coverage report + function test() internal virtual {} + + string public name = "Wrapped Ether"; + string public symbol = "WETH"; + uint8 public decimals = 18; + + event Approval(address indexed src, address indexed guy, uint256 wad); + event Transfer(address indexed src, address indexed dst, uint256 wad); + event Deposit(address indexed dst, uint256 wad); + event Withdrawal(address indexed src, uint256 wad); + + mapping(address => uint256) public balanceOf; + mapping(address => mapping(address => uint256)) public allowance; + + receive() external payable { + deposit(); + } + + function deposit() public payable { + balanceOf[msg.sender] += msg.value; + emit Deposit(msg.sender, msg.value); + } + + function withdraw(uint256 wad) public { + require(balanceOf[msg.sender] >= wad, "weth9, 1"); + balanceOf[msg.sender] -= wad; + payable(msg.sender).transfer(wad); + emit Withdrawal(msg.sender, wad); + } + + function totalSupply() public view returns (uint256) { + return address(this).balance; + } + + function approve(address guy, uint256 wad) public returns (bool) { + allowance[msg.sender][guy] = wad; + emit Approval(msg.sender, guy, wad); + return true; + } + + function transfer(address dst, uint256 wad) public returns (bool) { + return transferFrom(msg.sender, dst, wad); + } + + function transferFrom(address src, address dst, uint256 wad) public returns (bool) { + require(balanceOf[src] >= wad, "weth9, 2"); + + if (src != msg.sender && allowance[src][msg.sender] != type(uint256).max) { + require(allowance[src][msg.sender] >= wad, "weth9, 3"); + allowance[src][msg.sender] -= wad; + } + + balanceOf[src] -= wad; + balanceOf[dst] += wad; + + emit Transfer(src, dst, wad); + + return true; + } +} diff --git a/contracts/l1-contracts/dev-contracts/interfaces/ITestnetERC20Token.sol b/contracts/l1-contracts/dev-contracts/interfaces/ITestnetERC20Token.sol new file mode 100644 index 0000000..9cf1d84 --- /dev/null +++ b/contracts/l1-contracts/dev-contracts/interfaces/ITestnetERC20Token.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +interface ITestnetERC20Token { + function mint(address _to, uint256 _amount) external returns (bool); + + function decimals() external returns (uint8); +} diff --git a/contracts/l1-contracts/dev-contracts/test/AddressAliasHelperTest.sol b/contracts/l1-contracts/dev-contracts/test/AddressAliasHelperTest.sol new file mode 100644 index 0000000..5391522 --- /dev/null +++ b/contracts/l1-contracts/dev-contracts/test/AddressAliasHelperTest.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {AddressAliasHelper} from "../../vendor/AddressAliasHelper.sol"; + +contract AddressAliasHelperTest { + function applyL1ToL2Alias(address _l1Address) external pure returns (address) { + return AddressAliasHelper.applyL1ToL2Alias(_l1Address); + } + + function undoL1ToL2Alias(address _l2Address) external pure returns (address) { + return AddressAliasHelper.undoL1ToL2Alias(_l2Address); + } +} diff --git a/contracts/l1-contracts/dev-contracts/test/AdminFacetTest.sol b/contracts/l1-contracts/dev-contracts/test/AdminFacetTest.sol new file mode 100644 index 0000000..614c34b --- /dev/null +++ b/contracts/l1-contracts/dev-contracts/test/AdminFacetTest.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {AdminFacet} from "../../state-transition/chain-deps/facets/Admin.sol"; + +contract AdminFacetTest is AdminFacet { + // add this to be excluded from coverage report + function test() internal virtual {} + + constructor() { + s.admin = msg.sender; + s.stateTransitionManager = msg.sender; + } + + function getPorterAvailability() external view returns (bool) { + return s.zkPorterIsAvailable; + } + + function isValidator(address _validator) external view returns (bool) { + return s.validators[_validator]; + } + + function getPriorityTxMaxGasLimit() external view returns (uint256) { + return s.priorityTxMaxGasLimit; + } + + function getAdmin() external view returns (address) { + return s.admin; + } + + function getPendingAdmin() external view returns (address) { + return s.pendingAdmin; + } +} diff --git a/contracts/l1-contracts/dev-contracts/test/CustomUpgradeTest.sol b/contracts/l1-contracts/dev-contracts/test/CustomUpgradeTest.sol new file mode 100644 index 0000000..7055ce5 --- /dev/null +++ b/contracts/l1-contracts/dev-contracts/test/CustomUpgradeTest.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {Diamond} from "../../state-transition/libraries/Diamond.sol"; +import {BaseZkSyncUpgrade, ProposedUpgrade} from "../../upgrades/BaseZkSyncUpgrade.sol"; + +contract CustomUpgradeTest is BaseZkSyncUpgrade { + // add this to be excluded from coverage report + function test() internal virtual {} + + event Test(); + + /// @notice Placeholder function for custom logic for upgrading L1 contract. + /// Typically this function will never be used. + /// @param _customCallDataForUpgrade Custom data for upgrade, which may be interpreted differently for each upgrade. + function _upgradeL1Contract(bytes calldata _customCallDataForUpgrade) internal override { + keccak256(_customCallDataForUpgrade); // called to suppress compilation warning + emit Test(); + } + + /// @notice placeholder function for custom logic for post-upgrade logic. + /// Typically this function will never be used. + /// @param _customCallDataForUpgrade Custom data for an upgrade, which may be interpreted differently for each + /// upgrade. + function _postUpgrade(bytes calldata _customCallDataForUpgrade) internal override {} + + /// @notice The main function that will be called by the upgrade proxy. + /// @param _proposedUpgrade The upgrade to be executed. + function upgrade(ProposedUpgrade calldata _proposedUpgrade) public override returns (bytes32) { + (uint32 newMinorVersion, bool isPatchOnly) = _setNewProtocolVersion(_proposedUpgrade.newProtocolVersion); + _upgradeL1Contract(_proposedUpgrade.l1ContractsUpgradeCalldata); + _upgradeVerifier(_proposedUpgrade.verifier, _proposedUpgrade.verifierParams); + _setBaseSystemContracts(_proposedUpgrade.bootloaderHash, _proposedUpgrade.defaultAccountHash, isPatchOnly); + + bytes32 txHash; + txHash = _setL2SystemContractUpgrade( + _proposedUpgrade.l2ProtocolUpgradeTx, + _proposedUpgrade.factoryDeps, + newMinorVersion, + isPatchOnly + ); + + _postUpgrade(_proposedUpgrade.postUpgradeCalldata); + + emit UpgradeComplete(_proposedUpgrade.newProtocolVersion, txHash, _proposedUpgrade); + + return Diamond.DIAMOND_INIT_SUCCESS_RETURN_VALUE; + } +} diff --git a/contracts/l1-contracts/dev-contracts/test/DiamondCutTestContract.sol b/contracts/l1-contracts/dev-contracts/test/DiamondCutTestContract.sol new file mode 100644 index 0000000..50424e1 --- /dev/null +++ b/contracts/l1-contracts/dev-contracts/test/DiamondCutTestContract.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {Diamond} from "../../state-transition/libraries/Diamond.sol"; +import {GettersFacet} from "../../state-transition/chain-deps/facets/Getters.sol"; + +contract DiamondCutTestContract is GettersFacet { + function diamondCut(Diamond.DiamondCutData memory _diamondCut) external { + Diamond.diamondCut(_diamondCut); + } +} diff --git a/contracts/l1-contracts/dev-contracts/test/DiamondProxyTest.sol b/contracts/l1-contracts/dev-contracts/test/DiamondProxyTest.sol new file mode 100644 index 0000000..212a2b7 --- /dev/null +++ b/contracts/l1-contracts/dev-contracts/test/DiamondProxyTest.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {Diamond} from "../../state-transition/libraries/Diamond.sol"; +import {ZkSyncHyperchainBase} from "../../state-transition/chain-deps/facets/ZkSyncHyperchainBase.sol"; + +contract DiamondProxyTest is ZkSyncHyperchainBase { + // add this to be excluded from coverage report + function test() internal virtual {} + + function setFreezability(bool _freeze) external returns (bytes32) { + Diamond.DiamondStorage storage diamondStorage = Diamond.getDiamondStorage(); + diamondStorage.isFrozen = _freeze; + return Diamond.DIAMOND_INIT_SUCCESS_RETURN_VALUE; + } +} diff --git a/contracts/l1-contracts/dev-contracts/test/DummyAdminFacet.sol b/contracts/l1-contracts/dev-contracts/test/DummyAdminFacet.sol new file mode 100644 index 0000000..0a27a7e --- /dev/null +++ b/contracts/l1-contracts/dev-contracts/test/DummyAdminFacet.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {ZkSyncHyperchainBase} from "../../state-transition/chain-deps/facets/ZkSyncHyperchainBase.sol"; + +contract DummyAdminFacet is ZkSyncHyperchainBase { + // add this to be excluded from coverage report + function test() internal virtual {} + + function getName() external pure returns (string memory) { + return "DummyAdminFacet"; + } + + function dummySetValidator(address _validator) external { + s.validators[_validator] = true; + } +} diff --git a/contracts/l1-contracts/dev-contracts/test/DummyAdminFacetNoOverlap.sol b/contracts/l1-contracts/dev-contracts/test/DummyAdminFacetNoOverlap.sol new file mode 100644 index 0000000..0805e53 --- /dev/null +++ b/contracts/l1-contracts/dev-contracts/test/DummyAdminFacetNoOverlap.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {Diamond} from "../../state-transition/libraries/Diamond.sol"; +import {ZkSyncHyperchainBase} from "../../state-transition/chain-deps/facets/ZkSyncHyperchainBase.sol"; + +/// selectors do not overlap with normal facet selectors (getName does not count) +contract DummyAdminFacetNoOverlap is ZkSyncHyperchainBase { + // add this to be excluded from coverage report + function test() internal virtual {} + + function getName() external pure returns (string memory) { + return "DummyAdminFacetNoOverlap"; + } + + function executeUpgradeNoOverlap(Diamond.DiamondCutData calldata _diamondCut) external { + Diamond.diamondCut(_diamondCut); + } + + function receiveEther() external payable {} +} diff --git a/contracts/l1-contracts/dev-contracts/test/DummyBridgehub.sol b/contracts/l1-contracts/dev-contracts/test/DummyBridgehub.sol new file mode 100644 index 0000000..79f9dc6 --- /dev/null +++ b/contracts/l1-contracts/dev-contracts/test/DummyBridgehub.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {Bridgehub} from "../../bridgehub/Bridgehub.sol"; + +/// @title DummyBridgehub +/// @notice A test smart contract that allows to set State Transition Manager for a given chain +contract DummyBridgehub is Bridgehub { + // add this to be excluded from coverage report + function test() internal virtual {} + + constructor() Bridgehub() {} + + function setStateTransitionManager(uint256 _chainId, address _stm) external { + stateTransitionManager[_chainId] = _stm; + } +} diff --git a/contracts/l1-contracts/dev-contracts/test/DummyERC20BytesTransferReturnValue.sol b/contracts/l1-contracts/dev-contracts/test/DummyERC20BytesTransferReturnValue.sol new file mode 100644 index 0000000..34371a0 --- /dev/null +++ b/contracts/l1-contracts/dev-contracts/test/DummyERC20BytesTransferReturnValue.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +contract DummyERC20BytesTransferReturnValue { + // add this to be excluded from coverage report + function test() internal virtual {} + + bytes returnValue; + + constructor(bytes memory _returnValue) { + returnValue = _returnValue; + } + + function transfer(address _recipient, uint256 _amount) external view returns (bytes memory) { + // Hack to prevent Solidity warnings + _recipient; + _amount; + + return returnValue; + } +} diff --git a/contracts/l1-contracts/dev-contracts/test/DummyERC20NoTransferReturnValue.sol b/contracts/l1-contracts/dev-contracts/test/DummyERC20NoTransferReturnValue.sol new file mode 100644 index 0000000..b88023e --- /dev/null +++ b/contracts/l1-contracts/dev-contracts/test/DummyERC20NoTransferReturnValue.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +contract DummyERC20NoTransferReturnValue { + // add this to be excluded from coverage report + function test() internal virtual {} + + function transfer(address recipient, uint256 amount) external {} +} diff --git a/contracts/l1-contracts/dev-contracts/test/DummyEraBaseTokenBridge.sol b/contracts/l1-contracts/dev-contracts/test/DummyEraBaseTokenBridge.sol new file mode 100644 index 0000000..96382c4 --- /dev/null +++ b/contracts/l1-contracts/dev-contracts/test/DummyEraBaseTokenBridge.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +contract DummyEraBaseTokenBridge { + // add this to be excluded from coverage report + function test() internal virtual {} + + function bridgehubDepositBaseToken( + uint256 _chainId, + address _prevMsgSender, + address _l1Token, + uint256 _amount + ) external payable {} +} diff --git a/contracts/l1-contracts/dev-contracts/test/DummyExecutor.sol b/contracts/l1-contracts/dev-contracts/test/DummyExecutor.sol new file mode 100644 index 0000000..7da7113 --- /dev/null +++ b/contracts/l1-contracts/dev-contracts/test/DummyExecutor.sol @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {IExecutor} from "../../state-transition/chain-interfaces/IExecutor.sol"; + +/// @title DummyExecutor +/// @notice A test smart contract implementing the IExecutor interface to simulate Executor behavior for testing purposes. +contract DummyExecutor is IExecutor { + // add this to be excluded from coverage report + function test() internal virtual {} + + address owner; + + // Flags to control if the contract should revert during commit, prove, and execute batch operations + bool shouldRevertOnCommitBatches; + bool shouldRevertOnProveBatches; + bool shouldRevertOnExecuteBatches; + + // Counters to track the total number of committed, verified, and executed batches + uint256 public getTotalBatchesCommitted; + uint256 public getTotalBatchesVerified; + uint256 public getTotalBatchesExecuted; + string public constant override getName = "DummyExecutor"; + + /// @notice Constructor sets the contract owner to the message sender + constructor() { + owner = msg.sender; + } + + /// @notice Modifier that only allows the owner to call certain functions + modifier onlyOwner() { + require(msg.sender == owner); + _; + } + + function getAdmin() external view returns (address) { + return owner; + } + + /// @notice Removing txs from the priority queue + function removePriorityQueueFront(uint256 _index) external {} + + /// @notice Allows the owner to set whether the contract should revert during commit blocks operation + function setShouldRevertOnCommitBatches(bool _shouldRevert) external onlyOwner { + shouldRevertOnCommitBatches = _shouldRevert; + } + + /// @notice Allows the owner to set whether the contract should revert during prove batches operation + function setShouldRevertOnProveBatches(bool _shouldRevert) external onlyOwner { + shouldRevertOnProveBatches = _shouldRevert; + } + + /// @notice Allows the owner to set whether the contract should revert during execute batches operation + function setShouldRevertOnExecuteBatches(bool _shouldRevert) external onlyOwner { + shouldRevertOnExecuteBatches = _shouldRevert; + } + + function commitBatches( + StoredBatchInfo calldata _lastCommittedBatchData, + CommitBatchInfo[] calldata _newBatchesData + ) public { + require(!shouldRevertOnCommitBatches, "DummyExecutor: shouldRevertOnCommitBatches"); + require( + _lastCommittedBatchData.batchNumber == getTotalBatchesCommitted, + "DummyExecutor: Invalid last committed batch number" + ); + + uint256 batchesLength = _newBatchesData.length; + for (uint256 i = 0; i < batchesLength; ++i) { + require(getTotalBatchesCommitted + i + 1 == _newBatchesData[i].batchNumber); + } + + getTotalBatchesCommitted += batchesLength; + } + + function commitBatchesSharedBridge( + uint256, + StoredBatchInfo calldata _lastCommittedBatchData, + CommitBatchInfo[] calldata _newBatchesData + ) external { + commitBatches(_lastCommittedBatchData, _newBatchesData); + } + + function proveBatches( + StoredBatchInfo calldata _prevBatch, + StoredBatchInfo[] calldata _committedBatches, + ProofInput calldata + ) public { + require(!shouldRevertOnProveBatches, "DummyExecutor: shouldRevertOnProveBatches"); + require(_prevBatch.batchNumber == getTotalBatchesVerified, "DummyExecutor: Invalid previous batch number"); + + require(_committedBatches.length == 1, "DummyExecutor: Can prove only one batch"); + require( + _committedBatches[0].batchNumber == _prevBatch.batchNumber + 1, + "DummyExecutor 1: Can't prove batch out of order" + ); + + getTotalBatchesVerified += 1; + require( + getTotalBatchesVerified <= getTotalBatchesCommitted, + "DummyExecutor: prove more batches than were committed" + ); + } + + function proveBatchesSharedBridge( + uint256, + StoredBatchInfo calldata _prevBatch, + StoredBatchInfo[] calldata _committedBatches, + ProofInput calldata _proof + ) external { + proveBatches(_prevBatch, _committedBatches, _proof); + } + + function executeBatches(StoredBatchInfo[] calldata _batchesData) public { + require(!shouldRevertOnExecuteBatches, "DummyExecutor: shouldRevertOnExecuteBatches"); + uint256 nBatches = _batchesData.length; + for (uint256 i = 0; i < nBatches; ++i) { + require(_batchesData[i].batchNumber == getTotalBatchesExecuted + i + 1); + } + getTotalBatchesExecuted += nBatches; + require( + getTotalBatchesExecuted <= getTotalBatchesVerified, + "DummyExecutor 2: Can't execute batches more than committed and proven currently" + ); + } + + function executeBatchesSharedBridge(uint256, StoredBatchInfo[] calldata _batchesData) external { + executeBatches(_batchesData); + } + + function revertBatches(uint256 _newLastBatch) public { + require( + getTotalBatchesCommitted > _newLastBatch, + "DummyExecutor: The last committed batch is less than new last batch" + ); + uint256 newTotalBatchesCommitted = _maxU256(_newLastBatch, getTotalBatchesExecuted); + + if (newTotalBatchesCommitted < getTotalBatchesVerified) { + getTotalBatchesVerified = newTotalBatchesCommitted; + } + getTotalBatchesCommitted = newTotalBatchesCommitted; + } + + function revertBatchesSharedBridge(uint256, uint256 _newLastBatch) external { + revertBatches(_newLastBatch); + } + + /// @notice Returns larger of two values + function _maxU256(uint256 a, uint256 b) internal pure returns (uint256) { + return a < b ? b : a; + } +} diff --git a/contracts/l1-contracts/dev-contracts/test/DummyHyperchain.sol b/contracts/l1-contracts/dev-contracts/test/DummyHyperchain.sol new file mode 100644 index 0000000..11be65a --- /dev/null +++ b/contracts/l1-contracts/dev-contracts/test/DummyHyperchain.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import {MailboxFacet} from "../../state-transition/chain-deps/facets/Mailbox.sol"; +import {FeeParams, PubdataPricingMode} from "../../state-transition/chain-deps/ZkSyncHyperchainStorage.sol"; + +contract DummyHyperchain is MailboxFacet { + constructor(address bridgeHubAddress, uint256 _eraChainId) MailboxFacet(_eraChainId) { + s.bridgehub = bridgeHubAddress; + } + + function getEraChainId() public view returns (uint256) { + return ERA_CHAIN_ID; + } + + function setBridgeHubAddress(address bridgeHubAddress) public { + s.bridgehub = bridgeHubAddress; + } + + function setBaseTokenGasMultiplierPrice(uint128 nominator, uint128 denominator) public { + s.baseTokenGasPriceMultiplierNominator = nominator; + s.baseTokenGasPriceMultiplierDenominator = denominator; + } + + function getBridgeHubAddress() public view returns (address) { + return s.bridgehub; + } + + function setFeeParams() external { + FeeParams memory _feeParams = _randomFeeParams(); + s.feeParams = _feeParams; + s.priorityTxMaxGasLimit = type(uint256).max; + } + + function _randomFeeParams() internal pure returns (FeeParams memory) { + return + FeeParams({ + pubdataPricingMode: PubdataPricingMode.Rollup, + batchOverheadL1Gas: 1_000_000, + maxPubdataPerBatch: 110_000, + maxL2GasPerBatch: 80_000_000, + priorityTxMaxPubdata: 99_000, + minimalL2GasPrice: 250_000_000 + }); + } +} diff --git a/contracts/l1-contracts/dev-contracts/test/DummySharedBridge.sol b/contracts/l1-contracts/dev-contracts/test/DummySharedBridge.sol new file mode 100644 index 0000000..bebdbe4 --- /dev/null +++ b/contracts/l1-contracts/dev-contracts/test/DummySharedBridge.sol @@ -0,0 +1,195 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {IERC20} from "@openzeppelin/contracts-v4/token/ERC20/IERC20.sol"; + +import {L2TransactionRequestTwoBridgesInner} from "../../bridgehub/IBridgehub.sol"; +import {TWO_BRIDGES_MAGIC_VALUE, ETH_TOKEN_ADDRESS} from "../../common/Config.sol"; +import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable-v4/security/PausableUpgradeable.sol"; +import {SafeERC20} from "@openzeppelin/contracts-v4/token/ERC20/utils/SafeERC20.sol"; +import {IL2Bridge} from "../../bridge/interfaces/IL2Bridge.sol"; + +contract DummySharedBridge is PausableUpgradeable { + using SafeERC20 for IERC20; + + event BridgehubDepositBaseTokenInitiated( + uint256 indexed chainId, + address indexed from, + address l1Token, + uint256 amount + ); + + bytes32 dummyL2DepositTxHash; + + /// @dev Maps token balances for each chain to prevent unauthorized spending across hyperchains. + /// This serves as a security measure until hyperbridging is implemented. + mapping(uint256 chainId => mapping(address l1Token => uint256 balance)) public chainBalance; + + /// @dev Indicates whether the hyperbridging is enabled for a given chain. + mapping(uint256 chainId => bool enabled) internal hyperbridgingEnabled; + + address l1ReceiverReturnInFinalizeWithdrawal; + address l1TokenReturnInFinalizeWithdrawal; + uint256 amountReturnInFinalizeWithdrawal; + + constructor(bytes32 _dummyL2DepositTxHash) { + dummyL2DepositTxHash = _dummyL2DepositTxHash; + } + + function setDataToBeReturnedInFinalizeWithdrawal(address _l1Receiver, address _l1Token, uint256 _amount) external { + l1ReceiverReturnInFinalizeWithdrawal = _l1Receiver; + l1TokenReturnInFinalizeWithdrawal = _l1Token; + amountReturnInFinalizeWithdrawal = _amount; + } + + function receiveEth(uint256 _chainId) external payable {} + + function depositLegacyErc20Bridge( + address, //_msgSender, + address, //_l2Receiver, + address, //_l1Token, + uint256, //_amount, + uint256, //_l2TxGasLimit, + uint256, //_l2TxGasPerPubdataByte, + address //_refundRecipient + ) external payable returns (bytes32 txHash) { + txHash = dummyL2DepositTxHash; + } + + function claimFailedDepositLegacyErc20Bridge( + address, //_depositSender, + address, //_l1Token, + uint256, //_amount, + bytes32, //_l2TxHash, + uint256, //_l2BatchNumber, + uint256, //_l2MessageIndex, + uint16, //_l2TxNumberInBatch, + bytes32[] calldata // _merkleProof + ) external {} + + function finalizeWithdrawalLegacyErc20Bridge( + uint256, //_l2BatchNumber, + uint256, //_l2MessageIndex, + uint16, //_l2TxNumberInBatch, + bytes calldata, //_message, + bytes32[] calldata //_merkleProof + ) external view returns (address l1Receiver, address l1Token, uint256 amount) { + l1Receiver = l1ReceiverReturnInFinalizeWithdrawal; + l1Token = l1TokenReturnInFinalizeWithdrawal; + amount = amountReturnInFinalizeWithdrawal; + } + + event Debugger(uint256); + + function pause() external { + _pause(); + } + + function unpause() external { + _unpause(); + } + + // This function expects abi encoded data + function _parseL2WithdrawalMessage( + bytes memory _l2ToL1message + ) internal view returns (address l1Receiver, address l1Token, uint256 amount) { + (l1Receiver, l1Token, amount) = abi.decode(_l2ToL1message, (address, address, uint256)); + } + + // simple function to just transfer the funds + function finalizeWithdrawal( + uint256 _chainId, + uint256 _l2BatchNumber, + uint256 _l2MessageIndex, + uint16 _l2TxNumberInBatch, + bytes calldata _message, + bytes32[] calldata _merkleProof + ) external { + (address l1Receiver, address l1Token, uint256 amount) = _parseL2WithdrawalMessage(_message); + + if (l1Token == address(1)) { + bool callSuccess; + // Low-level assembly call, to avoid any memory copying (save gas) + assembly { + callSuccess := call(gas(), l1Receiver, amount, 0, 0, 0, 0) + } + require(callSuccess, "ShB: withdraw failed"); + } else { + // Withdraw funds + IERC20(l1Token).safeTransfer(l1Receiver, amount); + } + } + + function bridgehubDepositBaseToken( + uint256 _chainId, + address _prevMsgSender, + address _l1Token, + uint256 _amount + ) external payable whenNotPaused { + if (_l1Token == address(1)) { + require(msg.value == _amount, "L1SharedBridge: msg.value not equal to amount"); + } else { + // The Bridgehub also checks this, but we want to be sure + require(msg.value == 0, "ShB m.v > 0 b d.it"); + uint256 amount = _depositFunds(_prevMsgSender, IERC20(_l1Token), _amount); // note if _prevMsgSender is this contract, this will return 0. This does not happen. + require(amount == _amount, "3T"); // The token has non-standard transfer logic + } + + if (!hyperbridgingEnabled[_chainId]) { + chainBalance[_chainId][_l1Token] += _amount; + } + + emit Debugger(5); + // Note that we don't save the deposited amount, as this is for the base token, which gets sent to the refundRecipient if the tx fails + emit BridgehubDepositBaseTokenInitiated(_chainId, _prevMsgSender, _l1Token, _amount); + } + + function _depositFunds(address _from, IERC20 _token, uint256 _amount) internal returns (uint256) { + uint256 balanceBefore = _token.balanceOf(address(this)); + _token.transferFrom(_from, address(this), _amount); + uint256 balanceAfter = _token.balanceOf(address(this)); + + return balanceAfter - balanceBefore; + } + + function bridgehubDeposit( + uint256, + address _prevMsgSender, + uint256, + bytes calldata _data + ) external payable returns (L2TransactionRequestTwoBridgesInner memory request) { + (address _l1Token, uint256 _depositAmount, address _l2Receiver) = abi.decode( + _data, + (address, uint256, address) + ); + uint256 amount; + + if (_l1Token == ETH_TOKEN_ADDRESS) { + amount = msg.value; + require(_depositAmount == 0, "ShB wrong withdraw amount"); + } else { + require(msg.value == 0, "ShB m.v > 0 for BH d.it 2"); + amount = _depositAmount; + + uint256 withdrawAmount = _depositFunds(_prevMsgSender, IERC20(_l1Token), _depositAmount); + require(withdrawAmount == _depositAmount, "5T"); // The token has non-standard transfer logic + } + + bytes memory l2TxCalldata = abi.encodeCall( + IL2Bridge.finalizeDeposit, + (_prevMsgSender, _l2Receiver, _l1Token, amount, new bytes(0)) + ); + bytes32 txDataHash = keccak256(abi.encode(_prevMsgSender, _l1Token, amount)); + + request = L2TransactionRequestTwoBridgesInner({ + magicValue: TWO_BRIDGES_MAGIC_VALUE, + l2Contract: address(0xCAFE), + l2Calldata: l2TxCalldata, + factoryDeps: new bytes[](0), + txDataHash: txDataHash + }); + } + + function bridgehubConfirmL2Transaction(uint256 _chainId, bytes32 _txDataHash, bytes32 _txHash) external {} +} diff --git a/contracts/l1-contracts/dev-contracts/test/DummyStateTransitionManager.sol b/contracts/l1-contracts/dev-contracts/test/DummyStateTransitionManager.sol new file mode 100644 index 0000000..fba3746 --- /dev/null +++ b/contracts/l1-contracts/dev-contracts/test/DummyStateTransitionManager.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {EnumerableMap} from "@openzeppelin/contracts-v4/utils/structs/EnumerableMap.sol"; + +import {StateTransitionManager} from "../../state-transition/StateTransitionManager.sol"; + +/// @title DummyExecutor +/// @notice A test smart contract implementing the IExecutor interface to simulate Executor behavior for testing purposes. +contract DummyStateTransitionManager is StateTransitionManager { + using EnumerableMap for EnumerableMap.UintToAddressMap; + + // add this to be excluded from coverage report + function test() internal virtual {} + + /// @notice Constructor + constructor() StateTransitionManager(address(0), type(uint256).max) {} + + function setHyperchain(uint256 _chainId, address _hyperchain) external { + hyperchainMap.set(_chainId, _hyperchain); + } +} diff --git a/contracts/l1-contracts/dev-contracts/test/DummyStateTransitionManagerForValidatorTimelock.sol b/contracts/l1-contracts/dev-contracts/test/DummyStateTransitionManagerForValidatorTimelock.sol new file mode 100644 index 0000000..f2944d3 --- /dev/null +++ b/contracts/l1-contracts/dev-contracts/test/DummyStateTransitionManagerForValidatorTimelock.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +/// @title DummyStateTransitionManagerForValidatorTimelock +/// @notice A test smart contract implementing the IExecutor interface to simulate Executor behavior for testing purposes. +contract DummyStateTransitionManagerForValidatorTimelock { + // add this to be excluded from coverage report + function test() internal virtual {} + + address public chainAdmin; + address public hyperchainAddress; + + constructor(address _chainAdmin, address _hyperchain) { + chainAdmin = _chainAdmin; + hyperchainAddress = _hyperchain; + } + + function getChainAdmin(uint256) external view returns (address) { + return chainAdmin; + } + + function getHyperchain(uint256) external view returns (address) { + return hyperchainAddress; + } +} diff --git a/contracts/l1-contracts/dev-contracts/test/DummyStateTransitionManagerWithBridgeHubAddress.sol b/contracts/l1-contracts/dev-contracts/test/DummyStateTransitionManagerWithBridgeHubAddress.sol new file mode 100644 index 0000000..5109f7b --- /dev/null +++ b/contracts/l1-contracts/dev-contracts/test/DummyStateTransitionManagerWithBridgeHubAddress.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {EnumerableMap} from "@openzeppelin/contracts-v4/utils/structs/EnumerableMap.sol"; + +import {StateTransitionManager} from "../../state-transition/StateTransitionManager.sol"; + +/// @title DummyExecutor +/// @notice A test smart contract implementing the IExecutor interface to simulate Executor behavior for testing purposes. +contract DummyStateTransitionManagerWBH is StateTransitionManager { + using EnumerableMap for EnumerableMap.UintToAddressMap; + + /// @notice Constructor + constructor(address bridgeHub) StateTransitionManager(bridgeHub, type(uint256).max) {} + + function setHyperchain(uint256 _chainId, address _hyperchain) external { + hyperchainMap.set(_chainId, _hyperchain); + } +} diff --git a/contracts/l1-contracts/dev-contracts/test/DummyTransactionFiltererFalse.sol b/contracts/l1-contracts/dev-contracts/test/DummyTransactionFiltererFalse.sol new file mode 100644 index 0000000..3b5582a --- /dev/null +++ b/contracts/l1-contracts/dev-contracts/test/DummyTransactionFiltererFalse.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {ITransactionFilterer} from "../../state-transition/chain-interfaces/ITransactionFilterer.sol"; + +contract TransactionFiltererFalse is ITransactionFilterer { + // add this to be excluded from coverage report + function test() internal virtual {} + + function isTransactionAllowed( + address, + address, + uint256, + uint256, + bytes memory, + address + ) external pure returns (bool) { + return false; + } +} diff --git a/contracts/l1-contracts/dev-contracts/test/DummyTransactionFiltererTrue.sol b/contracts/l1-contracts/dev-contracts/test/DummyTransactionFiltererTrue.sol new file mode 100644 index 0000000..d7eb1a3 --- /dev/null +++ b/contracts/l1-contracts/dev-contracts/test/DummyTransactionFiltererTrue.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {ITransactionFilterer} from "../../state-transition/chain-interfaces/ITransactionFilterer.sol"; + +contract TransactionFiltererTrue is ITransactionFilterer { + // add this to be excluded from coverage report + function test() internal virtual {} + + function isTransactionAllowed( + address, + address, + uint256, + uint256, + bytes memory, + address + ) external pure returns (bool) { + return true; + } +} diff --git a/contracts/l1-contracts/dev-contracts/test/ExecutorProvingTest.sol b/contracts/l1-contracts/dev-contracts/test/ExecutorProvingTest.sol new file mode 100644 index 0000000..50bccb7 --- /dev/null +++ b/contracts/l1-contracts/dev-contracts/test/ExecutorProvingTest.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {ExecutorFacet} from "../../state-transition/chain-deps/facets/Executor.sol"; +import {PubdataPricingMode} from "../../state-transition/chain-deps/ZkSyncHyperchainStorage.sol"; +import {LogProcessingOutput} from "../../state-transition/chain-interfaces/IExecutor.sol"; +import {LogProcessingOutput} from "../../state-transition/chain-interfaces/IExecutor.sol"; + +contract ExecutorProvingTest is ExecutorFacet { + function getBatchProofPublicInput( + bytes32 _prevBatchCommitment, + bytes32 _currentBatchCommitment + ) external pure returns (uint256) { + return _getBatchProofPublicInput(_prevBatchCommitment, _currentBatchCommitment); + } + + function createBatchCommitment( + CommitBatchInfo calldata _newBatchData, + bytes32 _stateDiffHash, + bytes32[] memory _blobCommitments, + bytes32[] memory _blobHashes + ) external view returns (bytes32) { + return _createBatchCommitment(_newBatchData, _stateDiffHash, _blobCommitments, _blobHashes); + } + + function processL2Logs( + CommitBatchInfo calldata _newBatch, + bytes32 _expectedSystemContractUpgradeTxHash, + PubdataPricingMode + ) external pure returns (LogProcessingOutput memory logOutput) { + return _processL2Logs(_newBatch, _expectedSystemContractUpgradeTxHash); + } + + /// Sets the DefaultAccount Hash and Bootloader Hash. + function setHashes(bytes32 l2DefaultAccountBytecodeHash, bytes32 l2BootloaderBytecodeHash) external { + s.l2DefaultAccountBytecodeHash = l2DefaultAccountBytecodeHash; + s.l2BootloaderBytecodeHash = l2BootloaderBytecodeHash; + s.zkPorterIsAvailable = false; + } +} diff --git a/contracts/l1-contracts/dev-contracts/test/L1ERC20BridgeTest.sol b/contracts/l1-contracts/dev-contracts/test/L1ERC20BridgeTest.sol new file mode 100644 index 0000000..034fb33 --- /dev/null +++ b/contracts/l1-contracts/dev-contracts/test/L1ERC20BridgeTest.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {L1ERC20Bridge} from "../../bridge/L1ERC20Bridge.sol"; +import {IBridgehub, IL1SharedBridge} from "../../bridge/interfaces/IL1SharedBridge.sol"; + +/// @author Matter Labs +contract L1ERC20BridgeTest is L1ERC20Bridge { + // add this to be excluded from coverage report + function test() internal virtual {} + + constructor(IBridgehub _zkSync) L1ERC20Bridge(IL1SharedBridge(address(0))) {} +} diff --git a/contracts/l1-contracts/dev-contracts/test/MailboxFacetTest.sol b/contracts/l1-contracts/dev-contracts/test/MailboxFacetTest.sol new file mode 100644 index 0000000..d5a4155 --- /dev/null +++ b/contracts/l1-contracts/dev-contracts/test/MailboxFacetTest.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {FeeParams} from "../../state-transition/chain-deps/ZkSyncHyperchainStorage.sol"; +import {MailboxFacet} from "../../state-transition/chain-deps/facets/Mailbox.sol"; +import {REQUIRED_L2_GAS_PRICE_PER_PUBDATA} from "../../common/Config.sol"; + +contract MailboxFacetTest is MailboxFacet { + // add this to be excluded from coverage report + function test() internal virtual {} + + constructor(uint256 _eraChainId) MailboxFacet(_eraChainId) { + s.admin = msg.sender; + } + + function setFeeParams(FeeParams memory _feeParams) external { + s.feeParams = _feeParams; + } + + function getL2GasPrice(uint256 _l1GasPrice) external view returns (uint256) { + return _deriveL2GasPrice(_l1GasPrice, REQUIRED_L2_GAS_PRICE_PER_PUBDATA); + } +} diff --git a/contracts/l1-contracts/dev-contracts/test/MerkleTest.sol b/contracts/l1-contracts/dev-contracts/test/MerkleTest.sol new file mode 100644 index 0000000..7db97f8 --- /dev/null +++ b/contracts/l1-contracts/dev-contracts/test/MerkleTest.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {Merkle} from "../../state-transition/libraries/Merkle.sol"; + +contract MerkleTest { + function calculateRoot( + bytes32[] calldata _path, + uint256 _index, + bytes32 _itemHash + ) external pure returns (bytes32) { + return Merkle.calculateRoot(_path, _index, _itemHash); + } +} diff --git a/contracts/l1-contracts/dev-contracts/test/MockExecutor.sol b/contracts/l1-contracts/dev-contracts/test/MockExecutor.sol new file mode 100644 index 0000000..9c7878b --- /dev/null +++ b/contracts/l1-contracts/dev-contracts/test/MockExecutor.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {ZkSyncHyperchainBase} from "../../state-transition/chain-deps/facets/ZkSyncHyperchainBase.sol"; + +contract MockExecutorFacet is ZkSyncHyperchainBase { + // add this to be excluded from coverage report + function test() internal virtual {} + + function saveL2LogsRootHash(uint256 _batchNumber, bytes32 _l2LogsTreeRoot) external { + s.totalBatchesExecuted = _batchNumber; + s.l2LogsRootHashes[_batchNumber] = _l2LogsTreeRoot; + } +} diff --git a/contracts/l1-contracts/dev-contracts/test/PriorityQueueTest.sol b/contracts/l1-contracts/dev-contracts/test/PriorityQueueTest.sol new file mode 100644 index 0000000..8619e2f --- /dev/null +++ b/contracts/l1-contracts/dev-contracts/test/PriorityQueueTest.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {PriorityQueue, PriorityOperation} from "../../state-transition/libraries/PriorityQueue.sol"; + +contract PriorityQueueTest { + PriorityQueue.Queue priorityQueue; + + function getFirstUnprocessedPriorityTx() external view returns (uint256) { + return PriorityQueue.getFirstUnprocessedPriorityTx(priorityQueue); + } + + function getTotalPriorityTxs() external view returns (uint256) { + return PriorityQueue.getTotalPriorityTxs(priorityQueue); + } + + function getSize() external view returns (uint256) { + return PriorityQueue.getSize(priorityQueue); + } + + function isEmpty() external view returns (bool) { + return PriorityQueue.isEmpty(priorityQueue); + } + + function pushBack(PriorityOperation memory _operation) external { + return PriorityQueue.pushBack(priorityQueue, _operation); + } + + function front() external view returns (PriorityOperation memory) { + return PriorityQueue.front(priorityQueue); + } + + function popFront() external returns (PriorityOperation memory operation) { + return PriorityQueue.popFront(priorityQueue); + } +} diff --git a/contracts/l1-contracts/dev-contracts/test/ReenterGovernance.sol b/contracts/l1-contracts/dev-contracts/test/ReenterGovernance.sol new file mode 100644 index 0000000..0d619c5 --- /dev/null +++ b/contracts/l1-contracts/dev-contracts/test/ReenterGovernance.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {IGovernance} from "../../governance/IGovernance.sol"; + +contract ReenterGovernance { + // add this to be excluded from coverage report + function test() internal virtual {} + + IGovernance governance; + + // Store call, predecessor and salt separately, + // because Operation struct can't be stored on storage. + IGovernance.Call call; + bytes32 predecessor; + bytes32 salt; + + // Save one value to determine whether reentrancy already happen. + bool alreadyReentered; + + enum FunctionToCall { + Unset, + Execute, + ExecuteInstant, + Cancel + } + + FunctionToCall functionToCall; + + function initialize( + IGovernance _governance, + IGovernance.Operation memory _op, + FunctionToCall _functionToCall + ) external { + governance = _governance; + require(_op.calls.length == 1, "Only 1 calls supported"); + call = _op.calls[0]; + predecessor = _op.predecessor; + salt = _op.salt; + + functionToCall = _functionToCall; + } + + fallback() external payable { + if (!alreadyReentered) { + alreadyReentered = true; + IGovernance.Call[] memory calls = new IGovernance.Call[](1); + calls[0] = call; + IGovernance.Operation memory op = IGovernance.Operation({ + calls: calls, + predecessor: predecessor, + salt: salt + }); + + if (functionToCall == ReenterGovernance.FunctionToCall.Execute) { + governance.execute(op); + } else if (functionToCall == ReenterGovernance.FunctionToCall.ExecuteInstant) { + governance.executeInstant(op); + } else if (functionToCall == ReenterGovernance.FunctionToCall.Cancel) { + bytes32 opId = governance.hashOperation(op); + governance.cancel(opId); + } else { + revert("Unset function to call"); + } + } + } +} diff --git a/contracts/l1-contracts/dev-contracts/test/ReenterL1ERC20Bridge.sol b/contracts/l1-contracts/dev-contracts/test/ReenterL1ERC20Bridge.sol new file mode 100644 index 0000000..aa63c82 --- /dev/null +++ b/contracts/l1-contracts/dev-contracts/test/ReenterL1ERC20Bridge.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {IL1ERC20Bridge} from "../../bridge/interfaces/IL1ERC20Bridge.sol"; + +contract ReenterL1ERC20Bridge { + // add this to be excluded from coverage report + function test() internal virtual {} + + IL1ERC20Bridge l1Erc20Bridge; + + enum FunctionToCall { + Unset, + LegacyDeposit, + Deposit, + ClaimFailedDeposit, + FinalizeWithdrawal + } + + FunctionToCall functionToCall; + + function setBridge(IL1ERC20Bridge _l1Erc20Bridge) external { + l1Erc20Bridge = _l1Erc20Bridge; + } + + function setFunctionToCall(FunctionToCall _functionToCall) external { + functionToCall = _functionToCall; + } + + fallback() external payable { + if (functionToCall == FunctionToCall.LegacyDeposit) { + l1Erc20Bridge.deposit({ + _l2Receiver: address(0), + _l1Token: address(0), + _amount: 0, + _l2TxGasLimit: 0, + _l2TxGasPerPubdataByte: 0, + _refundRecipient: address(0) + }); + } else if (functionToCall == FunctionToCall.Deposit) { + l1Erc20Bridge.deposit({ + _l2Receiver: address(0), + _l1Token: address(0), + _amount: 0, + _l2TxGasLimit: 0, + _l2TxGasPerPubdataByte: 0, + _refundRecipient: address(0) + }); + } else if (functionToCall == FunctionToCall.ClaimFailedDeposit) { + bytes32[] memory merkleProof; + l1Erc20Bridge.claimFailedDeposit({ + _depositSender: address(0), + _l1Token: address(0), + _l2TxHash: bytes32(0), + _l2BatchNumber: 0, + _l2MessageIndex: 0, + _l2TxNumberInBatch: 0, + _merkleProof: merkleProof + }); + } else if (functionToCall == FunctionToCall.FinalizeWithdrawal) { + bytes32[] memory merkleProof; + l1Erc20Bridge.finalizeWithdrawal({ + _l2BatchNumber: 0, + _l2MessageIndex: 0, + _l2TxNumberInBatch: 0, + _message: bytes(""), + _merkleProof: merkleProof + }); + } else { + revert("Unset function to call"); + } + } + + receive() external payable { + // revert("Receive not allowed"); + } +} diff --git a/contracts/l1-contracts/dev-contracts/test/TestExecutor.sol b/contracts/l1-contracts/dev-contracts/test/TestExecutor.sol new file mode 100644 index 0000000..8da6425 --- /dev/null +++ b/contracts/l1-contracts/dev-contracts/test/TestExecutor.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT + +import {ExecutorFacet} from "../../state-transition/chain-deps/facets/Executor.sol"; + +pragma solidity 0.8.24; + +contract TestExecutor is ExecutorFacet { + /// @dev Since we want to test the blob functionality we want mock the calls to the blobhash opcode. + function _getBlobVersionedHash(uint256 _index) internal view virtual override returns (bytes32 versionedHash) { + (bool success, bytes memory data) = s.blobVersionedHashRetriever.staticcall(abi.encode(_index)); + require(success, "vc"); + versionedHash = abi.decode(data, (bytes32)); + } +} diff --git a/contracts/l1-contracts/dev-contracts/test/UncheckedMathTest.sol b/contracts/l1-contracts/dev-contracts/test/UncheckedMathTest.sol new file mode 100644 index 0000000..ada7586 --- /dev/null +++ b/contracts/l1-contracts/dev-contracts/test/UncheckedMathTest.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {UncheckedMath} from "../../common/libraries/UncheckedMath.sol"; + +contract UncheckedMathTest { + function uncheckedInc(uint256 _number) external pure returns (uint256) { + return UncheckedMath.uncheckedInc(_number); + } + + function uncheckedAdd(uint256 _lhs, uint256 _rhs) external pure returns (uint256) { + return UncheckedMath.uncheckedAdd(_lhs, _rhs); + } +} diff --git a/contracts/l1-contracts/dev-contracts/test/UnsafeBytesTest.sol b/contracts/l1-contracts/dev-contracts/test/UnsafeBytesTest.sol new file mode 100644 index 0000000..a6ee952 --- /dev/null +++ b/contracts/l1-contracts/dev-contracts/test/UnsafeBytesTest.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {UnsafeBytes} from "../../common/libraries/UnsafeBytes.sol"; + +contract UnsafeBytesTest { + // add this to be excluded from coverage report + function test() internal virtual {} + + using UnsafeBytes for bytes; + + function readUint32(bytes memory _bytes, uint256 _start) external pure returns (uint32 readValue, uint256 offset) { + return _bytes.readUint32(_start); + } + + function readAddress( + bytes memory _bytes, + uint256 _start + ) external pure returns (address readValue, uint256 offset) { + return _bytes.readAddress(_start); + } + + function readUint256( + bytes memory _bytes, + uint256 _start + ) external pure returns (uint256 readValue, uint256 offset) { + return _bytes.readUint256(_start); + } + + function readBytes32( + bytes memory _bytes, + uint256 _start + ) external pure returns (bytes32 readValue, uint256 offset) { + return _bytes.readBytes32(_start); + } +} diff --git a/contracts/l1-contracts/dev-contracts/test/VerifierRecursiveTest.sol b/contracts/l1-contracts/dev-contracts/test/VerifierRecursiveTest.sol new file mode 100644 index 0000000..e2ea7cb --- /dev/null +++ b/contracts/l1-contracts/dev-contracts/test/VerifierRecursiveTest.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {Verifier} from "../../state-transition/Verifier.sol"; + +/// @author Matter Labs +contract VerifierRecursiveTest is Verifier { + // add this to be excluded from coverage report + function test() internal virtual {} + + function _loadVerificationKey() internal pure override { + assembly { + // gate setup commitments + mstore(VK_GATE_SETUP_0_X_SLOT, 0x046e45fd137982bd0f6cf731b4650d2d520e8d675827744e1edf1308583599bb) + mstore(VK_GATE_SETUP_0_Y_SLOT, 0x177f14d16b716d4298be5e07b83add3fb61ff1ee08dce19f9a54fa8f04937f7e) + mstore(VK_GATE_SETUP_1_X_SLOT, 0x169ad5156d25b56f7b67ea6382f88b845ed5bae5b91aacfe51d8f0716afff2fb) + mstore(VK_GATE_SETUP_1_Y_SLOT, 0x2406e3268e4d5fa672142998ecf834034638a4a6f8b5e90205552c6aa1dde163) + mstore(VK_GATE_SETUP_2_X_SLOT, 0x05fd0ce0fdc590938d29c738c8dc956b32ca8e69c3babfbb49dc1c13a6d9a8d4) + mstore(VK_GATE_SETUP_2_Y_SLOT, 0x0a27dac323a04dd319d9805be879875c95063d0a55c96214cd45c913fba84460) + mstore(VK_GATE_SETUP_3_X_SLOT, 0x0d58a2a86b208a4976beb9bfd918514d448656e0ee66175eb344a4a17bba99f8) + mstore(VK_GATE_SETUP_3_Y_SLOT, 0x215fa609a1a425b84c9dc218c6cf999596d9eba6d35597ad7aaf2d077a6616ed) + mstore(VK_GATE_SETUP_4_X_SLOT, 0x1a26e6deccf91174ab13613363eb4939680828f0c6031f5039f9e6f264afa68c) + mstore(VK_GATE_SETUP_4_Y_SLOT, 0x1f5b2d6bffac1839edfd02cd0e41acc411f0ecbf6c5c4b1da0e12b68b99cb25d) + mstore(VK_GATE_SETUP_5_X_SLOT, 0x09b71be2e8a45dcbe7654cf369c4f1f2e7eab4b97869a469fb7a149d989f7226) + mstore(VK_GATE_SETUP_5_Y_SLOT, 0x197e1e2cefbd4f99558b89ca875e01fec0f14f05e5128bd869c87d6bf2f307fa) + mstore(VK_GATE_SETUP_6_X_SLOT, 0x0d7cef745da686fd44760403d452d72be504bb41b0a7f4fbe973a07558893871) + mstore(VK_GATE_SETUP_6_Y_SLOT, 0x1e9a863307cdfd3fdcf119f72279ddfda08b6f23c3672e8378dbb9d548734c29) + mstore(VK_GATE_SETUP_7_X_SLOT, 0x16af3f5d978446fdb37d84f5cf12e59f5c1088bde23f8260c0bb6792c5f78e99) + mstore(VK_GATE_SETUP_7_Y_SLOT, 0x167d3aeee50c0e53fd1e8a33941a806a34cfae5dc8b66578486e5d7207b5d546) + + // gate selectors commitments + mstore(VK_GATE_SELECTORS_0_X_SLOT, 0x1addc8e154c74bed403dc19558096ce22f1ceb2c656a2a5e85e56d2be6580ed1) + mstore(VK_GATE_SELECTORS_0_Y_SLOT, 0x1420d38f0ef206828efc36d0f5ad2b4d85fe768097f358fc671b7b3ec0239234) + mstore(VK_GATE_SELECTORS_1_X_SLOT, 0x2d5c06d0c8aa6a3520b8351f82341affcbb1a0bf27bceb9bab175e3e1d38cf47) + mstore(VK_GATE_SELECTORS_1_Y_SLOT, 0x0ff8d923a0374308147f6dd4fc513f6d0640f5df699f4836825ef460df3f8d6a) + + // permutation commitments + mstore(VK_PERMUTATION_0_X_SLOT, 0x1de8943a8f67d9f6fcbda10a1f37a82de9e9ffd0a0102ea5ce0ce6dd13b4031b) + mstore(VK_PERMUTATION_0_Y_SLOT, 0x1e04b0824853ab5d7c3412a217a1c5b88a2b4011be7e7e849485be8ed7332e41) + mstore(VK_PERMUTATION_1_X_SLOT, 0x2aa1817b9cc40b6cc7a7b3f832f3267580f9fb8e539666c00541e1a77e34a3da) + mstore(VK_PERMUTATION_1_Y_SLOT, 0x0edb3cde226205b01212fc1861303c49ef3ff66f060b5833dc9a3f661ef31dd9) + mstore(VK_PERMUTATION_2_X_SLOT, 0x13f5ae93c8eccc1455a0095302923442d4b0b3c8233d66ded99ffcf2ad641c27) + mstore(VK_PERMUTATION_2_Y_SLOT, 0x2dd42d42ccdea8b1901435ace12bc9e52c7dbbeb409d20c517ba942ed0cc7519) + mstore(VK_PERMUTATION_3_X_SLOT, 0x1a15a70a016be11af71e46e9c8a8d31ece32a7e657ae90356dd9535e6566645f) + mstore(VK_PERMUTATION_3_Y_SLOT, 0x0381d23e115521c6fc233c5346f79a6777bfa8871b7ee623d990cdcb5d8c3ce1) + + // lookup tables commitments + mstore(VK_LOOKUP_TABLE_0_X_SLOT, 0x2c513ed74d9d57a5ec901e074032741036353a2c4513422e96e7b53b302d765b) + mstore(VK_LOOKUP_TABLE_0_Y_SLOT, 0x04dd964427e430f16004076d708c0cb21e225056cc1d57418cfbd3d472981468) + mstore(VK_LOOKUP_TABLE_1_X_SLOT, 0x1ea83e5e65c6f8068f4677e2911678cf329b28259642a32db1f14b8347828aac) + mstore(VK_LOOKUP_TABLE_1_Y_SLOT, 0x1d22bc884a2da4962a893ba8de13f57aaeb785ed52c5e686994839cab8f7475d) + mstore(VK_LOOKUP_TABLE_2_X_SLOT, 0x0b2e7212d0d9cff26d0bdf3d79b2cac029a25dfeb1cafdf49e2349d7db348d89) + mstore(VK_LOOKUP_TABLE_2_Y_SLOT, 0x1301f9b252419ea240eb67fda720ca0b16d92364027285f95e9b1349490fa283) + mstore(VK_LOOKUP_TABLE_3_X_SLOT, 0x02f7b99fdfa5b418548c2d777785820e02383cfc87e7085e280a375a358153bf) + mstore(VK_LOOKUP_TABLE_3_Y_SLOT, 0x09d004fe08dc4d19c382df36fad22ef676185663543703e6a4b40203e50fd8a6) + + // lookup selector commitment + mstore(VK_LOOKUP_SELECTOR_X_SLOT, 0x1641f5d312e6f62720b1e6cd1d1be5bc0e69d10d20a12dc97ff04e2107e10ccc) + mstore(VK_LOOKUP_SELECTOR_Y_SLOT, 0x277f435d376acc3261ef9d5748e6705086214daf46d04edc80fbd657f8d9e73d) + + // table type commitment + mstore(VK_LOOKUP_TABLE_TYPE_X_SLOT, 0x1b5f1cfddd6713cf25d9e6850a1b3fe80d6ef7fe2c67248f25362d5f9b31893c) + mstore(VK_LOOKUP_TABLE_TYPE_Y_SLOT, 0x0945076de03a0d240067e5f02b8fc11eaa589df3343542576eb59fdb3ecb57e0) + + // flag for using recursive part + mstore(VK_RECURSIVE_FLAG_SLOT, 1) + } + } +} diff --git a/contracts/l1-contracts/dev-contracts/test/VerifierTest.sol b/contracts/l1-contracts/dev-contracts/test/VerifierTest.sol new file mode 100644 index 0000000..ef60794 --- /dev/null +++ b/contracts/l1-contracts/dev-contracts/test/VerifierTest.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {Verifier} from "../../state-transition/Verifier.sol"; + +/// @author Matter Labs +contract VerifierTest is Verifier { + // add this to be excluded from coverage report + function test() internal virtual {} + + function _loadVerificationKey() internal pure override { + assembly { + // gate setup commitments + mstore(VK_GATE_SETUP_0_X_SLOT, 0x046e45fd137982bd0f6cf731b4650d2d520e8d675827744e1edf1308583599bb) + mstore(VK_GATE_SETUP_0_Y_SLOT, 0x177f14d16b716d4298be5e07b83add3fb61ff1ee08dce19f9a54fa8f04937f7e) + mstore(VK_GATE_SETUP_1_X_SLOT, 0x169ad5156d25b56f7b67ea6382f88b845ed5bae5b91aacfe51d8f0716afff2fb) + mstore(VK_GATE_SETUP_1_Y_SLOT, 0x2406e3268e4d5fa672142998ecf834034638a4a6f8b5e90205552c6aa1dde163) + mstore(VK_GATE_SETUP_2_X_SLOT, 0x05fd0ce0fdc590938d29c738c8dc956b32ca8e69c3babfbb49dc1c13a6d9a8d4) + mstore(VK_GATE_SETUP_2_Y_SLOT, 0x0a27dac323a04dd319d9805be879875c95063d0a55c96214cd45c913fba84460) + mstore(VK_GATE_SETUP_3_X_SLOT, 0x0d58a2a86b208a4976beb9bfd918514d448656e0ee66175eb344a4a17bba99f8) + mstore(VK_GATE_SETUP_3_Y_SLOT, 0x215fa609a1a425b84c9dc218c6cf999596d9eba6d35597ad7aaf2d077a6616ed) + mstore(VK_GATE_SETUP_4_X_SLOT, 0x1a26e6deccf91174ab13613363eb4939680828f0c6031f5039f9e6f264afa68c) + mstore(VK_GATE_SETUP_4_Y_SLOT, 0x1f5b2d6bffac1839edfd02cd0e41acc411f0ecbf6c5c4b1da0e12b68b99cb25d) + mstore(VK_GATE_SETUP_5_X_SLOT, 0x09b71be2e8a45dcbe7654cf369c4f1f2e7eab4b97869a469fb7a149d989f7226) + mstore(VK_GATE_SETUP_5_Y_SLOT, 0x197e1e2cefbd4f99558b89ca875e01fec0f14f05e5128bd869c87d6bf2f307fa) + mstore(VK_GATE_SETUP_6_X_SLOT, 0x0d7cef745da686fd44760403d452d72be504bb41b0a7f4fbe973a07558893871) + mstore(VK_GATE_SETUP_6_Y_SLOT, 0x1e9a863307cdfd3fdcf119f72279ddfda08b6f23c3672e8378dbb9d548734c29) + mstore(VK_GATE_SETUP_7_X_SLOT, 0x16af3f5d978446fdb37d84f5cf12e59f5c1088bde23f8260c0bb6792c5f78e99) + mstore(VK_GATE_SETUP_7_Y_SLOT, 0x167d3aeee50c0e53fd1e8a33941a806a34cfae5dc8b66578486e5d7207b5d546) + + // gate selectors commitments + mstore(VK_GATE_SELECTORS_0_X_SLOT, 0x1addc8e154c74bed403dc19558096ce22f1ceb2c656a2a5e85e56d2be6580ed1) + mstore(VK_GATE_SELECTORS_0_Y_SLOT, 0x1420d38f0ef206828efc36d0f5ad2b4d85fe768097f358fc671b7b3ec0239234) + mstore(VK_GATE_SELECTORS_1_X_SLOT, 0x2d5c06d0c8aa6a3520b8351f82341affcbb1a0bf27bceb9bab175e3e1d38cf47) + mstore(VK_GATE_SELECTORS_1_Y_SLOT, 0x0ff8d923a0374308147f6dd4fc513f6d0640f5df699f4836825ef460df3f8d6a) + + // permutation commitments + mstore(VK_PERMUTATION_0_X_SLOT, 0x1de8943a8f67d9f6fcbda10a1f37a82de9e9ffd0a0102ea5ce0ce6dd13b4031b) + mstore(VK_PERMUTATION_0_Y_SLOT, 0x1e04b0824853ab5d7c3412a217a1c5b88a2b4011be7e7e849485be8ed7332e41) + mstore(VK_PERMUTATION_1_X_SLOT, 0x2aa1817b9cc40b6cc7a7b3f832f3267580f9fb8e539666c00541e1a77e34a3da) + mstore(VK_PERMUTATION_1_Y_SLOT, 0x0edb3cde226205b01212fc1861303c49ef3ff66f060b5833dc9a3f661ef31dd9) + mstore(VK_PERMUTATION_2_X_SLOT, 0x13f5ae93c8eccc1455a0095302923442d4b0b3c8233d66ded99ffcf2ad641c27) + mstore(VK_PERMUTATION_2_Y_SLOT, 0x2dd42d42ccdea8b1901435ace12bc9e52c7dbbeb409d20c517ba942ed0cc7519) + mstore(VK_PERMUTATION_3_X_SLOT, 0x1a15a70a016be11af71e46e9c8a8d31ece32a7e657ae90356dd9535e6566645f) + mstore(VK_PERMUTATION_3_Y_SLOT, 0x0381d23e115521c6fc233c5346f79a6777bfa8871b7ee623d990cdcb5d8c3ce1) + + // lookup tables commitments + mstore(VK_LOOKUP_TABLE_0_X_SLOT, 0x2c513ed74d9d57a5ec901e074032741036353a2c4513422e96e7b53b302d765b) + mstore(VK_LOOKUP_TABLE_0_Y_SLOT, 0x04dd964427e430f16004076d708c0cb21e225056cc1d57418cfbd3d472981468) + mstore(VK_LOOKUP_TABLE_1_X_SLOT, 0x1ea83e5e65c6f8068f4677e2911678cf329b28259642a32db1f14b8347828aac) + mstore(VK_LOOKUP_TABLE_1_Y_SLOT, 0x1d22bc884a2da4962a893ba8de13f57aaeb785ed52c5e686994839cab8f7475d) + mstore(VK_LOOKUP_TABLE_2_X_SLOT, 0x0b2e7212d0d9cff26d0bdf3d79b2cac029a25dfeb1cafdf49e2349d7db348d89) + mstore(VK_LOOKUP_TABLE_2_Y_SLOT, 0x1301f9b252419ea240eb67fda720ca0b16d92364027285f95e9b1349490fa283) + mstore(VK_LOOKUP_TABLE_3_X_SLOT, 0x02f7b99fdfa5b418548c2d777785820e02383cfc87e7085e280a375a358153bf) + mstore(VK_LOOKUP_TABLE_3_Y_SLOT, 0x09d004fe08dc4d19c382df36fad22ef676185663543703e6a4b40203e50fd8a6) + + // lookup selector commitment + mstore(VK_LOOKUP_SELECTOR_X_SLOT, 0x1641f5d312e6f62720b1e6cd1d1be5bc0e69d10d20a12dc97ff04e2107e10ccc) + mstore(VK_LOOKUP_SELECTOR_Y_SLOT, 0x277f435d376acc3261ef9d5748e6705086214daf46d04edc80fbd657f8d9e73d) + + // table type commitment + mstore(VK_LOOKUP_TABLE_TYPE_X_SLOT, 0x1b5f1cfddd6713cf25d9e6850a1b3fe80d6ef7fe2c67248f25362d5f9b31893c) + mstore(VK_LOOKUP_TABLE_TYPE_Y_SLOT, 0x0945076de03a0d240067e5f02b8fc11eaa589df3343542576eb59fdb3ecb57e0) + + // flag for using recursive part + mstore(VK_RECURSIVE_FLAG_SLOT, 0) + } + } +} diff --git a/contracts/l1-contracts/governance/ChainAdmin.sol b/contracts/l1-contracts/governance/ChainAdmin.sol new file mode 100644 index 0000000..4d9ff85 --- /dev/null +++ b/contracts/l1-contracts/governance/ChainAdmin.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {Ownable2Step} from "@openzeppelin/contracts-v4/access/Ownable2Step.sol"; +import {IChainAdmin} from "./IChainAdmin.sol"; +import {IAdmin} from "../state-transition/chain-interfaces/IAdmin.sol"; +import {NoCallsProvided, Unauthorized, ZeroAddress} from "../common/L1ContractErrors.sol"; + +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +/// @notice The contract is designed to hold the `admin` role in ZKSync Chain (State Transition) contracts. +/// The owner of the contract can perform any external calls and also save the information needed for +/// the blockchain node to accept the protocol upgrade. Another role - `tokenMultiplierSetter` can be used in the contract +/// to change the base token gas price in the Chain contract. +contract ChainAdmin is IChainAdmin, Ownable2Step { + /// @notice Mapping of protocol versions to their expected upgrade timestamps. + /// @dev Needed for the offchain node administration to know when to start building batches with the new protocol version. + mapping(uint256 protocolVersion => uint256 upgradeTimestamp) public protocolVersionToUpgradeTimestamp; + + /// @notice The address which can call `setTokenMultiplier` function to change the base token gas price in the Chain contract. + /// @dev The token base price can be changed quite often, so the private key for this role is supposed to be stored in the node + /// and used by the automated service in a way similar to the sequencer workflow. + address public tokenMultiplierSetter; + + constructor(address _initialOwner, address _initialTokenMultiplierSetter) { + if (_initialOwner == address(0)) { + revert ZeroAddress(); + } + _transferOwnership(_initialOwner); + // Can be zero if no one has this permission. + tokenMultiplierSetter = _initialTokenMultiplierSetter; + emit NewTokenMultiplierSetter(address(0), _initialTokenMultiplierSetter); + } + + /// @notice Updates the address responsible for setting token multipliers on the Chain contract . + /// @param _tokenMultiplierSetter The new address to be set as the token multiplier setter. + function setTokenMultiplierSetter(address _tokenMultiplierSetter) external onlyOwner { + emit NewTokenMultiplierSetter(tokenMultiplierSetter, _tokenMultiplierSetter); + tokenMultiplierSetter = _tokenMultiplierSetter; + } + + /// @notice Set the expected upgrade timestamp for a specific protocol version. + /// @param _protocolVersion The ZKsync chain protocol version. + /// @param _upgradeTimestamp The timestamp at which the chain node should expect the upgrade to happen. + function setUpgradeTimestamp(uint256 _protocolVersion, uint256 _upgradeTimestamp) external onlyOwner { + protocolVersionToUpgradeTimestamp[_protocolVersion] = _upgradeTimestamp; + emit UpdateUpgradeTimestamp(_protocolVersion, _upgradeTimestamp); + } + + /// @notice Execute multiple calls as part of contract administration. + /// @param _calls Array of Call structures defining target, value, and data for each call. + /// @param _requireSuccess If true, reverts transaction on any call failure. + /// @dev Intended for batch processing of contract interactions, managing gas efficiency and atomicity of operations. + function multicall(Call[] calldata _calls, bool _requireSuccess) external payable onlyOwner { + if (_calls.length == 0) { + revert NoCallsProvided(); + } + // solhint-disable-next-line gas-length-in-loops + for (uint256 i = 0; i < _calls.length; ++i) { + // slither-disable-next-line arbitrary-send-eth + (bool success, bytes memory returnData) = _calls[i].target.call{value: _calls[i].value}(_calls[i].data); + if (_requireSuccess && !success) { + // Propagate an error if the call fails. + assembly { + revert(add(returnData, 0x20), mload(returnData)) + } + } + emit CallExecuted(_calls[i], success, returnData); + } + } + + /// @notice Sets the token multiplier in the specified Chain contract. + /// @param _chainContract The chain contract address where the token multiplier will be set. + /// @param _nominator The numerator part of the token multiplier. + /// @param _denominator The denominator part of the token multiplier. + function setTokenMultiplier(IAdmin _chainContract, uint128 _nominator, uint128 _denominator) external { + if (msg.sender != tokenMultiplierSetter) { + revert Unauthorized(msg.sender); + } + _chainContract.setTokenMultiplier(_nominator, _denominator); + } + + /// @dev Contract might receive/hold ETH as part of the maintenance process. + receive() external payable {} +} diff --git a/contracts/l1-contracts/governance/Governance.sol b/contracts/l1-contracts/governance/Governance.sol new file mode 100644 index 0000000..790b79a --- /dev/null +++ b/contracts/l1-contracts/governance/Governance.sol @@ -0,0 +1,293 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {Ownable2Step} from "@openzeppelin/contracts-v4/access/Ownable2Step.sol"; +import {IGovernance} from "./IGovernance.sol"; +import {ZeroAddress, Unauthorized, OperationMustBeReady, OperationMustBePending, OperationExists, InvalidDelay, PreviousOperationNotExecuted} from "../common/L1ContractErrors.sol"; + +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +/// @dev Contract design is inspired by OpenZeppelin TimelockController and in-house Diamond Proxy upgrade mechanism. +/// @notice This contract manages operations (calls with preconditions) for governance tasks. +/// The contract allows for operations to be scheduled, executed, and canceled with +/// appropriate permissions and delays. It is used for managing and coordinating upgrades +/// and changes in all ZKsync hyperchain governed contracts. +/// +/// Operations can be proposed as either fully transparent upgrades with on-chain data, +/// or "shadow" upgrades where upgrade data is not published on-chain before execution. Proposed operations +/// are subject to a delay before they can be executed, but they can be executed instantly +/// with the security council’s permission. +contract Governance is IGovernance, Ownable2Step { + /// @notice A constant representing the timestamp for completed operations. + uint256 internal constant EXECUTED_PROPOSAL_TIMESTAMP = uint256(1); + + /// @notice The address of the security council. + /// @dev It is supposed to be multisig contract. + address public securityCouncil; + + /// @notice A mapping to store timestamps when each operation will be ready for execution. + /// @dev - 0 means the operation is not created. + /// @dev - 1 (EXECUTED_PROPOSAL_TIMESTAMP) means the operation is already executed. + /// @dev - any other value means timestamp in seconds when the operation will be ready for execution. + mapping(bytes32 operationId => uint256 executionTimestamp) public timestamps; + + /// @notice The minimum delay in seconds for operations to be ready for execution. + uint256 public minDelay; + + /// @notice Initializes the contract with the admin address, security council address, and minimum delay. + /// @param _admin The address to be assigned as the admin of the contract. + /// @param _securityCouncil The address to be assigned as the security council of the contract. + /// @param _minDelay The initial minimum delay (in seconds) to be set for operations. + /// @dev We allow for a zero address for _securityCouncil because it can be set later + constructor(address _admin, address _securityCouncil, uint256 _minDelay) { + if (_admin == address(0)) { + revert ZeroAddress(); + } + + _transferOwnership(_admin); + + securityCouncil = _securityCouncil; + emit ChangeSecurityCouncil(address(0), _securityCouncil); + + minDelay = _minDelay; + emit ChangeMinDelay(0, _minDelay); + } + + /*////////////////////////////////////////////////////////////// + MODIFIERS + //////////////////////////////////////////////////////////////*/ + + /// @notice Checks that the message sender is contract itself. + modifier onlySelf() { + if (msg.sender != address(this)) { + revert Unauthorized(msg.sender); + } + _; + } + + /// @notice Checks that the message sender is an active security council. + modifier onlySecurityCouncil() { + if (msg.sender != securityCouncil) { + revert Unauthorized(msg.sender); + } + _; + } + + /// @notice Checks that the message sender is an active owner or an active security council. + modifier onlyOwnerOrSecurityCouncil() { + if (msg.sender != owner() && msg.sender != securityCouncil) { + revert Unauthorized(msg.sender); + } + _; + } + + /*////////////////////////////////////////////////////////////// + OPERATION GETTERS + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns whether an id corresponds to a registered operation. This + /// includes Waiting, Ready, and Done operations. + function isOperation(bytes32 _id) public view returns (bool) { + return getOperationState(_id) != OperationState.Unset; + } + + /// @dev Returns whether an operation is pending or not. Note that a "pending" operation may also be "ready". + function isOperationPending(bytes32 _id) public view returns (bool) { + OperationState state = getOperationState(_id); + return state == OperationState.Waiting || state == OperationState.Ready; + } + + /// @dev Returns whether an operation is ready for execution. Note that a "ready" operation is also "pending". + function isOperationReady(bytes32 _id) public view returns (bool) { + return getOperationState(_id) == OperationState.Ready; + } + + /// @dev Returns whether an operation is done or not. + function isOperationDone(bytes32 _id) public view returns (bool) { + return getOperationState(_id) == OperationState.Done; + } + + /// @dev Returns operation state. + function getOperationState(bytes32 _id) public view returns (OperationState) { + uint256 timestamp = timestamps[_id]; + if (timestamp == 0) { + return OperationState.Unset; + } else if (timestamp == EXECUTED_PROPOSAL_TIMESTAMP) { + return OperationState.Done; + } else if (timestamp > block.timestamp) { + return OperationState.Waiting; + } else { + return OperationState.Ready; + } + } + + /*////////////////////////////////////////////////////////////// + SCHEDULING CALLS + //////////////////////////////////////////////////////////////*/ + + /// @notice Propose a fully transparent upgrade, providing upgrade data on-chain. + /// @notice The owner will be able to execute the proposal either: + /// - With a `delay` timelock on its own. + /// - With security council instantly. + /// @dev Only the current owner can propose an upgrade. + /// @param _operation The operation parameters will be executed with the upgrade. + /// @param _delay The delay time (in seconds) after which the proposed upgrade can be executed by the owner. + function scheduleTransparent(Operation calldata _operation, uint256 _delay) external onlyOwner { + bytes32 id = hashOperation(_operation); + _schedule(id, _delay); + emit TransparentOperationScheduled(id, _delay, _operation); + } + + /// @notice Propose "shadow" upgrade, upgrade data is not publishing on-chain. + /// @notice The owner will be able to execute the proposal either: + /// - With a `delay` timelock on its own. + /// - With security council instantly. + /// @dev Only the current owner can propose an upgrade. + /// @param _id The operation hash (see `hashOperation` function) + /// @param _delay The delay time (in seconds) after which the proposed upgrade may be executed by the owner. + function scheduleShadow(bytes32 _id, uint256 _delay) external onlyOwner { + _schedule(_id, _delay); + emit ShadowOperationScheduled(_id, _delay); + } + + /*////////////////////////////////////////////////////////////// + CANCELING CALLS + //////////////////////////////////////////////////////////////*/ + + /// @dev Cancel the scheduled operation. + /// @dev Only owner can call this function. + /// @param _id Proposal id value (see `hashOperation`) + function cancel(bytes32 _id) external onlyOwner { + if (!isOperationPending(_id)) { + revert OperationMustBePending(); + } + delete timestamps[_id]; + emit OperationCancelled(_id); + } + + /*////////////////////////////////////////////////////////////// + EXECUTING CALLS + //////////////////////////////////////////////////////////////*/ + + /// @notice Executes the scheduled operation after the delay passed. + /// @dev Both the owner and security council may execute delayed operations. + /// @param _operation The operation parameters will be executed with the upgrade. + // slither-disable-next-line reentrancy-eth + function execute(Operation calldata _operation) external payable onlyOwnerOrSecurityCouncil { + bytes32 id = hashOperation(_operation); + // Check if the predecessor operation is completed. + _checkPredecessorDone(_operation.predecessor); + // Ensure that the operation is ready to proceed. + if (!isOperationReady(id)) { + revert OperationMustBeReady(); + } + // Execute operation. + // slither-disable-next-line reentrancy-eth + _execute(_operation.calls); + // Reconfirming that the operation is still ready after execution. + // This is needed to avoid unexpected reentrancy attacks of re-executing the same operation. + if (!isOperationReady(id)) { + revert OperationMustBeReady(); + } + // Set operation to be done + timestamps[id] = EXECUTED_PROPOSAL_TIMESTAMP; + emit OperationExecuted(id); + } + + /// @notice Executes the scheduled operation with the security council instantly. + /// @dev Only the security council may execute an operation instantly. + /// @param _operation The operation parameters will be executed with the upgrade. + // slither-disable-next-line reentrancy-eth + function executeInstant(Operation calldata _operation) external payable onlySecurityCouncil { + bytes32 id = hashOperation(_operation); + // Check if the predecessor operation is completed. + _checkPredecessorDone(_operation.predecessor); + // Ensure that the operation is in a pending state before proceeding. + if (!isOperationPending(id)) { + revert OperationMustBePending(); + } + // Execute operation. + // slither-disable-next-line reentrancy-eth + _execute(_operation.calls); + // Reconfirming that the operation is still pending before execution. + // This is needed to avoid unexpected reentrancy attacks of re-executing the same operation. + if (!isOperationPending(id)) { + revert OperationMustBePending(); + } + // Set operation to be done + timestamps[id] = EXECUTED_PROPOSAL_TIMESTAMP; + emit OperationExecuted(id); + } + + /// @dev Returns the identifier of an operation. + /// @param _operation The operation object to compute the identifier for. + function hashOperation(Operation calldata _operation) public pure returns (bytes32) { + return keccak256(abi.encode(_operation)); + } + + /*////////////////////////////////////////////////////////////// + HELPERS + //////////////////////////////////////////////////////////////*/ + + /// @dev Schedule an operation that is to become valid after a given delay. + /// @param _id The operation hash (see `hashOperation` function) + /// @param _delay The delay time (in seconds) after which the proposed upgrade can be executed by the owner. + function _schedule(bytes32 _id, uint256 _delay) internal { + if (isOperation(_id)) { + revert OperationExists(); + } + if (_delay < minDelay) { + revert InvalidDelay(); + } + + timestamps[_id] = block.timestamp + _delay; + } + + /// @dev Execute an operation's calls. + /// @param _calls The array of calls to be executed. + function _execute(Call[] calldata _calls) internal { + // We disable this check because calldata array length is cheap. + // solhint-disable-next-line gas-length-in-loops + for (uint256 i = 0; i < _calls.length; ++i) { + // slither-disable-next-line arbitrary-send-eth + (bool success, bytes memory returnData) = _calls[i].target.call{value: _calls[i].value}(_calls[i].data); + if (!success) { + // Propagate an error if the call fails. + assembly { + revert(add(returnData, 0x20), mload(returnData)) + } + } + } + } + + /// @notice Verifies if the predecessor operation is completed. + /// @param _predecessorId The hash of the operation that should be completed. + /// @dev Doesn't check the operation to be complete if the input is zero. + function _checkPredecessorDone(bytes32 _predecessorId) internal view { + if (_predecessorId != bytes32(0) && !isOperationDone(_predecessorId)) { + revert PreviousOperationNotExecuted(); + } + } + + /*////////////////////////////////////////////////////////////// + SELF UPGRADES + //////////////////////////////////////////////////////////////*/ + + /// @dev Changes the minimum timelock duration for future operations. + /// @param _newDelay The new minimum delay time (in seconds) for future operations. + function updateDelay(uint256 _newDelay) external onlySelf { + emit ChangeMinDelay(minDelay, _newDelay); + minDelay = _newDelay; + } + + /// @dev Updates the address of the security council. + /// @param _newSecurityCouncil The address of the new security council. + function updateSecurityCouncil(address _newSecurityCouncil) external onlySelf { + emit ChangeSecurityCouncil(securityCouncil, _newSecurityCouncil); + securityCouncil = _newSecurityCouncil; + } + + /// @dev Contract might receive/hold ETH as part of the maintenance process. + receive() external payable {} +} diff --git a/contracts/l1-contracts/governance/IChainAdmin.sol b/contracts/l1-contracts/governance/IChainAdmin.sol new file mode 100644 index 0000000..d5d8f11 --- /dev/null +++ b/contracts/l1-contracts/governance/IChainAdmin.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {IAdmin} from "../state-transition/chain-interfaces/IAdmin.sol"; + +/// @title ChainAdmin contract interface +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +interface IChainAdmin { + /// @dev Represents a call to be made during multicall. + /// @param target The address to which the call will be made. + /// @param value The amount of Ether (in wei) to be sent along with the call. + /// @param data The calldata to be executed on the `target` address. + struct Call { + address target; + uint256 value; + bytes data; + } + + /// @notice Emitted when the expected upgrade timestamp for a specific protocol version is set. + event UpdateUpgradeTimestamp(uint256 indexed _protocolVersion, uint256 _upgradeTimestamp); + + /// @notice Emitted when the call is executed from the contract. + event CallExecuted(Call _call, bool _success, bytes _returnData); + + /// @notice Emitted when the new token multiplier address is set. + event NewTokenMultiplierSetter(address _oldTokenMultiplierSetter, address _newTokenMultiplierSetter); + + function setTokenMultiplierSetter(address _tokenMultiplierSetter) external; + + function setUpgradeTimestamp(uint256 _protocolVersion, uint256 _upgradeTimestamp) external; + + function multicall(Call[] calldata _calls, bool _requireSuccess) external payable; + + function setTokenMultiplier(IAdmin _chainContract, uint128 _nominator, uint128 _denominator) external; +} diff --git a/contracts/l1-contracts/governance/IGovernance.sol b/contracts/l1-contracts/governance/IGovernance.sol new file mode 100644 index 0000000..2b03ed4 --- /dev/null +++ b/contracts/l1-contracts/governance/IGovernance.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.21; + +/// @title Governance contract interface +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +interface IGovernance { + /// @dev This enumeration includes the following states: + /// @param Unset Default state, indicating the operation has not been set. + /// @param Waiting The operation is scheduled but not yet ready to be executed. + /// @param Ready The operation is ready to be executed. + /// @param Done The operation has been successfully executed. + enum OperationState { + Unset, + Waiting, + Ready, + Done + } + + /// @dev Represents a call to be made during an operation. + /// @param target The address to which the call will be made. + /// @param value The amount of Ether (in wei) to be sent along with the call. + /// @param data The calldata to be executed on the `target` address. + struct Call { + address target; + uint256 value; + bytes data; + } + + /// @dev Defines the structure of an operation that Governance executes. + /// @param calls An array of `Call` structs, each representing a call to be made during the operation. + /// @param predecessor The hash of the predecessor operation, that should be executed before this operation. + /// @param salt A bytes32 value used for creating unique operation hashes. + struct Operation { + Call[] calls; + bytes32 predecessor; + bytes32 salt; + } + + function isOperation(bytes32 _id) external view returns (bool); + + function isOperationPending(bytes32 _id) external view returns (bool); + + function isOperationReady(bytes32 _id) external view returns (bool); + + function isOperationDone(bytes32 _id) external view returns (bool); + + function getOperationState(bytes32 _id) external view returns (OperationState); + + function scheduleTransparent(Operation calldata _operation, uint256 _delay) external; + + function scheduleShadow(bytes32 _id, uint256 _delay) external; + + function cancel(bytes32 _id) external; + + function execute(Operation calldata _operation) external payable; + + function executeInstant(Operation calldata _operation) external payable; + + function hashOperation(Operation calldata _operation) external pure returns (bytes32); + + function updateDelay(uint256 _newDelay) external; + + function updateSecurityCouncil(address _newSecurityCouncil) external; + + /// @notice Emitted when transparent operation is scheduled. + event TransparentOperationScheduled(bytes32 indexed _id, uint256 delay, Operation _operation); + + /// @notice Emitted when shadow operation is scheduled. + event ShadowOperationScheduled(bytes32 indexed _id, uint256 delay); + + /// @notice Emitted when the operation is executed with delay or instantly. + event OperationExecuted(bytes32 indexed _id); + + /// @notice Emitted when the security council address is changed. + event ChangeSecurityCouncil(address _securityCouncilBefore, address _securityCouncilAfter); + + /// @notice Emitted when the minimum delay for future operations is modified. + event ChangeMinDelay(uint256 _delayBefore, uint256 _delayAfter); + + /// @notice Emitted when the operation with specified id is cancelled. + event OperationCancelled(bytes32 indexed _id); +} diff --git a/contracts/l1-contracts/state-transition/IStateTransitionManager.sol b/contracts/l1-contracts/state-transition/IStateTransitionManager.sol new file mode 100644 index 0000000..fec1c31 --- /dev/null +++ b/contracts/l1-contracts/state-transition/IStateTransitionManager.sol @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.21; + +import {Diamond} from "./libraries/Diamond.sol"; +import {L2CanonicalTransaction} from "../common/Messaging.sol"; +import {FeeParams} from "./chain-deps/ZkSyncHyperchainStorage.sol"; + +/// @notice Struct that holds all data needed for initializing STM Proxy. +/// @dev We use struct instead of raw parameters in `initialize` function to prevent "Stack too deep" error +/// @param owner The address who can manage non-critical updates in the contract +/// @param validatorTimelock The address that serves as consensus, i.e. can submit blocks to be processed +/// @param chainCreationParams The struct that contains the fields that define how a new chain should be created +/// @param protocolVersion The initial protocol version on the newly deployed chain +struct StateTransitionManagerInitializeData { + address owner; + address validatorTimelock; + ChainCreationParams chainCreationParams; + uint256 protocolVersion; +} + +/// @notice The struct that contains the fields that define how a new chain should be created +/// within this STM. +/// @param genesisUpgrade The address that is used in the diamond cut initialize address on chain creation +/// @param genesisBatchHash Batch hash of the genesis (initial) batch +/// @param genesisIndexRepeatedStorageChanges The serial number of the shortcut storage key for the genesis batch +/// @param genesisBatchCommitment The zk-proof commitment for the genesis batch +/// @param diamondCut The diamond cut for the first upgrade transaction on the newly deployed chain +// solhint-disable-next-line gas-struct-packing +struct ChainCreationParams { + address genesisUpgrade; + bytes32 genesisBatchHash; + uint64 genesisIndexRepeatedStorageChanges; + bytes32 genesisBatchCommitment; + Diamond.DiamondCutData diamondCut; +} + +interface IStateTransitionManager { + /// @dev Emitted when a new Hyperchain is added + event NewHyperchain(uint256 indexed _chainId, address indexed _hyperchainContract); + + /// @dev emitted when an chain registers and a SetChainIdUpgrade happens + event SetChainIdUpgrade( + address indexed _hyperchain, + L2CanonicalTransaction _l2Transaction, + uint256 indexed _protocolVersion + ); + + /// @notice pendingAdmin is changed + /// @dev Also emitted when new admin is accepted and in this case, `newPendingAdmin` would be zero address + event NewPendingAdmin(address indexed oldPendingAdmin, address indexed newPendingAdmin); + + /// @notice Admin changed + event NewAdmin(address indexed oldAdmin, address indexed newAdmin); + + /// @notice ValidatorTimelock changed + event NewValidatorTimelock(address indexed oldValidatorTimelock, address indexed newValidatorTimelock); + + /// @notice chain creation parameters changed + event NewChainCreationParams( + address genesisUpgrade, + bytes32 genesisBatchHash, + uint64 genesisIndexRepeatedStorageChanges, + bytes32 genesisBatchCommitment, + bytes32 newInitialCutHash + ); + + /// @notice New UpgradeCutHash + event NewUpgradeCutHash(uint256 indexed protocolVersion, bytes32 indexed upgradeCutHash); + + /// @notice New UpgradeCutData + event NewUpgradeCutData(uint256 indexed protocolVersion, Diamond.DiamondCutData diamondCutData); + + /// @notice New ProtocolVersion + event NewProtocolVersion(uint256 indexed oldProtocolVersion, uint256 indexed newProtocolVersion); + + function BRIDGE_HUB() external view returns (address); + + function setPendingAdmin(address _newPendingAdmin) external; + + function acceptAdmin() external; + + function getAllHyperchains() external view returns (address[] memory); + + function getAllHyperchainChainIDs() external view returns (uint256[] memory); + + function getHyperchain(uint256 _chainId) external view returns (address); + + function storedBatchZero() external view returns (bytes32); + + function initialCutHash() external view returns (bytes32); + + function genesisUpgrade() external view returns (address); + + function upgradeCutHash(uint256 _protocolVersion) external view returns (bytes32); + + function protocolVersion() external view returns (uint256); + + function protocolVersionDeadline(uint256 _protocolVersion) external view returns (uint256); + + function protocolVersionIsActive(uint256 _protocolVersion) external view returns (bool); + + function initialize(StateTransitionManagerInitializeData calldata _initializeData) external; + + function setValidatorTimelock(address _validatorTimelock) external; + + function setChainCreationParams(ChainCreationParams calldata _chainCreationParams) external; + + function getChainAdmin(uint256 _chainId) external view returns (address); + + function createNewChain( + uint256 _chainId, + address _baseToken, + address _sharedBridge, + address _admin, + bytes calldata _diamondCut + ) external; + + function registerAlreadyDeployedHyperchain(uint256 _chainId, address _hyperchain) external; + + function setNewVersionUpgrade( + Diamond.DiamondCutData calldata _cutData, + uint256 _oldProtocolVersion, + uint256 _oldProtocolVersionDeadline, + uint256 _newProtocolVersion + ) external; + + function setUpgradeDiamondCut(Diamond.DiamondCutData calldata _cutData, uint256 _oldProtocolVersion) external; + + function executeUpgrade(uint256 _chainId, Diamond.DiamondCutData calldata _diamondCut) external; + + function setPriorityTxMaxGasLimit(uint256 _chainId, uint256 _maxGasLimit) external; + + function freezeChain(uint256 _chainId) external; + + function unfreezeChain(uint256 _chainId) external; + + function setTokenMultiplier(uint256 _chainId, uint128 _nominator, uint128 _denominator) external; + + function changeFeeParams(uint256 _chainId, FeeParams calldata _newFeeParams) external; + + function setValidator(uint256 _chainId, address _validator, bool _active) external; + + function setPorterAvailability(uint256 _chainId, bool _zkPorterIsAvailable) external; + + function upgradeChainFromVersion( + uint256 _chainId, + uint256 _oldProtocolVersion, + Diamond.DiamondCutData calldata _diamondCut + ) external; + + function getSemverProtocolVersion() external view returns (uint32, uint32, uint32); +} diff --git a/contracts/l1-contracts/state-transition/StateTransitionManager.sol b/contracts/l1-contracts/state-transition/StateTransitionManager.sol new file mode 100644 index 0000000..0dbdd2a --- /dev/null +++ b/contracts/l1-contracts/state-transition/StateTransitionManager.sol @@ -0,0 +1,460 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {EnumerableMap} from "@openzeppelin/contracts-v4/utils/structs/EnumerableMap.sol"; +import {SafeCast} from "@openzeppelin/contracts-v4/utils/math/SafeCast.sol"; + +import {Diamond} from "./libraries/Diamond.sol"; +import {DiamondProxy} from "./chain-deps/DiamondProxy.sol"; +import {IAdmin} from "./chain-interfaces/IAdmin.sol"; +import {IDefaultUpgrade} from "../upgrades/IDefaultUpgrade.sol"; +import {IDiamondInit} from "./chain-interfaces/IDiamondInit.sol"; +import {IExecutor} from "./chain-interfaces/IExecutor.sol"; +import {IStateTransitionManager, StateTransitionManagerInitializeData, ChainCreationParams} from "./IStateTransitionManager.sol"; +import {ISystemContext} from "./l2-deps/ISystemContext.sol"; +import {IZkSyncHyperchain} from "./chain-interfaces/IZkSyncHyperchain.sol"; +import {FeeParams} from "./chain-deps/ZkSyncHyperchainStorage.sol"; +import {L2_SYSTEM_CONTEXT_SYSTEM_CONTRACT_ADDR, L2_FORCE_DEPLOYER_ADDR} from "../common/L2ContractAddresses.sol"; +import {L2CanonicalTransaction} from "../common/Messaging.sol"; +import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable-v4/access/Ownable2StepUpgradeable.sol"; +import {ProposedUpgrade} from "../upgrades/BaseZkSyncUpgrade.sol"; +import {ReentrancyGuard} from "../common/ReentrancyGuard.sol"; +import {REQUIRED_L2_GAS_PRICE_PER_PUBDATA, L2_TO_L1_LOG_SERIALIZE_SIZE, DEFAULT_L2_LOGS_TREE_ROOT_HASH, EMPTY_STRING_KECCAK, SYSTEM_UPGRADE_L2_TX_TYPE, PRIORITY_TX_MAX_GAS_LIMIT} from "../common/Config.sol"; +import {VerifierParams} from "./chain-interfaces/IVerifier.sol"; +import {Unauthorized, ZeroAddress, HashMismatch, HyperchainLimitReached, GenesisUpgradeZero, GenesisBatchHashZero, GenesisIndexStorageZero, GenesisBatchCommitmentZero} from "../common/L1ContractErrors.sol"; +import {SemVer} from "../common/libraries/SemVer.sol"; + +/// @title State Transition Manager contract +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +contract StateTransitionManager is IStateTransitionManager, ReentrancyGuard, Ownable2StepUpgradeable { + using EnumerableMap for EnumerableMap.UintToAddressMap; + + /// @notice Address of the bridgehub + address public immutable BRIDGE_HUB; + + /// @notice The total number of hyperchains can be created/connected to this STM. + /// This is the temporary security measure. + uint256 public immutable MAX_NUMBER_OF_HYPERCHAINS; + + /// @notice The map from chainId => hyperchain contract + EnumerableMap.UintToAddressMap internal hyperchainMap; + + /// @dev The batch zero hash, calculated at initialization + bytes32 public storedBatchZero; + + /// @dev The stored cutData for diamond cut + bytes32 public initialCutHash; + + /// @dev The genesisUpgrade contract address, used to setChainId + address public genesisUpgrade; + + /// @dev The current packed protocolVersion. To access human-readable version, use `getSemverProtocolVersion` function. + uint256 public protocolVersion; + + /// @dev The timestamp when protocolVersion can be last used + mapping(uint256 _protocolVersion => uint256) public protocolVersionDeadline; + + /// @dev The validatorTimelock contract address, used to setChainId + address public validatorTimelock; + + /// @dev The stored cutData for upgrade diamond cut. protocolVersion => cutHash + mapping(uint256 protocolVersion => bytes32 cutHash) public upgradeCutHash; + + /// @dev The address used to manage non critical updates + address public admin; + + /// @dev The address to accept the admin role + address private pendingAdmin; + + /// @dev Contract is expected to be used as proxy implementation. + /// @dev Initialize the implementation to prevent Parity hack. + constructor(address _bridgehub, uint256 _maxNumberOfHyperchains) reentrancyGuardInitializer { + BRIDGE_HUB = _bridgehub; + MAX_NUMBER_OF_HYPERCHAINS = _maxNumberOfHyperchains; + + // While this does not provide a protection in the production, it is needed for local testing + // Length of the L2Log encoding should not be equal to the length of other L2Logs' tree nodes preimages + assert(L2_TO_L1_LOG_SERIALIZE_SIZE != 2 * 32); + } + + /// @notice only the bridgehub can call + modifier onlyBridgehub() { + if (msg.sender != BRIDGE_HUB) { + revert Unauthorized(msg.sender); + } + _; + } + + /// @notice the admin can call, for non-critical updates + modifier onlyOwnerOrAdmin() { + if (msg.sender != admin && msg.sender != owner()) { + revert Unauthorized(msg.sender); + } + _; + } + + /// @return The tuple of (major, minor, patch) protocol version. + function getSemverProtocolVersion() external view returns (uint32, uint32, uint32) { + // slither-disable-next-line unused-return + return SemVer.unpackSemVer(SafeCast.toUint96(protocolVersion)); + } + + /// @notice Returns all the registered hyperchain addresses + function getAllHyperchains() public view override returns (address[] memory chainAddresses) { + uint256[] memory keys = hyperchainMap.keys(); + chainAddresses = new address[](keys.length); + uint256 keysLength = keys.length; + for (uint256 i = 0; i < keysLength; ++i) { + chainAddresses[i] = hyperchainMap.get(keys[i]); + } + } + + /// @notice Returns all the registered hyperchain chainIDs + function getAllHyperchainChainIDs() public view override returns (uint256[] memory) { + return hyperchainMap.keys(); + } + + /// @notice Returns the address of the hyperchain with the corresponding chainID + function getHyperchain(uint256 _chainId) public view override returns (address chainAddress) { + // slither-disable-next-line unused-return + (, chainAddress) = hyperchainMap.tryGet(_chainId); + } + + /// @notice Returns the address of the hyperchain admin with the corresponding chainID + function getChainAdmin(uint256 _chainId) external view override returns (address) { + return IZkSyncHyperchain(hyperchainMap.get(_chainId)).getAdmin(); + } + + /// @dev initialize + function initialize( + StateTransitionManagerInitializeData calldata _initializeData + ) external reentrancyGuardInitializer { + if (_initializeData.owner == address(0)) { + revert ZeroAddress(); + } + _transferOwnership(_initializeData.owner); + + protocolVersion = _initializeData.protocolVersion; + protocolVersionDeadline[_initializeData.protocolVersion] = type(uint256).max; + validatorTimelock = _initializeData.validatorTimelock; + + _setChainCreationParams(_initializeData.chainCreationParams); + } + + /// @notice Updates the parameters with which a new chain is created + /// @param _chainCreationParams The new chain creation parameters + function _setChainCreationParams(ChainCreationParams calldata _chainCreationParams) internal { + if (_chainCreationParams.genesisUpgrade == address(0)) { + revert GenesisUpgradeZero(); + } + if (_chainCreationParams.genesisBatchHash == bytes32(0)) { + revert GenesisBatchHashZero(); + } + if (_chainCreationParams.genesisIndexRepeatedStorageChanges == uint64(0)) { + revert GenesisIndexStorageZero(); + } + if (_chainCreationParams.genesisBatchCommitment == bytes32(0)) { + revert GenesisBatchCommitmentZero(); + } + + genesisUpgrade = _chainCreationParams.genesisUpgrade; + + // We need to initialize the state hash because it is used in the commitment of the next batch + IExecutor.StoredBatchInfo memory batchZero = IExecutor.StoredBatchInfo({ + batchNumber: 0, + batchHash: _chainCreationParams.genesisBatchHash, + indexRepeatedStorageChanges: _chainCreationParams.genesisIndexRepeatedStorageChanges, + numberOfLayer1Txs: 0, + priorityOperationsHash: EMPTY_STRING_KECCAK, + l2LogsTreeRoot: DEFAULT_L2_LOGS_TREE_ROOT_HASH, + timestamp: 0, + commitment: _chainCreationParams.genesisBatchCommitment + }); + storedBatchZero = keccak256(abi.encode(batchZero)); + bytes32 newInitialCutHash = keccak256(abi.encode(_chainCreationParams.diamondCut)); + initialCutHash = newInitialCutHash; + + emit NewChainCreationParams({ + genesisUpgrade: _chainCreationParams.genesisUpgrade, + genesisBatchHash: _chainCreationParams.genesisBatchHash, + genesisIndexRepeatedStorageChanges: _chainCreationParams.genesisIndexRepeatedStorageChanges, + genesisBatchCommitment: _chainCreationParams.genesisBatchCommitment, + newInitialCutHash: newInitialCutHash + }); + } + + /// @notice Updates the parameters with which a new chain is created + /// @param _chainCreationParams The new chain creation parameters + function setChainCreationParams(ChainCreationParams calldata _chainCreationParams) external onlyOwner { + _setChainCreationParams(_chainCreationParams); + } + + /// @notice Starts the transfer of admin rights. Only the current admin can propose a new pending one. + /// @notice New admin can accept admin rights by calling `acceptAdmin` function. + /// @param _newPendingAdmin Address of the new admin + /// @dev Please note, if the owner wants to enforce the admin change it must execute both `setPendingAdmin` and + /// `acceptAdmin` atomically. Otherwise `admin` can set different pending admin and so fail to accept the admin rights. + function setPendingAdmin(address _newPendingAdmin) external onlyOwnerOrAdmin { + // Save previous value into the stack to put it into the event later + address oldPendingAdmin = pendingAdmin; + // Change pending admin + pendingAdmin = _newPendingAdmin; + emit NewPendingAdmin(oldPendingAdmin, _newPendingAdmin); + } + + /// @notice Accepts transfer of admin rights. Only pending admin can accept the role. + function acceptAdmin() external { + address currentPendingAdmin = pendingAdmin; + // Only proposed by current admin address can claim the admin rights + if (msg.sender != currentPendingAdmin) { + revert Unauthorized(msg.sender); + } + + address previousAdmin = admin; + admin = currentPendingAdmin; + delete pendingAdmin; + + emit NewPendingAdmin(currentPendingAdmin, address(0)); + emit NewAdmin(previousAdmin, currentPendingAdmin); + } + + /// @dev set validatorTimelock. Cannot do it during initialization, as validatorTimelock is deployed after STM + function setValidatorTimelock(address _validatorTimelock) external onlyOwnerOrAdmin { + address oldValidatorTimelock = validatorTimelock; + validatorTimelock = _validatorTimelock; + emit NewValidatorTimelock(oldValidatorTimelock, _validatorTimelock); + } + + /// @dev set New Version with upgrade from old version + function setNewVersionUpgrade( + Diamond.DiamondCutData calldata _cutData, + uint256 _oldProtocolVersion, + uint256 _oldProtocolVersionDeadline, + uint256 _newProtocolVersion + ) external onlyOwner { + bytes32 newCutHash = keccak256(abi.encode(_cutData)); + uint256 previousProtocolVersion = protocolVersion; + upgradeCutHash[_oldProtocolVersion] = newCutHash; + protocolVersionDeadline[_oldProtocolVersion] = _oldProtocolVersionDeadline; + protocolVersionDeadline[_newProtocolVersion] = type(uint256).max; + protocolVersion = _newProtocolVersion; + emit NewProtocolVersion(previousProtocolVersion, _newProtocolVersion); + emit NewUpgradeCutHash(_oldProtocolVersion, newCutHash); + emit NewUpgradeCutData(_newProtocolVersion, _cutData); + } + + /// @dev check that the protocolVersion is active + function protocolVersionIsActive(uint256 _protocolVersion) external view override returns (bool) { + return block.timestamp <= protocolVersionDeadline[_protocolVersion]; + } + + /// @dev set the protocol version timestamp + function setProtocolVersionDeadline(uint256 _protocolVersion, uint256 _timestamp) external onlyOwner { + protocolVersionDeadline[_protocolVersion] = _timestamp; + } + + /// @dev set upgrade for some protocolVersion + function setUpgradeDiamondCut( + Diamond.DiamondCutData calldata _cutData, + uint256 _oldProtocolVersion + ) external onlyOwner { + bytes32 newCutHash = keccak256(abi.encode(_cutData)); + upgradeCutHash[_oldProtocolVersion] = newCutHash; + emit NewUpgradeCutHash(_oldProtocolVersion, newCutHash); + } + + /// @dev freezes the specified chain + function freezeChain(uint256 _chainId) external onlyOwner { + IZkSyncHyperchain(hyperchainMap.get(_chainId)).freezeDiamond(); + } + + /// @dev freezes the specified chain + function unfreezeChain(uint256 _chainId) external onlyOwner { + IZkSyncHyperchain(hyperchainMap.get(_chainId)).unfreezeDiamond(); + } + + /// @dev reverts batches on the specified chain + function revertBatches(uint256 _chainId, uint256 _newLastBatch) external onlyOwnerOrAdmin { + IZkSyncHyperchain(hyperchainMap.get(_chainId)).revertBatches(_newLastBatch); + } + + /// @dev execute predefined upgrade + function upgradeChainFromVersion( + uint256 _chainId, + uint256 _oldProtocolVersion, + Diamond.DiamondCutData calldata _diamondCut + ) external onlyOwner { + IZkSyncHyperchain(hyperchainMap.get(_chainId)).upgradeChainFromVersion(_oldProtocolVersion, _diamondCut); + } + + /// @dev executes upgrade on chain + function executeUpgrade(uint256 _chainId, Diamond.DiamondCutData calldata _diamondCut) external onlyOwner { + IZkSyncHyperchain(hyperchainMap.get(_chainId)).executeUpgrade(_diamondCut); + } + + /// @dev setPriorityTxMaxGasLimit for the specified chain + function setPriorityTxMaxGasLimit(uint256 _chainId, uint256 _maxGasLimit) external onlyOwner { + IZkSyncHyperchain(hyperchainMap.get(_chainId)).setPriorityTxMaxGasLimit(_maxGasLimit); + } + + /// @dev setTokenMultiplier for the specified chain + function setTokenMultiplier(uint256 _chainId, uint128 _nominator, uint128 _denominator) external onlyOwner { + IZkSyncHyperchain(hyperchainMap.get(_chainId)).setTokenMultiplier(_nominator, _denominator); + } + + /// @dev changeFeeParams for the specified chain + function changeFeeParams(uint256 _chainId, FeeParams calldata _newFeeParams) external onlyOwner { + IZkSyncHyperchain(hyperchainMap.get(_chainId)).changeFeeParams(_newFeeParams); + } + + /// @dev setValidator for the specified chain + function setValidator(uint256 _chainId, address _validator, bool _active) external onlyOwnerOrAdmin { + IZkSyncHyperchain(hyperchainMap.get(_chainId)).setValidator(_validator, _active); + } + + /// @dev setPorterAvailability for the specified chain + function setPorterAvailability(uint256 _chainId, bool _zkPorterIsAvailable) external onlyOwner { + IZkSyncHyperchain(hyperchainMap.get(_chainId)).setPorterAvailability(_zkPorterIsAvailable); + } + + /// registration + + /// @dev we have to set the chainId at genesis, as blockhashzero is the same for all chains with the same chainId + function _setChainIdUpgrade(uint256 _chainId, address _chainContract) internal { + bytes memory systemContextCalldata = abi.encodeCall(ISystemContext.setChainId, (_chainId)); + uint256[] memory uintEmptyArray; + bytes[] memory bytesEmptyArray; + + uint256 cachedProtocolVersion = protocolVersion; + // slither-disable-next-line unused-return + (, uint32 minorVersion, ) = SemVer.unpackSemVer(SafeCast.toUint96(cachedProtocolVersion)); + + L2CanonicalTransaction memory l2ProtocolUpgradeTx = L2CanonicalTransaction({ + txType: SYSTEM_UPGRADE_L2_TX_TYPE, + from: uint256(uint160(L2_FORCE_DEPLOYER_ADDR)), + to: uint256(uint160(L2_SYSTEM_CONTEXT_SYSTEM_CONTRACT_ADDR)), + gasLimit: PRIORITY_TX_MAX_GAS_LIMIT, + gasPerPubdataByteLimit: REQUIRED_L2_GAS_PRICE_PER_PUBDATA, + maxFeePerGas: uint256(0), + maxPriorityFeePerGas: uint256(0), + paymaster: uint256(0), + // Note, that the `minor` of the protocol version is used as "nonce" for system upgrade transactions + nonce: uint256(minorVersion), + value: 0, + reserved: [uint256(0), 0, 0, 0], + data: systemContextCalldata, + signature: new bytes(0), + factoryDeps: uintEmptyArray, + paymasterInput: new bytes(0), + reservedDynamic: new bytes(0) + }); + + ProposedUpgrade memory proposedUpgrade = ProposedUpgrade({ + l2ProtocolUpgradeTx: l2ProtocolUpgradeTx, + factoryDeps: bytesEmptyArray, + bootloaderHash: bytes32(0), + defaultAccountHash: bytes32(0), + verifier: address(0), + verifierParams: VerifierParams({ + recursionNodeLevelVkHash: bytes32(0), + recursionLeafLevelVkHash: bytes32(0), + recursionCircuitsSetVksHash: bytes32(0) + }), + l1ContractsUpgradeCalldata: new bytes(0), + postUpgradeCalldata: new bytes(0), + upgradeTimestamp: 0, + newProtocolVersion: cachedProtocolVersion + }); + + Diamond.FacetCut[] memory emptyArray; + Diamond.DiamondCutData memory cutData = Diamond.DiamondCutData({ + facetCuts: emptyArray, + initAddress: genesisUpgrade, + initCalldata: abi.encodeCall(IDefaultUpgrade.upgrade, (proposedUpgrade)) + }); + + IAdmin(_chainContract).executeUpgrade(cutData); + emit SetChainIdUpgrade(_chainContract, l2ProtocolUpgradeTx, cachedProtocolVersion); + } + + /// @dev used to register already deployed hyperchain contracts + /// @param _chainId the chain's id + /// @param _hyperchain the chain's contract address + function registerAlreadyDeployedHyperchain(uint256 _chainId, address _hyperchain) external onlyOwner { + if (_hyperchain == address(0)) { + revert ZeroAddress(); + } + + _registerNewHyperchain(_chainId, _hyperchain); + } + + /// @notice called by Bridgehub when a chain registers + /// @param _chainId the chain's id + /// @param _baseToken the base token address used to pay for gas fees + /// @param _sharedBridge the shared bridge address, used as base token bridge + /// @param _admin the chain's admin address + /// @param _diamondCut the diamond cut data that initializes the chains Diamond Proxy + function createNewChain( + uint256 _chainId, + address _baseToken, + address _sharedBridge, + address _admin, + bytes calldata _diamondCut + ) external onlyBridgehub { + if (getHyperchain(_chainId) != address(0)) { + // Hyperchain already registered + return; + } + + // check not registered + Diamond.DiamondCutData memory diamondCut = abi.decode(_diamondCut, (Diamond.DiamondCutData)); + + // check input + bytes32 cutHashInput = keccak256(_diamondCut); + if (cutHashInput != initialCutHash) { + revert HashMismatch(initialCutHash, cutHashInput); + } + + // construct init data + bytes memory initData; + /// all together 4+9*32=292 bytes + // solhint-disable-next-line func-named-parameters + initData = bytes.concat( + IDiamondInit.initialize.selector, + bytes32(_chainId), + bytes32(uint256(uint160(BRIDGE_HUB))), + bytes32(uint256(uint160(address(this)))), + bytes32(protocolVersion), + bytes32(uint256(uint160(_admin))), + bytes32(uint256(uint160(validatorTimelock))), + bytes32(uint256(uint160(_baseToken))), + bytes32(uint256(uint160(_sharedBridge))), + storedBatchZero, + diamondCut.initCalldata + ); + + diamondCut.initCalldata = initData; + // deploy hyperchainContract + // slither-disable-next-line reentrancy-no-eth + DiamondProxy hyperchainContract = new DiamondProxy{salt: bytes32(0)}(block.chainid, diamondCut); + // save data + address hyperchainAddress = address(hyperchainContract); + + _registerNewHyperchain(_chainId, hyperchainAddress); + + // set chainId in VM + _setChainIdUpgrade(_chainId, hyperchainAddress); + } + + /// @dev This internal function is used to register a new hyperchain in the system. + function _registerNewHyperchain(uint256 _chainId, address _hyperchain) internal { + // slither-disable-next-line unused-return + hyperchainMap.set(_chainId, _hyperchain); + if (hyperchainMap.length() > MAX_NUMBER_OF_HYPERCHAINS) { + revert HyperchainLimitReached(); + } + emit NewHyperchain(_chainId, _hyperchain); + } +} diff --git a/contracts/l1-contracts/state-transition/TestnetVerifier.sol b/contracts/l1-contracts/state-transition/TestnetVerifier.sol new file mode 100644 index 0000000..6e97fed --- /dev/null +++ b/contracts/l1-contracts/state-transition/TestnetVerifier.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {Verifier} from "./Verifier.sol"; +import {IVerifier} from "./chain-interfaces/IVerifier.sol"; + +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +/// @notice Modified version of the main verifier contract for the testnet environment +/// @dev This contract is used to skip the zkp verification for the testnet environment. +/// If the proof is not empty, it will verify it using the main verifier contract, +/// otherwise, it will skip the verification. +contract TestnetVerifier is Verifier { + constructor() { + assert(block.chainid != 1); + } + + /// @dev Verifies a zk-SNARK proof, skipping the verification if the proof is empty. + /// @inheritdoc IVerifier + function verify( + uint256[] calldata _publicInputs, + uint256[] calldata _proof, + uint256[] calldata _recursiveAggregationInput + ) public view override returns (bool) { + // We allow skipping the zkp verification for the test(net) environment + // If the proof is not empty, verify it, otherwise, skip the verification + if (_proof.length == 0) { + return true; + } + + return super.verify(_publicInputs, _proof, _recursiveAggregationInput); + } +} diff --git a/contracts/l1-contracts/state-transition/ValidatorTimelock.sol b/contracts/l1-contracts/state-transition/ValidatorTimelock.sol new file mode 100644 index 0000000..61576f7 --- /dev/null +++ b/contracts/l1-contracts/state-transition/ValidatorTimelock.sol @@ -0,0 +1,249 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {Ownable2Step} from "@openzeppelin/contracts-v4/access/Ownable2Step.sol"; +import {LibMap} from "./libraries/LibMap.sol"; +import {IExecutor} from "./chain-interfaces/IExecutor.sol"; +import {IStateTransitionManager} from "./IStateTransitionManager.sol"; +import {Unauthorized, TimeNotReached, ZeroAddress} from "../common/L1ContractErrors.sol"; + +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +/// @notice Intermediate smart contract between the validator EOA account and the hyperchains state transition diamond smart contract. +/// @dev The primary purpose of this contract is to provide a trustless means of delaying batch execution without +/// modifying the main hyperchain diamond contract. As such, even if this contract is compromised, it will not impact the main +/// contract. +/// @dev 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. +/// @dev The contract overloads all of the 4 methods, that are used in state transition. When the batch is committed, +/// the timestamp is stored for it. Later, when the owner calls the batch execution, the contract checks that batch +/// was committed not earlier than X time ago. +contract ValidatorTimelock is IExecutor, Ownable2Step { + using LibMap for LibMap.Uint32Map; + + /// @dev Part of the IBase interface. Not used in this contract. + string public constant override getName = "ValidatorTimelock"; + + /// @notice The delay between committing and executing batches is changed. + event NewExecutionDelay(uint256 _newExecutionDelay); + + /// @notice A new validator has been added. + event ValidatorAdded(uint256 indexed _chainId, address _addedValidator); + + /// @notice A validator has been removed. + event ValidatorRemoved(uint256 indexed _chainId, address _removedValidator); + + /// @notice Error for when an address is already a validator. + error AddressAlreadyValidator(uint256 _chainId); + + /// @notice Error for when an address is not a validator. + error ValidatorDoesNotExist(uint256 _chainId); + + /// @dev The stateTransitionManager smart contract. + IStateTransitionManager public stateTransitionManager; + + /// @dev The mapping of L2 chainId => batch number => timestamp when it was committed. + mapping(uint256 chainId => LibMap.Uint32Map batchNumberToTimestampMapping) internal committedBatchTimestamp; + + /// @dev The address that can commit/revert/validate/execute batches. + mapping(uint256 _chainId => mapping(address _validator => bool)) public validators; + + /// @dev The delay between committing and executing batches. + uint32 public executionDelay; + + /// @dev Era's chainID + uint256 internal immutable ERA_CHAIN_ID; + + constructor(address _initialOwner, uint32 _executionDelay, uint256 _eraChainId) { + _transferOwnership(_initialOwner); + executionDelay = _executionDelay; + ERA_CHAIN_ID = _eraChainId; + } + + /// @notice Checks if the caller is the admin of the chain. + modifier onlyChainAdmin(uint256 _chainId) { + if (msg.sender != stateTransitionManager.getChainAdmin(_chainId)) { + revert Unauthorized(msg.sender); + } + _; + } + + /// @notice Checks if the caller is a validator. + modifier onlyValidator(uint256 _chainId) { + if (!validators[_chainId][msg.sender]) { + revert Unauthorized(msg.sender); + } + _; + } + + /// @dev Sets a new state transition manager. + function setStateTransitionManager(IStateTransitionManager _stateTransitionManager) external onlyOwner { + if (address(_stateTransitionManager) == address(0)) { + revert ZeroAddress(); + } + stateTransitionManager = _stateTransitionManager; + } + + /// @dev Sets an address as a validator. + function addValidator(uint256 _chainId, address _newValidator) external onlyChainAdmin(_chainId) { + if (validators[_chainId][_newValidator]) { + revert AddressAlreadyValidator(_chainId); + } + validators[_chainId][_newValidator] = true; + emit ValidatorAdded(_chainId, _newValidator); + } + + /// @dev Removes an address as a validator. + function removeValidator(uint256 _chainId, address _validator) external onlyChainAdmin(_chainId) { + if (!validators[_chainId][_validator]) { + revert ValidatorDoesNotExist(_chainId); + } + validators[_chainId][_validator] = false; + emit ValidatorRemoved(_chainId, _validator); + } + + /// @dev Set the delay between committing and executing batches. + function setExecutionDelay(uint32 _executionDelay) external onlyOwner { + executionDelay = _executionDelay; + emit NewExecutionDelay(_executionDelay); + } + + /// @dev Returns the timestamp when `_l2BatchNumber` was committed. + function getCommittedBatchTimestamp(uint256 _chainId, uint256 _l2BatchNumber) external view returns (uint256) { + return committedBatchTimestamp[_chainId].get(_l2BatchNumber); + } + + /// @dev Records the timestamp for all provided committed batches and make + /// a call to the hyperchain diamond contract with the same calldata. + function commitBatches( + StoredBatchInfo calldata, + CommitBatchInfo[] calldata _newBatchesData + ) external onlyValidator(ERA_CHAIN_ID) { + _commitBatchesInner(ERA_CHAIN_ID, _newBatchesData); + } + + /// @dev Records the timestamp for all provided committed batches and make + /// a call to the hyperchain diamond contract with the same calldata. + function commitBatchesSharedBridge( + uint256 _chainId, + StoredBatchInfo calldata, + CommitBatchInfo[] calldata _newBatchesData + ) external onlyValidator(_chainId) { + _commitBatchesInner(_chainId, _newBatchesData); + } + + function _commitBatchesInner(uint256 _chainId, CommitBatchInfo[] calldata _newBatchesData) internal { + unchecked { + // This contract is only a temporary solution, that hopefully will be disabled until 2106 year, so... + // It is safe to cast. + uint32 timestamp = uint32(block.timestamp); + // We disable this check because calldata array length is cheap. + // solhint-disable-next-line gas-length-in-loops + for (uint256 i = 0; i < _newBatchesData.length; ++i) { + committedBatchTimestamp[_chainId].set(_newBatchesData[i].batchNumber, timestamp); + } + } + + _propagateToZkSyncHyperchain(_chainId); + } + + /// @dev Make a call to the hyperchain diamond contract with the same calldata. + /// Note: If the batch is reverted, it needs to be committed first before the execution. + /// So it's safe to not override the committed batches. + function revertBatches(uint256) external onlyValidator(ERA_CHAIN_ID) { + _propagateToZkSyncHyperchain(ERA_CHAIN_ID); + } + + /// @dev Make a call to the hyperchain diamond contract with the same calldata. + /// Note: If the batch is reverted, it needs to be committed first before the execution. + /// So it's safe to not override the committed batches. + function revertBatchesSharedBridge(uint256 _chainId, uint256) external onlyValidator(_chainId) { + _propagateToZkSyncHyperchain(_chainId); + } + + /// @dev Make a call to the hyperchain diamond contract with the same calldata. + /// Note: We don't track the time when batches are proven, since all information about + /// the batch is known on the commit stage and the proved is not finalized (may be reverted). + function proveBatches( + StoredBatchInfo calldata, + StoredBatchInfo[] calldata, + ProofInput calldata + ) external onlyValidator(ERA_CHAIN_ID) { + _propagateToZkSyncHyperchain(ERA_CHAIN_ID); + } + + /// @dev Make a call to the hyperchain diamond contract with the same calldata. + /// Note: We don't track the time when batches are proven, since all information about + /// the batch is known on the commit stage and the proved is not finalized (may be reverted). + function proveBatchesSharedBridge( + uint256 _chainId, + StoredBatchInfo calldata, + StoredBatchInfo[] calldata, + ProofInput calldata + ) external onlyValidator(_chainId) { + _propagateToZkSyncHyperchain(_chainId); + } + + /// @dev Check that batches were committed at least X time ago and + /// make a call to the hyperchain diamond contract with the same calldata. + function executeBatches(StoredBatchInfo[] calldata _newBatchesData) external onlyValidator(ERA_CHAIN_ID) { + _executeBatchesInner(ERA_CHAIN_ID, _newBatchesData); + } + + /// @dev Check that batches were committed at least X time ago and + /// make a call to the hyperchain diamond contract with the same calldata. + function executeBatchesSharedBridge( + uint256 _chainId, + StoredBatchInfo[] calldata _newBatchesData + ) external onlyValidator(_chainId) { + _executeBatchesInner(_chainId, _newBatchesData); + } + + function _executeBatchesInner(uint256 _chainId, StoredBatchInfo[] calldata _newBatchesData) internal { + uint256 delay = executionDelay; // uint32 + unchecked { + // We disable this check because calldata array length is cheap. + // solhint-disable-next-line gas-length-in-loops + for (uint256 i = 0; i < _newBatchesData.length; ++i) { + uint256 commitBatchTimestamp = committedBatchTimestamp[_chainId].get(_newBatchesData[i].batchNumber); + + // Note: if the `commitBatchTimestamp` is zero, that means either: + // * The batch was committed, but not through this contract. + // * The batch wasn't committed at all, so execution will fail in the ZKsync contract. + // We allow executing such batches. + + if (block.timestamp < commitBatchTimestamp + delay) { + revert TimeNotReached(commitBatchTimestamp + delay, block.timestamp); + } + } + } + _propagateToZkSyncHyperchain(_chainId); + } + + /// @dev Call the hyperchain diamond contract with the same calldata as this contract was called. + /// Note: it is called the hyperchain diamond contract, not delegatecalled! + function _propagateToZkSyncHyperchain(uint256 _chainId) internal { + address contractAddress = stateTransitionManager.getHyperchain(_chainId); + assembly { + // Copy function signature and arguments from calldata at zero position into memory at pointer position + calldatacopy(0, 0, calldatasize()) + // Call method of the hyperchain diamond contract returns 0 on error + let result := call(gas(), contractAddress, 0, 0, calldatasize(), 0, 0) + // Get the size of the last return data + let size := returndatasize() + // Copy the size length of bytes from return data at zero position to pointer position + returndatacopy(0, 0, size) + // Depending on the result value + switch result + case 0 { + // End execution and revert state changes + revert(0, size) + } + default { + // Return data with length of size at pointers position + return(0, size) + } + } + } +} diff --git a/contracts/l1-contracts/state-transition/Verifier.sol b/contracts/l1-contracts/state-transition/Verifier.sol new file mode 100644 index 0000000..a74ecb1 --- /dev/null +++ b/contracts/l1-contracts/state-transition/Verifier.sol @@ -0,0 +1,1708 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {IVerifier} from "./chain-interfaces/IVerifier.sol"; + +/* solhint-disable max-line-length */ +/// @author Matter Labs +/// @notice Modified version of the Permutations over Lagrange-bases for Oecumenical Noninteractive arguments of +/// Knowledge (PLONK) verifier. +/// Modifications have been made to optimize the proof system for ZKsync hyperchain circuits. +/// @dev Contract was generated from a verification key with a hash of 0x14f97b81e54b35fe673d8708cc1a19e1ea5b5e348e12d31e39824ed4f42bbca2 +/// @dev It uses a custom memory layout inside the inline assembly block. Each reserved memory cell is declared in the +/// constants below. +/// @dev For a better understanding of the verifier algorithm please refer to the following papers: +/// * Original Plonk Article: https://eprint.iacr.org/2019/953.pdf +/// * Original LookUp Article: https://eprint.iacr.org/2020/315.pdf +/// * Plonk for ZKsync v1.1: https://github.com/matter-labs/solidity_plonk_verifier/raw/recursive/bellman_vk_codegen_recursive/RecursivePlonkUnrolledForEthereum.pdf +/// The notation used in the code is the same as in the papers. +/* solhint-enable max-line-length */ +contract Verifier is IVerifier { + /*////////////////////////////////////////////////////////////// + Verification keys + //////////////////////////////////////////////////////////////*/ + + // Memory slots from 0x000 to 0x200 are reserved for intermediate computations and call to precompiles. + + uint256 internal constant VK_GATE_SETUP_0_X_SLOT = 0x200 + 0x000; + uint256 internal constant VK_GATE_SETUP_0_Y_SLOT = 0x200 + 0x020; + uint256 internal constant VK_GATE_SETUP_1_X_SLOT = 0x200 + 0x040; + uint256 internal constant VK_GATE_SETUP_1_Y_SLOT = 0x200 + 0x060; + uint256 internal constant VK_GATE_SETUP_2_X_SLOT = 0x200 + 0x080; + uint256 internal constant VK_GATE_SETUP_2_Y_SLOT = 0x200 + 0x0a0; + uint256 internal constant VK_GATE_SETUP_3_X_SLOT = 0x200 + 0x0c0; + uint256 internal constant VK_GATE_SETUP_3_Y_SLOT = 0x200 + 0x0e0; + uint256 internal constant VK_GATE_SETUP_4_X_SLOT = 0x200 + 0x100; + uint256 internal constant VK_GATE_SETUP_4_Y_SLOT = 0x200 + 0x120; + uint256 internal constant VK_GATE_SETUP_5_X_SLOT = 0x200 + 0x140; + uint256 internal constant VK_GATE_SETUP_5_Y_SLOT = 0x200 + 0x160; + uint256 internal constant VK_GATE_SETUP_6_X_SLOT = 0x200 + 0x180; + uint256 internal constant VK_GATE_SETUP_6_Y_SLOT = 0x200 + 0x1a0; + uint256 internal constant VK_GATE_SETUP_7_X_SLOT = 0x200 + 0x1c0; + uint256 internal constant VK_GATE_SETUP_7_Y_SLOT = 0x200 + 0x1e0; + + uint256 internal constant VK_GATE_SELECTORS_0_X_SLOT = 0x200 + 0x200; + uint256 internal constant VK_GATE_SELECTORS_0_Y_SLOT = 0x200 + 0x220; + uint256 internal constant VK_GATE_SELECTORS_1_X_SLOT = 0x200 + 0x240; + uint256 internal constant VK_GATE_SELECTORS_1_Y_SLOT = 0x200 + 0x260; + + uint256 internal constant VK_PERMUTATION_0_X_SLOT = 0x200 + 0x280; + uint256 internal constant VK_PERMUTATION_0_Y_SLOT = 0x200 + 0x2a0; + uint256 internal constant VK_PERMUTATION_1_X_SLOT = 0x200 + 0x2c0; + uint256 internal constant VK_PERMUTATION_1_Y_SLOT = 0x200 + 0x2e0; + uint256 internal constant VK_PERMUTATION_2_X_SLOT = 0x200 + 0x300; + uint256 internal constant VK_PERMUTATION_2_Y_SLOT = 0x200 + 0x320; + uint256 internal constant VK_PERMUTATION_3_X_SLOT = 0x200 + 0x340; + uint256 internal constant VK_PERMUTATION_3_Y_SLOT = 0x200 + 0x360; + + uint256 internal constant VK_LOOKUP_SELECTOR_X_SLOT = 0x200 + 0x380; + uint256 internal constant VK_LOOKUP_SELECTOR_Y_SLOT = 0x200 + 0x3a0; + + uint256 internal constant VK_LOOKUP_TABLE_0_X_SLOT = 0x200 + 0x3c0; + uint256 internal constant VK_LOOKUP_TABLE_0_Y_SLOT = 0x200 + 0x3e0; + uint256 internal constant VK_LOOKUP_TABLE_1_X_SLOT = 0x200 + 0x400; + uint256 internal constant VK_LOOKUP_TABLE_1_Y_SLOT = 0x200 + 0x420; + uint256 internal constant VK_LOOKUP_TABLE_2_X_SLOT = 0x200 + 0x440; + uint256 internal constant VK_LOOKUP_TABLE_2_Y_SLOT = 0x200 + 0x460; + uint256 internal constant VK_LOOKUP_TABLE_3_X_SLOT = 0x200 + 0x480; + uint256 internal constant VK_LOOKUP_TABLE_3_Y_SLOT = 0x200 + 0x4a0; + + uint256 internal constant VK_LOOKUP_TABLE_TYPE_X_SLOT = 0x200 + 0x4c0; + uint256 internal constant VK_LOOKUP_TABLE_TYPE_Y_SLOT = 0x200 + 0x4e0; + + uint256 internal constant VK_RECURSIVE_FLAG_SLOT = 0x200 + 0x500; + + /*////////////////////////////////////////////////////////////// + Proof + //////////////////////////////////////////////////////////////*/ + + uint256 internal constant PROOF_PUBLIC_INPUT = 0x200 + 0x520 + 0x000; + + uint256 internal constant PROOF_STATE_POLYS_0_X_SLOT = 0x200 + 0x520 + 0x020; + uint256 internal constant PROOF_STATE_POLYS_0_Y_SLOT = 0x200 + 0x520 + 0x040; + uint256 internal constant PROOF_STATE_POLYS_1_X_SLOT = 0x200 + 0x520 + 0x060; + uint256 internal constant PROOF_STATE_POLYS_1_Y_SLOT = 0x200 + 0x520 + 0x080; + uint256 internal constant PROOF_STATE_POLYS_2_X_SLOT = 0x200 + 0x520 + 0x0a0; + uint256 internal constant PROOF_STATE_POLYS_2_Y_SLOT = 0x200 + 0x520 + 0x0c0; + uint256 internal constant PROOF_STATE_POLYS_3_X_SLOT = 0x200 + 0x520 + 0x0e0; + uint256 internal constant PROOF_STATE_POLYS_3_Y_SLOT = 0x200 + 0x520 + 0x100; + + uint256 internal constant PROOF_COPY_PERMUTATION_GRAND_PRODUCT_X_SLOT = 0x200 + 0x520 + 0x120; + uint256 internal constant PROOF_COPY_PERMUTATION_GRAND_PRODUCT_Y_SLOT = 0x200 + 0x520 + 0x140; + + uint256 internal constant PROOF_LOOKUP_S_POLY_X_SLOT = 0x200 + 0x520 + 0x160; + uint256 internal constant PROOF_LOOKUP_S_POLY_Y_SLOT = 0x200 + 0x520 + 0x180; + + uint256 internal constant PROOF_LOOKUP_GRAND_PRODUCT_X_SLOT = 0x200 + 0x520 + 0x1a0; + uint256 internal constant PROOF_LOOKUP_GRAND_PRODUCT_Y_SLOT = 0x200 + 0x520 + 0x1c0; + + uint256 internal constant PROOF_QUOTIENT_POLY_PARTS_0_X_SLOT = 0x200 + 0x520 + 0x1e0; + uint256 internal constant PROOF_QUOTIENT_POLY_PARTS_0_Y_SLOT = 0x200 + 0x520 + 0x200; + uint256 internal constant PROOF_QUOTIENT_POLY_PARTS_1_X_SLOT = 0x200 + 0x520 + 0x220; + uint256 internal constant PROOF_QUOTIENT_POLY_PARTS_1_Y_SLOT = 0x200 + 0x520 + 0x240; + uint256 internal constant PROOF_QUOTIENT_POLY_PARTS_2_X_SLOT = 0x200 + 0x520 + 0x260; + uint256 internal constant PROOF_QUOTIENT_POLY_PARTS_2_Y_SLOT = 0x200 + 0x520 + 0x280; + uint256 internal constant PROOF_QUOTIENT_POLY_PARTS_3_X_SLOT = 0x200 + 0x520 + 0x2a0; + uint256 internal constant PROOF_QUOTIENT_POLY_PARTS_3_Y_SLOT = 0x200 + 0x520 + 0x2c0; + + uint256 internal constant PROOF_STATE_POLYS_0_OPENING_AT_Z_SLOT = 0x200 + 0x520 + 0x2e0; + uint256 internal constant PROOF_STATE_POLYS_1_OPENING_AT_Z_SLOT = 0x200 + 0x520 + 0x300; + uint256 internal constant PROOF_STATE_POLYS_2_OPENING_AT_Z_SLOT = 0x200 + 0x520 + 0x320; + uint256 internal constant PROOF_STATE_POLYS_3_OPENING_AT_Z_SLOT = 0x200 + 0x520 + 0x340; + + uint256 internal constant PROOF_STATE_POLYS_3_OPENING_AT_Z_OMEGA_SLOT = 0x200 + 0x520 + 0x360; + uint256 internal constant PROOF_GATE_SELECTORS_0_OPENING_AT_Z_SLOT = 0x200 + 0x520 + 0x380; + + uint256 internal constant PROOF_COPY_PERMUTATION_POLYS_0_OPENING_AT_Z_SLOT = 0x200 + 0x520 + 0x3a0; + uint256 internal constant PROOF_COPY_PERMUTATION_POLYS_1_OPENING_AT_Z_SLOT = 0x200 + 0x520 + 0x3c0; + uint256 internal constant PROOF_COPY_PERMUTATION_POLYS_2_OPENING_AT_Z_SLOT = 0x200 + 0x520 + 0x3e0; + + uint256 internal constant PROOF_COPY_PERMUTATION_GRAND_PRODUCT_OPENING_AT_Z_OMEGA_SLOT = 0x200 + 0x520 + 0x400; + uint256 internal constant PROOF_LOOKUP_S_POLY_OPENING_AT_Z_OMEGA_SLOT = 0x200 + 0x520 + 0x420; + uint256 internal constant PROOF_LOOKUP_GRAND_PRODUCT_OPENING_AT_Z_OMEGA_SLOT = 0x200 + 0x520 + 0x440; + uint256 internal constant PROOF_LOOKUP_T_POLY_OPENING_AT_Z_SLOT = 0x200 + 0x520 + 0x460; + uint256 internal constant PROOF_LOOKUP_T_POLY_OPENING_AT_Z_OMEGA_SLOT = 0x200 + 0x520 + 0x480; + uint256 internal constant PROOF_LOOKUP_SELECTOR_POLY_OPENING_AT_Z_SLOT = 0x200 + 0x520 + 0x4a0; + uint256 internal constant PROOF_LOOKUP_TABLE_TYPE_POLY_OPENING_AT_Z_SLOT = 0x200 + 0x520 + 0x4c0; + uint256 internal constant PROOF_QUOTIENT_POLY_OPENING_AT_Z_SLOT = 0x200 + 0x520 + 0x4e0; + uint256 internal constant PROOF_LINEARISATION_POLY_OPENING_AT_Z_SLOT = 0x200 + 0x520 + 0x500; + + uint256 internal constant PROOF_OPENING_PROOF_AT_Z_X_SLOT = 0x200 + 0x520 + 0x520; + uint256 internal constant PROOF_OPENING_PROOF_AT_Z_Y_SLOT = 0x200 + 0x520 + 0x540; + uint256 internal constant PROOF_OPENING_PROOF_AT_Z_OMEGA_X_SLOT = 0x200 + 0x520 + 0x560; + uint256 internal constant PROOF_OPENING_PROOF_AT_Z_OMEGA_Y_SLOT = 0x200 + 0x520 + 0x580; + + uint256 internal constant PROOF_RECURSIVE_PART_P1_X_SLOT = 0x200 + 0x520 + 0x5a0; + uint256 internal constant PROOF_RECURSIVE_PART_P1_Y_SLOT = 0x200 + 0x520 + 0x5c0; + + uint256 internal constant PROOF_RECURSIVE_PART_P2_X_SLOT = 0x200 + 0x520 + 0x5e0; + uint256 internal constant PROOF_RECURSIVE_PART_P2_Y_SLOT = 0x200 + 0x520 + 0x600; + + /*////////////////////////////////////////////////////////////// + Transcript slot + //////////////////////////////////////////////////////////////*/ + + uint256 internal constant TRANSCRIPT_BEGIN_SLOT = 0x200 + 0x520 + 0x620 + 0x00; + uint256 internal constant TRANSCRIPT_DST_BYTE_SLOT = 0x200 + 0x520 + 0x620 + 0x03; + uint256 internal constant TRANSCRIPT_STATE_0_SLOT = 0x200 + 0x520 + 0x620 + 0x04; + uint256 internal constant TRANSCRIPT_STATE_1_SLOT = 0x200 + 0x520 + 0x620 + 0x24; + uint256 internal constant TRANSCRIPT_CHALLENGE_SLOT = 0x200 + 0x520 + 0x620 + 0x44; + + /*////////////////////////////////////////////////////////////// + Partial verifier state + //////////////////////////////////////////////////////////////*/ + + uint256 internal constant STATE_ALPHA_SLOT = 0x200 + 0x520 + 0x620 + 0x80 + 0x000; + uint256 internal constant STATE_BETA_SLOT = 0x200 + 0x520 + 0x620 + 0x80 + 0x020; + uint256 internal constant STATE_GAMMA_SLOT = 0x200 + 0x520 + 0x620 + 0x80 + 0x040; + uint256 internal constant STATE_POWER_OF_ALPHA_2_SLOT = 0x200 + 0x520 + 0x620 + 0x80 + 0x060; + uint256 internal constant STATE_POWER_OF_ALPHA_3_SLOT = 0x200 + 0x520 + 0x620 + 0x80 + 0x080; + uint256 internal constant STATE_POWER_OF_ALPHA_4_SLOT = 0x200 + 0x520 + 0x620 + 0x80 + 0x0a0; + uint256 internal constant STATE_POWER_OF_ALPHA_5_SLOT = 0x200 + 0x520 + 0x620 + 0x80 + 0x0c0; + uint256 internal constant STATE_POWER_OF_ALPHA_6_SLOT = 0x200 + 0x520 + 0x620 + 0x80 + 0x0e0; + uint256 internal constant STATE_POWER_OF_ALPHA_7_SLOT = 0x200 + 0x520 + 0x620 + 0x80 + 0x100; + uint256 internal constant STATE_POWER_OF_ALPHA_8_SLOT = 0x200 + 0x520 + 0x620 + 0x80 + 0x120; + uint256 internal constant STATE_ETA_SLOT = 0x200 + 0x520 + 0x620 + 0x80 + 0x140; + uint256 internal constant STATE_BETA_LOOKUP_SLOT = 0x200 + 0x520 + 0x620 + 0x80 + 0x160; + uint256 internal constant STATE_GAMMA_LOOKUP_SLOT = 0x200 + 0x520 + 0x620 + 0x80 + 0x180; + uint256 internal constant STATE_BETA_PLUS_ONE_SLOT = 0x200 + 0x520 + 0x620 + 0x80 + 0x1a0; + uint256 internal constant STATE_BETA_GAMMA_PLUS_GAMMA_SLOT = 0x200 + 0x520 + 0x620 + 0x80 + 0x1c0; + uint256 internal constant STATE_V_SLOT = 0x200 + 0x520 + 0x620 + 0x80 + 0x1e0; + uint256 internal constant STATE_U_SLOT = 0x200 + 0x520 + 0x620 + 0x80 + 0x200; + uint256 internal constant STATE_Z_SLOT = 0x200 + 0x520 + 0x620 + 0x80 + 0x220; + uint256 internal constant STATE_Z_MINUS_LAST_OMEGA_SLOT = 0x200 + 0x520 + 0x620 + 0x80 + 0x240; + uint256 internal constant STATE_L_0_AT_Z_SLOT = 0x200 + 0x520 + 0x620 + 0x80 + 0x260; + uint256 internal constant STATE_L_N_MINUS_ONE_AT_Z_SLOT = 0x200 + 0x520 + 0x620 + 0x80 + 0x280; + uint256 internal constant STATE_Z_IN_DOMAIN_SIZE = 0x200 + 0x520 + 0x620 + 0x80 + 0x2a0; + + /*////////////////////////////////////////////////////////////// + Queries + //////////////////////////////////////////////////////////////*/ + + uint256 internal constant QUERIES_BUFFER_POINT_SLOT = 0x200 + 0x520 + 0x620 + 0x80 + 0x2c0 + 0x00; + + uint256 internal constant QUERIES_AT_Z_0_X_SLOT = 0x200 + 0x520 + 0x620 + 0x80 + 0x2c0 + 0x40; + uint256 internal constant QUERIES_AT_Z_0_Y_SLOT = 0x200 + 0x520 + 0x620 + 0x80 + 0x2c0 + 0x60; + uint256 internal constant QUERIES_AT_Z_1_X_SLOT = 0x200 + 0x520 + 0x620 + 0x80 + 0x2c0 + 0x80; + uint256 internal constant QUERIES_AT_Z_1_Y_SLOT = 0x200 + 0x520 + 0x620 + 0x80 + 0x2c0 + 0xa0; + + uint256 internal constant QUERIES_T_POLY_AGGREGATED_X_SLOT = 0x200 + 0x520 + 0x620 + 0x80 + 0x2c0 + 0xc0; + uint256 internal constant QUERIES_T_POLY_AGGREGATED_Y_SLOT = 0x200 + 0x520 + 0x620 + 0x80 + 0x2c0 + 0xe0; + + /*////////////////////////////////////////////////////////////// + Aggregated commitment + //////////////////////////////////////////////////////////////*/ + + uint256 internal constant AGGREGATED_AT_Z_X_SLOT = 0x200 + 0x520 + 0x620 + 0x80 + 0x2c0 + 0x100 + 0x00; + uint256 internal constant AGGREGATED_AT_Z_Y_SLOT = 0x200 + 0x520 + 0x620 + 0x80 + 0x2c0 + 0x100 + 0x20; + + uint256 internal constant AGGREGATED_AT_Z_OMEGA_X_SLOT = 0x200 + 0x520 + 0x620 + 0x80 + 0x2c0 + 0x100 + 0x40; + uint256 internal constant AGGREGATED_AT_Z_OMEGA_Y_SLOT = 0x200 + 0x520 + 0x620 + 0x80 + 0x2c0 + 0x100 + 0x60; + + uint256 internal constant AGGREGATED_OPENING_AT_Z_SLOT = 0x200 + 0x520 + 0x620 + 0x80 + 0x2c0 + 0x100 + 0x80; + uint256 internal constant AGGREGATED_OPENING_AT_Z_OMEGA_SLOT = 0x200 + 0x520 + 0x620 + 0x80 + 0x2c0 + 0x100 + 0xa0; + + /*////////////////////////////////////////////////////////////// + Pairing data + //////////////////////////////////////////////////////////////*/ + + uint256 internal constant PAIRING_BUFFER_POINT_X_SLOT = 0x200 + 0x520 + 0x620 + 0x80 + 0x2c0 + 0x100 + 0xc0 + 0x00; + uint256 internal constant PAIRING_BUFFER_POINT_Y_SLOT = 0x200 + 0x520 + 0x620 + 0x80 + 0x2c0 + 0x100 + 0xc0 + 0x20; + + uint256 internal constant PAIRING_PAIR_WITH_GENERATOR_X_SLOT = + 0x200 + 0x520 + 0x620 + 0x80 + 0x2c0 + 0x100 + 0xc0 + 0x40; + uint256 internal constant PAIRING_PAIR_WITH_GENERATOR_Y_SLOT = + 0x200 + 0x520 + 0x620 + 0x80 + 0x2c0 + 0x100 + 0xc0 + 0x60; + + uint256 internal constant PAIRING_PAIR_WITH_X_X_SLOT = 0x200 + 0x520 + 0x620 + 0x80 + 0x2c0 + 0x100 + 0x100 + 0x80; + uint256 internal constant PAIRING_PAIR_WITH_X_Y_SLOT = 0x200 + 0x520 + 0x620 + 0x80 + 0x2c0 + 0x100 + 0x100 + 0xa0; + + /*////////////////////////////////////////////////////////////// + Slots for scalar multiplication optimizations + //////////////////////////////////////////////////////////////*/ + + uint256 internal constant COPY_PERMUTATION_FIRST_AGGREGATED_COMMITMENT_COEFF = + 0x200 + 0x520 + 0x620 + 0x80 + 0x2c0 + 0x100 + 0x100 + 0xc0; + uint256 internal constant LOOKUP_GRAND_PRODUCT_FIRST_AGGREGATED_COMMITMENT_COEFF = + 0x200 + 0x520 + 0x620 + 0x80 + 0x2c0 + 0x100 + 0x100 + 0xe0; + uint256 internal constant LOOKUP_S_FIRST_AGGREGATED_COMMITMENT_COEFF = + 0x200 + 0x520 + 0x620 + 0x80 + 0x2c0 + 0x100 + 0x100 + 0x100; + + /*////////////////////////////////////////////////////////////// + Constants + //////////////////////////////////////////////////////////////*/ + + uint256 internal constant OMEGA = 0x1951441010b2b95a6e47a6075066a50a036f5ba978c050f2821df86636c0facb; + uint256 internal constant DOMAIN_SIZE = 0x1000000; // 2^24 + uint256 internal constant Q_MOD = 21888242871839275222246405745257275088696311157297823662689037894645226208583; + uint256 internal constant R_MOD = 21888242871839275222246405745257275088548364400416034343698204186575808495617; + + /// @dev flip of 0xe000000000000000000000000000000000000000000000000000000000000000; + uint256 internal constant FR_MASK = 0x1fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; + + // non residues + uint256 internal constant NON_RESIDUES_0 = 0x05; + uint256 internal constant NON_RESIDUES_1 = 0x07; + uint256 internal constant NON_RESIDUES_2 = 0x0a; + + // trusted setup g2 elements + uint256 internal constant G2_ELEMENTS_0_X1 = 0x198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c2; + uint256 internal constant G2_ELEMENTS_0_X2 = 0x1800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed; + uint256 internal constant G2_ELEMENTS_0_Y1 = 0x090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b; + uint256 internal constant G2_ELEMENTS_0_Y2 = 0x12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa; + uint256 internal constant G2_ELEMENTS_1_X1 = 0x260e01b251f6f1c7e7ff4e580791dee8ea51d87a358e038b4efe30fac09383c1; + uint256 internal constant G2_ELEMENTS_1_X2 = 0x0118c4d5b837bcc2bc89b5b398b5974e9f5944073b32078b7e231fec938883b0; + uint256 internal constant G2_ELEMENTS_1_Y1 = 0x04fc6369f7110fe3d25156c1bb9a72859cf2a04641f99ba4ee413c80da6a5fe4; + uint256 internal constant G2_ELEMENTS_1_Y2 = 0x22febda3c0c0632a56475b4214e5615e11e6dd3f96e6cea2854a87d4dacc5e55; + + /// @inheritdoc IVerifier + function verificationKeyHash() external pure returns (bytes32 vkHash) { + _loadVerificationKey(); + + assembly { + let start := VK_GATE_SETUP_0_X_SLOT + let end := VK_RECURSIVE_FLAG_SLOT + let length := add(sub(end, start), 0x20) + + vkHash := keccak256(start, length) + } + } + + /// @notice Load verification keys to memory in runtime. + /// @dev The constants are loaded into memory in a specific layout declared in the constants starting from + /// `VK_` prefix. + /// NOTE: Function may corrupt the memory state if some memory was used before this function was called. + /// The VK consists of commitments to setup polynomials: + /// [q_a], [q_b], [q_c], [q_d], - main gate setup commitments + /// [q_{d_next}], [q_ab], [q_ac], [q_const] / + /// [main_gate_selector], [custom_gate_selector] - gate selectors commitments + /// [sigma_0], [sigma_1], [sigma_2], [sigma_3] - permutation polynomials commitments + /// [lookup_selector] - lookup selector commitment + /// [col_0], [col_1], [col_2], [col_3] - lookup columns commitments + /// [table_type] - lookup table type commitment + function _loadVerificationKey() internal pure virtual { + assembly { + // gate setup commitments + mstore(VK_GATE_SETUP_0_X_SLOT, 0x110deb1e0863737f9a3d7b4de641a03aa00a77bc9f1a05acc9d55b76ab9fdd4d) + mstore(VK_GATE_SETUP_0_Y_SLOT, 0x2c9dc252441e9298b7f6df6335a252517b7bccb924adf537b87c5cd3383fd7a9) + mstore(VK_GATE_SETUP_1_X_SLOT, 0x04659caf7b05471ba5ba85b1ab62267aa6c456836e625f169f7119d55b9462d2) + mstore(VK_GATE_SETUP_1_Y_SLOT, 0x0ea63403692148d2ad22189a1e5420076312f4d46e62036a043a6b0b84d5b410) + mstore(VK_GATE_SETUP_2_X_SLOT, 0x0e6696d09d65fce1e42805be03fca1f14aea247281f688981f925e77d4ce2291) + mstore(VK_GATE_SETUP_2_Y_SLOT, 0x0228f6cf8fe20c1e07e5b78bf8c41d50e55975a126d22a198d1e56acd4bbb3dd) + mstore(VK_GATE_SETUP_3_X_SLOT, 0x14685dafe340b1dec5eafcd5e7faddaf24f3781ddc53309cc25d0b42c00541dd) + mstore(VK_GATE_SETUP_3_Y_SLOT, 0x0e651cff9447cb360198899b80fa23e89ec13bc94ff161729aa841d2b55ea5be) + mstore(VK_GATE_SETUP_4_X_SLOT, 0x16e9ef76cb68f2750eb0ee72382dd9911a982308d0ab10ef94dada13c382ae73) + mstore(VK_GATE_SETUP_4_Y_SLOT, 0x22e404bc91350f3bc7daad1d1025113742436983c85eac5ab7b42221a181b81e) + mstore(VK_GATE_SETUP_5_X_SLOT, 0x0d9b29613037a5025655c82b143d2b7449c98f3aea358307c8529249cc54f3b9) + mstore(VK_GATE_SETUP_5_Y_SLOT, 0x15b3c4c946ad1babfc4c03ff7c2423fd354af3a9305c499b7fb3aaebe2fee746) + mstore(VK_GATE_SETUP_6_X_SLOT, 0x2a4cb6c495dbc7201142cc773da895ae2046e790073988fb850aca6aead27b8a) + mstore(VK_GATE_SETUP_6_Y_SLOT, 0x28ef9200c3cb67da82030520d640292014f5f7c2e2909da608812e04671a3acf) + mstore(VK_GATE_SETUP_7_X_SLOT, 0x283344a1ab3e55ecfd904d0b8e9f4faea338df5a4ead2fa9a42f0e103da40abc) + mstore(VK_GATE_SETUP_7_Y_SLOT, 0x223b37b83b9687512d322993edd70e508dd80adb10bcf7321a3cc8a44c269521) + + // gate selectors commitments + mstore(VK_GATE_SELECTORS_0_X_SLOT, 0x1f67f0ba5f7e837bc680acb4e612ebd938ad35211aa6e05b96cad19e66b82d2d) + mstore(VK_GATE_SELECTORS_0_Y_SLOT, 0x2820641a84d2e8298ac2ac42bd4b912c0c37f768ecc83d3a29e7c720763d15a1) + mstore(VK_GATE_SELECTORS_1_X_SLOT, 0x0353257957562270292a17860ca8e8827703f828f440ee004848b1e23fdf9de2) + mstore(VK_GATE_SELECTORS_1_Y_SLOT, 0x305f4137fee253dff8b2bfe579038e8f25d5bd217865072af5d89fc8800ada24) + + // permutation commitments + mstore(VK_PERMUTATION_0_X_SLOT, 0x13a600154b369ff3237706d00948e465ee1c32c7a6d3e18bccd9c4a15910f2e5) + mstore(VK_PERMUTATION_0_Y_SLOT, 0x138aa24fbf4cdddc75114811b3d59040394c218ecef3eb46ef9bd646f7e53776) + mstore(VK_PERMUTATION_1_X_SLOT, 0x277fff1f80c409357e2d251d79f6e3fd2164b755ce69cfd72de5c690289df662) + mstore(VK_PERMUTATION_1_Y_SLOT, 0x25235588e28c70eea3e35531c80deac25cd9b53ea3f98993f120108bc7abf670) + mstore(VK_PERMUTATION_2_X_SLOT, 0x0990e07a9b001048b947d0e5bd6157214c7359b771f01bf52bd771ba563a900e) + mstore(VK_PERMUTATION_2_Y_SLOT, 0x05e5fb090dd40914c8606d875e301167ae3047d684a02b44d9d36f1eaf43d0b4) + mstore(VK_PERMUTATION_3_X_SLOT, 0x1d4656690b33299db5631401a282afab3e16c78ee2c9ad9efea628171dcbc6bc) + mstore(VK_PERMUTATION_3_Y_SLOT, 0x0ebda2ebe582f601f813ec1e3970d13ef1500c742a85cce9b7f190f333de03b0) + + // lookup tables commitments + mstore(VK_LOOKUP_TABLE_0_X_SLOT, 0x2c513ed74d9d57a5ec901e074032741036353a2c4513422e96e7b53b302d765b) + mstore(VK_LOOKUP_TABLE_0_Y_SLOT, 0x04dd964427e430f16004076d708c0cb21e225056cc1d57418cfbd3d472981468) + mstore(VK_LOOKUP_TABLE_1_X_SLOT, 0x1ea83e5e65c6f8068f4677e2911678cf329b28259642a32db1f14b8347828aac) + mstore(VK_LOOKUP_TABLE_1_Y_SLOT, 0x1d22bc884a2da4962a893ba8de13f57aaeb785ed52c5e686994839cab8f7475d) + mstore(VK_LOOKUP_TABLE_2_X_SLOT, 0x0b2e7212d0d9cff26d0bdf3d79b2cac029a25dfeb1cafdf49e2349d7db348d89) + mstore(VK_LOOKUP_TABLE_2_Y_SLOT, 0x1301f9b252419ea240eb67fda720ca0b16d92364027285f95e9b1349490fa283) + mstore(VK_LOOKUP_TABLE_3_X_SLOT, 0x02f7b99fdfa5b418548c2d777785820e02383cfc87e7085e280a375a358153bf) + mstore(VK_LOOKUP_TABLE_3_Y_SLOT, 0x09d004fe08dc4d19c382df36fad22ef676185663543703e6a4b40203e50fd8a6) + + // lookup selector commitment + mstore(VK_LOOKUP_SELECTOR_X_SLOT, 0x2f4d347c7fb61daaadfff881e24f4b5dcfdc0d70a95bcb148168b90ef93e0007) + mstore(VK_LOOKUP_SELECTOR_Y_SLOT, 0x2322632465ba8e28cd0a4befd813ea85a972f4f6fa8e8603cf5d062dbcb14065) + + // table type commitment + mstore(VK_LOOKUP_TABLE_TYPE_X_SLOT, 0x1e3c9fc98c118e4bc34f1f93d214a5d86898e980c40d8e2c180c6ada377a7467) + mstore(VK_LOOKUP_TABLE_TYPE_Y_SLOT, 0x2260a13535c35a15c173f5e5797d4b675b55d164a9995bfb7624971324bd84a8) + + // flag for using recursive part + mstore(VK_RECURSIVE_FLAG_SLOT, 0) + } + } + + /// @inheritdoc IVerifier + function verify( + uint256[] calldata, // _publicInputs + uint256[] calldata, // _proof + uint256[] calldata // _recursiveAggregationInput + ) public view virtual returns (bool) { + // No memory was accessed yet, so keys can be loaded into the right place and not corrupt any other memory. + _loadVerificationKey(); + + // Beginning of the big inline assembly block that makes all the verification work. + // Note: We use the custom memory layout, so the return value should be returned from the assembly, not + // Solidity code. + assembly { + /*////////////////////////////////////////////////////////////// + Utils + //////////////////////////////////////////////////////////////*/ + + /// @dev Reverts execution with a provided revert reason. + /// @param len The byte length of the error message string, which is expected to be no more than 32. + /// @param reason The 1-word revert reason string, encoded in ASCII. + function revertWithMessage(len, reason) { + // "Error(string)" signature: bytes32(bytes4(keccak256("Error(string)"))) + mstore(0x00, 0x08c379a000000000000000000000000000000000000000000000000000000000) + // Data offset + mstore(0x04, 0x0000000000000000000000000000000000000000000000000000000000000020) + // Length of revert string + mstore(0x24, len) + // Revert reason + mstore(0x44, reason) + // Revert + revert(0x00, 0x64) + } + + /// @dev Performs modular exponentiation using the formula (value ^ power) mod R_MOD. + function modexp(value, power) -> res { + mstore(0x00, 0x20) + mstore(0x20, 0x20) + mstore(0x40, 0x20) + mstore(0x60, value) + mstore(0x80, power) + mstore(0xa0, R_MOD) + if iszero(staticcall(gas(), 5, 0, 0xc0, 0x00, 0x20)) { + revertWithMessage(24, "modexp precompile failed") + } + res := mload(0x00) + } + + /// @dev Performs a point multiplication operation and stores the result in a given memory destination. + function pointMulIntoDest(point, s, dest) { + mstore(0x00, mload(point)) + mstore(0x20, mload(add(point, 0x20))) + mstore(0x40, s) + if iszero(staticcall(gas(), 7, 0, 0x60, dest, 0x40)) { + revertWithMessage(30, "pointMulIntoDest: ecMul failed") + } + } + + /// @dev Performs a point addition operation and stores the result in a given memory destination. + function pointAddIntoDest(p1, p2, dest) { + mstore(0x00, mload(p1)) + mstore(0x20, mload(add(p1, 0x20))) + mstore(0x40, mload(p2)) + mstore(0x60, mload(add(p2, 0x20))) + if iszero(staticcall(gas(), 6, 0x00, 0x80, dest, 0x40)) { + revertWithMessage(30, "pointAddIntoDest: ecAdd failed") + } + } + + /// @dev Performs a point subtraction operation and updates the first point with the result. + function pointSubAssign(p1, p2) { + mstore(0x00, mload(p1)) + mstore(0x20, mload(add(p1, 0x20))) + mstore(0x40, mload(p2)) + mstore(0x60, sub(Q_MOD, mload(add(p2, 0x20)))) + if iszero(staticcall(gas(), 6, 0x00, 0x80, p1, 0x40)) { + revertWithMessage(28, "pointSubAssign: ecAdd failed") + } + } + + /// @dev Performs a point addition operation and updates the first point with the result. + function pointAddAssign(p1, p2) { + mstore(0x00, mload(p1)) + mstore(0x20, mload(add(p1, 0x20))) + mstore(0x40, mload(p2)) + mstore(0x60, mload(add(p2, 0x20))) + if iszero(staticcall(gas(), 6, 0x00, 0x80, p1, 0x40)) { + revertWithMessage(28, "pointAddAssign: ecAdd failed") + } + } + + /// @dev Performs a point multiplication operation and then adds the result to the destination point. + function pointMulAndAddIntoDest(point, s, dest) { + mstore(0x00, mload(point)) + mstore(0x20, mload(add(point, 0x20))) + mstore(0x40, s) + let success := staticcall(gas(), 7, 0, 0x60, 0, 0x40) + + mstore(0x40, mload(dest)) + mstore(0x60, mload(add(dest, 0x20))) + success := and(success, staticcall(gas(), 6, 0x00, 0x80, dest, 0x40)) + + if iszero(success) { + revertWithMessage(22, "pointMulAndAddIntoDest") + } + } + + /// @dev Negates an elliptic curve point by changing the sign of the y-coordinate. + function pointNegate(point) { + let pY := mload(add(point, 0x20)) + switch pY + case 0 { + if mload(point) { + revertWithMessage(26, "pointNegate: invalid point") + } + } + default { + mstore(add(point, 0x20), sub(Q_MOD, pY)) + } + } + + /*////////////////////////////////////////////////////////////// + Transcript helpers + //////////////////////////////////////////////////////////////*/ + + /// @dev Updates the transcript state with a new challenge value. + function updateTranscript(value) { + mstore8(TRANSCRIPT_DST_BYTE_SLOT, 0x00) + mstore(TRANSCRIPT_CHALLENGE_SLOT, value) + let newState0 := keccak256(TRANSCRIPT_BEGIN_SLOT, 0x64) + mstore8(TRANSCRIPT_DST_BYTE_SLOT, 0x01) + let newState1 := keccak256(TRANSCRIPT_BEGIN_SLOT, 0x64) + mstore(TRANSCRIPT_STATE_1_SLOT, newState1) + mstore(TRANSCRIPT_STATE_0_SLOT, newState0) + } + + /// @dev Retrieves a transcript challenge. + function getTranscriptChallenge(numberOfChallenge) -> challenge { + mstore8(TRANSCRIPT_DST_BYTE_SLOT, 0x02) + mstore(TRANSCRIPT_CHALLENGE_SLOT, shl(224, numberOfChallenge)) + challenge := and(keccak256(TRANSCRIPT_BEGIN_SLOT, 0x48), FR_MASK) + } + + /*////////////////////////////////////////////////////////////// + 1. Load Proof + //////////////////////////////////////////////////////////////*/ + + /// @dev This function loads a zk-SNARK proof, ensures it's properly formatted, and stores it in memory. + /// It ensures the number of inputs and the elliptic curve point's validity. + /// Note: It does NOT reject inputs that exceed these module sizes, but rather wraps them within the + /// module bounds. + /// The proof consists of: + /// 1. Public input: (1 field element from F_r) + /// + /// 2. Polynomial commitments (elliptic curve points over F_q): + /// [a], [b], [c], [d] - state polynomials commitments + /// [z_perm] - copy-permutation grand product commitment + /// [s] - polynomial for lookup argument commitment + /// [z_lookup] - lookup grand product commitment + /// [t_0], [t_1], [t_2], [t_3] - quotient polynomial parts commitments + /// [W], [W'] - proof openings commitments + /// + /// 3. Polynomial evaluations at z and z*omega (field elements from F_r): + /// t(z) - quotient polynomial opening + /// a(z), b(z), c(z), d(z), d(z*omega) - state polynomials openings + /// main_gate_selector(z) - main gate selector opening + /// sigma_0(z), sigma_1(z), sigma_2(z) - permutation polynomials openings + /// z_perm(z*omega) - copy-permutation grand product opening + /// z_lookup(z*omega) - lookup grand product opening + /// lookup_selector(z) - lookup selector opening + /// s(x*omega), t(z*omega), table_type(z) - lookup argument polynomial openings + /// r(z) - linearisation polynomial opening + /// + /// 4. Recursive proof (0 or 2 elliptic curve points over F_q) + function loadProof() { + // 1. Load public input + let offset := calldataload(0x04) + let publicInputLengthInWords := calldataload(add(offset, 0x04)) + let isValid := eq(publicInputLengthInWords, 1) // We expect only one public input + mstore(PROOF_PUBLIC_INPUT, and(calldataload(add(offset, 0x24)), FR_MASK)) + + // 2. Load the proof (except for the recursive part) + offset := calldataload(0x24) + let proofLengthInWords := calldataload(add(offset, 0x04)) + isValid := and(eq(proofLengthInWords, 44), isValid) + + // PROOF_STATE_POLYS_0 + { + let x := mod(calldataload(add(offset, 0x024)), Q_MOD) + let y := mod(calldataload(add(offset, 0x044)), Q_MOD) + let xx := mulmod(x, x, Q_MOD) + isValid := and(eq(mulmod(y, y, Q_MOD), addmod(mulmod(x, xx, Q_MOD), 3, Q_MOD)), isValid) + mstore(PROOF_STATE_POLYS_0_X_SLOT, x) + mstore(PROOF_STATE_POLYS_0_Y_SLOT, y) + } + // PROOF_STATE_POLYS_1 + { + let x := mod(calldataload(add(offset, 0x064)), Q_MOD) + let y := mod(calldataload(add(offset, 0x084)), Q_MOD) + let xx := mulmod(x, x, Q_MOD) + isValid := and(eq(mulmod(y, y, Q_MOD), addmod(mulmod(x, xx, Q_MOD), 3, Q_MOD)), isValid) + mstore(PROOF_STATE_POLYS_1_X_SLOT, x) + mstore(PROOF_STATE_POLYS_1_Y_SLOT, y) + } + // PROOF_STATE_POLYS_2 + { + let x := mod(calldataload(add(offset, 0x0a4)), Q_MOD) + let y := mod(calldataload(add(offset, 0x0c4)), Q_MOD) + let xx := mulmod(x, x, Q_MOD) + isValid := and(eq(mulmod(y, y, Q_MOD), addmod(mulmod(x, xx, Q_MOD), 3, Q_MOD)), isValid) + mstore(PROOF_STATE_POLYS_2_X_SLOT, x) + mstore(PROOF_STATE_POLYS_2_Y_SLOT, y) + } + // PROOF_STATE_POLYS_3 + { + let x := mod(calldataload(add(offset, 0x0e4)), Q_MOD) + let y := mod(calldataload(add(offset, 0x104)), Q_MOD) + let xx := mulmod(x, x, Q_MOD) + isValid := and(eq(mulmod(y, y, Q_MOD), addmod(mulmod(x, xx, Q_MOD), 3, Q_MOD)), isValid) + mstore(PROOF_STATE_POLYS_3_X_SLOT, x) + mstore(PROOF_STATE_POLYS_3_Y_SLOT, y) + } + // PROOF_COPY_PERMUTATION_GRAND_PRODUCT + { + let x := mod(calldataload(add(offset, 0x124)), Q_MOD) + let y := mod(calldataload(add(offset, 0x144)), Q_MOD) + let xx := mulmod(x, x, Q_MOD) + isValid := and(eq(mulmod(y, y, Q_MOD), addmod(mulmod(x, xx, Q_MOD), 3, Q_MOD)), isValid) + mstore(PROOF_COPY_PERMUTATION_GRAND_PRODUCT_X_SLOT, x) + mstore(PROOF_COPY_PERMUTATION_GRAND_PRODUCT_Y_SLOT, y) + } + // PROOF_LOOKUP_S_POLY + { + let x := mod(calldataload(add(offset, 0x164)), Q_MOD) + let y := mod(calldataload(add(offset, 0x184)), Q_MOD) + let xx := mulmod(x, x, Q_MOD) + isValid := and(eq(mulmod(y, y, Q_MOD), addmod(mulmod(x, xx, Q_MOD), 3, Q_MOD)), isValid) + mstore(PROOF_LOOKUP_S_POLY_X_SLOT, x) + mstore(PROOF_LOOKUP_S_POLY_Y_SLOT, y) + } + // PROOF_LOOKUP_GRAND_PRODUCT + { + let x := mod(calldataload(add(offset, 0x1a4)), Q_MOD) + let y := mod(calldataload(add(offset, 0x1c4)), Q_MOD) + let xx := mulmod(x, x, Q_MOD) + isValid := and(eq(mulmod(y, y, Q_MOD), addmod(mulmod(x, xx, Q_MOD), 3, Q_MOD)), isValid) + mstore(PROOF_LOOKUP_GRAND_PRODUCT_X_SLOT, x) + mstore(PROOF_LOOKUP_GRAND_PRODUCT_Y_SLOT, y) + } + // PROOF_QUOTIENT_POLY_PARTS_0 + { + let x := mod(calldataload(add(offset, 0x1e4)), Q_MOD) + let y := mod(calldataload(add(offset, 0x204)), Q_MOD) + let xx := mulmod(x, x, Q_MOD) + isValid := and(eq(mulmod(y, y, Q_MOD), addmod(mulmod(x, xx, Q_MOD), 3, Q_MOD)), isValid) + mstore(PROOF_QUOTIENT_POLY_PARTS_0_X_SLOT, x) + mstore(PROOF_QUOTIENT_POLY_PARTS_0_Y_SLOT, y) + } + // PROOF_QUOTIENT_POLY_PARTS_1 + { + let x := mod(calldataload(add(offset, 0x224)), Q_MOD) + let y := mod(calldataload(add(offset, 0x244)), Q_MOD) + let xx := mulmod(x, x, Q_MOD) + isValid := and(eq(mulmod(y, y, Q_MOD), addmod(mulmod(x, xx, Q_MOD), 3, Q_MOD)), isValid) + mstore(PROOF_QUOTIENT_POLY_PARTS_1_X_SLOT, x) + mstore(PROOF_QUOTIENT_POLY_PARTS_1_Y_SLOT, y) + } + // PROOF_QUOTIENT_POLY_PARTS_2 + { + let x := mod(calldataload(add(offset, 0x264)), Q_MOD) + let y := mod(calldataload(add(offset, 0x284)), Q_MOD) + let xx := mulmod(x, x, Q_MOD) + isValid := and(eq(mulmod(y, y, Q_MOD), addmod(mulmod(x, xx, Q_MOD), 3, Q_MOD)), isValid) + mstore(PROOF_QUOTIENT_POLY_PARTS_2_X_SLOT, x) + mstore(PROOF_QUOTIENT_POLY_PARTS_2_Y_SLOT, y) + } + // PROOF_QUOTIENT_POLY_PARTS_3 + { + let x := mod(calldataload(add(offset, 0x2a4)), Q_MOD) + let y := mod(calldataload(add(offset, 0x2c4)), Q_MOD) + let xx := mulmod(x, x, Q_MOD) + isValid := and(eq(mulmod(y, y, Q_MOD), addmod(mulmod(x, xx, Q_MOD), 3, Q_MOD)), isValid) + mstore(PROOF_QUOTIENT_POLY_PARTS_3_X_SLOT, x) + mstore(PROOF_QUOTIENT_POLY_PARTS_3_Y_SLOT, y) + } + + mstore(PROOF_STATE_POLYS_0_OPENING_AT_Z_SLOT, mod(calldataload(add(offset, 0x2e4)), R_MOD)) + mstore(PROOF_STATE_POLYS_1_OPENING_AT_Z_SLOT, mod(calldataload(add(offset, 0x304)), R_MOD)) + mstore(PROOF_STATE_POLYS_2_OPENING_AT_Z_SLOT, mod(calldataload(add(offset, 0x324)), R_MOD)) + mstore(PROOF_STATE_POLYS_3_OPENING_AT_Z_SLOT, mod(calldataload(add(offset, 0x344)), R_MOD)) + + mstore(PROOF_STATE_POLYS_3_OPENING_AT_Z_OMEGA_SLOT, mod(calldataload(add(offset, 0x364)), R_MOD)) + mstore(PROOF_GATE_SELECTORS_0_OPENING_AT_Z_SLOT, mod(calldataload(add(offset, 0x384)), R_MOD)) + + mstore(PROOF_COPY_PERMUTATION_POLYS_0_OPENING_AT_Z_SLOT, mod(calldataload(add(offset, 0x3a4)), R_MOD)) + mstore(PROOF_COPY_PERMUTATION_POLYS_1_OPENING_AT_Z_SLOT, mod(calldataload(add(offset, 0x3c4)), R_MOD)) + mstore(PROOF_COPY_PERMUTATION_POLYS_2_OPENING_AT_Z_SLOT, mod(calldataload(add(offset, 0x3e4)), R_MOD)) + + mstore( + PROOF_COPY_PERMUTATION_GRAND_PRODUCT_OPENING_AT_Z_OMEGA_SLOT, + mod(calldataload(add(offset, 0x404)), R_MOD) + ) + mstore(PROOF_LOOKUP_S_POLY_OPENING_AT_Z_OMEGA_SLOT, mod(calldataload(add(offset, 0x424)), R_MOD)) + mstore(PROOF_LOOKUP_GRAND_PRODUCT_OPENING_AT_Z_OMEGA_SLOT, mod(calldataload(add(offset, 0x444)), R_MOD)) + mstore(PROOF_LOOKUP_T_POLY_OPENING_AT_Z_SLOT, mod(calldataload(add(offset, 0x464)), R_MOD)) + mstore(PROOF_LOOKUP_T_POLY_OPENING_AT_Z_OMEGA_SLOT, mod(calldataload(add(offset, 0x484)), R_MOD)) + mstore(PROOF_LOOKUP_SELECTOR_POLY_OPENING_AT_Z_SLOT, mod(calldataload(add(offset, 0x4a4)), R_MOD)) + mstore(PROOF_LOOKUP_TABLE_TYPE_POLY_OPENING_AT_Z_SLOT, mod(calldataload(add(offset, 0x4c4)), R_MOD)) + mstore(PROOF_QUOTIENT_POLY_OPENING_AT_Z_SLOT, mod(calldataload(add(offset, 0x4e4)), R_MOD)) + mstore(PROOF_LINEARISATION_POLY_OPENING_AT_Z_SLOT, mod(calldataload(add(offset, 0x504)), R_MOD)) + + // PROOF_OPENING_PROOF_AT_Z + { + let x := mod(calldataload(add(offset, 0x524)), Q_MOD) + let y := mod(calldataload(add(offset, 0x544)), Q_MOD) + let xx := mulmod(x, x, Q_MOD) + isValid := and(eq(mulmod(y, y, Q_MOD), addmod(mulmod(x, xx, Q_MOD), 3, Q_MOD)), isValid) + mstore(PROOF_OPENING_PROOF_AT_Z_X_SLOT, x) + mstore(PROOF_OPENING_PROOF_AT_Z_Y_SLOT, y) + } + // PROOF_OPENING_PROOF_AT_Z_OMEGA + { + let x := mod(calldataload(add(offset, 0x564)), Q_MOD) + let y := mod(calldataload(add(offset, 0x584)), Q_MOD) + let xx := mulmod(x, x, Q_MOD) + isValid := and(eq(mulmod(y, y, Q_MOD), addmod(mulmod(x, xx, Q_MOD), 3, Q_MOD)), isValid) + mstore(PROOF_OPENING_PROOF_AT_Z_OMEGA_X_SLOT, x) + mstore(PROOF_OPENING_PROOF_AT_Z_OMEGA_Y_SLOT, y) + } + + // 3. Load the recursive part of the proof + offset := calldataload(0x44) + let recursiveProofLengthInWords := calldataload(add(offset, 0x04)) + + switch mload(VK_RECURSIVE_FLAG_SLOT) + case 0 { + // recursive part should be empty + isValid := and(iszero(recursiveProofLengthInWords), isValid) + } + default { + // recursive part should be consist of 2 points + isValid := and(eq(recursiveProofLengthInWords, 4), isValid) + // PROOF_RECURSIVE_PART_P1 + { + let x := mod(calldataload(add(offset, 0x024)), Q_MOD) + let y := mod(calldataload(add(offset, 0x044)), Q_MOD) + let xx := mulmod(x, x, Q_MOD) + isValid := and(eq(mulmod(y, y, Q_MOD), addmod(mulmod(x, xx, Q_MOD), 3, Q_MOD)), isValid) + mstore(PROOF_RECURSIVE_PART_P1_X_SLOT, x) + mstore(PROOF_RECURSIVE_PART_P1_Y_SLOT, y) + } + // PROOF_RECURSIVE_PART_P2 + { + let x := mod(calldataload(add(offset, 0x064)), Q_MOD) + let y := mod(calldataload(add(offset, 0x084)), Q_MOD) + let xx := mulmod(x, x, Q_MOD) + isValid := and(eq(mulmod(y, y, Q_MOD), addmod(mulmod(x, xx, Q_MOD), 3, Q_MOD)), isValid) + mstore(PROOF_RECURSIVE_PART_P2_X_SLOT, x) + mstore(PROOF_RECURSIVE_PART_P2_Y_SLOT, y) + } + } + + // Revert if a proof is not valid + if iszero(isValid) { + revertWithMessage(27, "loadProof: Proof is invalid") + } + } + + /*////////////////////////////////////////////////////////////// + 2. Transcript initialization + //////////////////////////////////////////////////////////////*/ + + /// @notice Recomputes all challenges + /// @dev The process is the following: + /// Commit: PI, [a], [b], [c], [d] + /// Get: eta + /// Commit: [s] + /// Get: beta, gamma + /// Commit: [z_perm] + /// Get: beta', gamma' + /// Commit: [z_lookup] + /// Get: alpha + /// Commit: [t_0], [t_1], [t_2], [t_3] + /// Get: z + /// Commit: t(z), a(z), b(z), c(z), d(z), d(z*omega), + /// main_gate_selector(z), + /// sigma_0(z), sigma_1(z), sigma_2(z), + /// z_perm(z*omega), + /// t(z), lookup_selector(z), table_type(z), + /// s(x*omega), z_lookup(z*omega), t(z*omega), + /// r(z) + /// Get: v + /// Commit: [W], [W'] + /// Get: u + function initializeTranscript() { + // Round 1 + updateTranscript(mload(PROOF_PUBLIC_INPUT)) + updateTranscript(mload(PROOF_STATE_POLYS_0_X_SLOT)) + updateTranscript(mload(PROOF_STATE_POLYS_0_Y_SLOT)) + updateTranscript(mload(PROOF_STATE_POLYS_1_X_SLOT)) + updateTranscript(mload(PROOF_STATE_POLYS_1_Y_SLOT)) + updateTranscript(mload(PROOF_STATE_POLYS_2_X_SLOT)) + updateTranscript(mload(PROOF_STATE_POLYS_2_Y_SLOT)) + updateTranscript(mload(PROOF_STATE_POLYS_3_X_SLOT)) + updateTranscript(mload(PROOF_STATE_POLYS_3_Y_SLOT)) + + mstore(STATE_ETA_SLOT, getTranscriptChallenge(0)) + + // Round 1.5 + updateTranscript(mload(PROOF_LOOKUP_S_POLY_X_SLOT)) + updateTranscript(mload(PROOF_LOOKUP_S_POLY_Y_SLOT)) + + mstore(STATE_BETA_SLOT, getTranscriptChallenge(1)) + mstore(STATE_GAMMA_SLOT, getTranscriptChallenge(2)) + + // Round 2 + updateTranscript(mload(PROOF_COPY_PERMUTATION_GRAND_PRODUCT_X_SLOT)) + updateTranscript(mload(PROOF_COPY_PERMUTATION_GRAND_PRODUCT_Y_SLOT)) + + mstore(STATE_BETA_LOOKUP_SLOT, getTranscriptChallenge(3)) + mstore(STATE_GAMMA_LOOKUP_SLOT, getTranscriptChallenge(4)) + + // Round 2.5 + updateTranscript(mload(PROOF_LOOKUP_GRAND_PRODUCT_X_SLOT)) + updateTranscript(mload(PROOF_LOOKUP_GRAND_PRODUCT_Y_SLOT)) + + mstore(STATE_ALPHA_SLOT, getTranscriptChallenge(5)) + + // Round 3 + updateTranscript(mload(PROOF_QUOTIENT_POLY_PARTS_0_X_SLOT)) + updateTranscript(mload(PROOF_QUOTIENT_POLY_PARTS_0_Y_SLOT)) + updateTranscript(mload(PROOF_QUOTIENT_POLY_PARTS_1_X_SLOT)) + updateTranscript(mload(PROOF_QUOTIENT_POLY_PARTS_1_Y_SLOT)) + updateTranscript(mload(PROOF_QUOTIENT_POLY_PARTS_2_X_SLOT)) + updateTranscript(mload(PROOF_QUOTIENT_POLY_PARTS_2_Y_SLOT)) + updateTranscript(mload(PROOF_QUOTIENT_POLY_PARTS_3_X_SLOT)) + updateTranscript(mload(PROOF_QUOTIENT_POLY_PARTS_3_Y_SLOT)) + + { + let z := getTranscriptChallenge(6) + + mstore(STATE_Z_SLOT, z) + mstore(STATE_Z_IN_DOMAIN_SIZE, modexp(z, DOMAIN_SIZE)) + } + + // Round 4 + updateTranscript(mload(PROOF_QUOTIENT_POLY_OPENING_AT_Z_SLOT)) + + updateTranscript(mload(PROOF_STATE_POLYS_0_OPENING_AT_Z_SLOT)) + updateTranscript(mload(PROOF_STATE_POLYS_1_OPENING_AT_Z_SLOT)) + updateTranscript(mload(PROOF_STATE_POLYS_2_OPENING_AT_Z_SLOT)) + updateTranscript(mload(PROOF_STATE_POLYS_3_OPENING_AT_Z_SLOT)) + + updateTranscript(mload(PROOF_STATE_POLYS_3_OPENING_AT_Z_OMEGA_SLOT)) + updateTranscript(mload(PROOF_GATE_SELECTORS_0_OPENING_AT_Z_SLOT)) + + updateTranscript(mload(PROOF_COPY_PERMUTATION_POLYS_0_OPENING_AT_Z_SLOT)) + updateTranscript(mload(PROOF_COPY_PERMUTATION_POLYS_1_OPENING_AT_Z_SLOT)) + updateTranscript(mload(PROOF_COPY_PERMUTATION_POLYS_2_OPENING_AT_Z_SLOT)) + + updateTranscript(mload(PROOF_COPY_PERMUTATION_GRAND_PRODUCT_OPENING_AT_Z_OMEGA_SLOT)) + updateTranscript(mload(PROOF_LOOKUP_T_POLY_OPENING_AT_Z_SLOT)) + updateTranscript(mload(PROOF_LOOKUP_SELECTOR_POLY_OPENING_AT_Z_SLOT)) + updateTranscript(mload(PROOF_LOOKUP_TABLE_TYPE_POLY_OPENING_AT_Z_SLOT)) + updateTranscript(mload(PROOF_LOOKUP_S_POLY_OPENING_AT_Z_OMEGA_SLOT)) + updateTranscript(mload(PROOF_LOOKUP_GRAND_PRODUCT_OPENING_AT_Z_OMEGA_SLOT)) + updateTranscript(mload(PROOF_LOOKUP_T_POLY_OPENING_AT_Z_OMEGA_SLOT)) + updateTranscript(mload(PROOF_LINEARISATION_POLY_OPENING_AT_Z_SLOT)) + + mstore(STATE_V_SLOT, getTranscriptChallenge(7)) + + // Round 5 + updateTranscript(mload(PROOF_OPENING_PROOF_AT_Z_X_SLOT)) + updateTranscript(mload(PROOF_OPENING_PROOF_AT_Z_Y_SLOT)) + updateTranscript(mload(PROOF_OPENING_PROOF_AT_Z_OMEGA_X_SLOT)) + updateTranscript(mload(PROOF_OPENING_PROOF_AT_Z_OMEGA_Y_SLOT)) + + mstore(STATE_U_SLOT, getTranscriptChallenge(8)) + } + + /*////////////////////////////////////////////////////////////// + 3. Verifying quotient evaluation + //////////////////////////////////////////////////////////////*/ + + /// @notice Compute linearisation polynomial's constant term: r_0 + /// @dev To save a verifier scalar multiplication, we split linearisation polynomial + /// into its constant and non-constant terms. The constant term is computed with the formula: + /// + /// r_0 = alpha^0 * L_0(z) * PI * q_{main selector}(z) + r(z) -- main gate contribution + /// + /// - alpha^4 * z_perm(z*omega)(sigma_0(z) * beta + gamma + a(z)) \ + /// (sigma_1(z) * beta + gamma + b(z)) | + /// (sigma_2(z) * beta + gamma + c(z)) | - permutation contribution + /// (sigma_3(z) + gamma) | + /// - alpha^5 * L_0(z) / + /// + /// + alpha^6 * (s(z*omega) * beta' + gamma' (beta' + 1)) \ + /// * (z - omega^{n-1}) * z_lookup(z*omega) | - lookup contribution + /// - alpha^7 * L_0(z) | + /// - alpha^8 * L_{n-1}(z) * (gamma' (beta' + 1))^{n-1} / + /// + /// In the end we should check that t(z)*Z_H(z) = r(z) + r_0! + function verifyQuotientEvaluation() { + // Compute power of alpha + { + let alpha := mload(STATE_ALPHA_SLOT) + let currentAlpha := mulmod(alpha, alpha, R_MOD) + mstore(STATE_POWER_OF_ALPHA_2_SLOT, currentAlpha) + currentAlpha := mulmod(currentAlpha, alpha, R_MOD) + mstore(STATE_POWER_OF_ALPHA_3_SLOT, currentAlpha) + currentAlpha := mulmod(currentAlpha, alpha, R_MOD) + mstore(STATE_POWER_OF_ALPHA_4_SLOT, currentAlpha) + currentAlpha := mulmod(currentAlpha, alpha, R_MOD) + mstore(STATE_POWER_OF_ALPHA_5_SLOT, currentAlpha) + currentAlpha := mulmod(currentAlpha, alpha, R_MOD) + mstore(STATE_POWER_OF_ALPHA_6_SLOT, currentAlpha) + currentAlpha := mulmod(currentAlpha, alpha, R_MOD) + mstore(STATE_POWER_OF_ALPHA_7_SLOT, currentAlpha) + currentAlpha := mulmod(currentAlpha, alpha, R_MOD) + mstore(STATE_POWER_OF_ALPHA_8_SLOT, currentAlpha) + } + + // z + let stateZ := mload(STATE_Z_SLOT) + // L_0(z) + mstore(STATE_L_0_AT_Z_SLOT, evaluateLagrangePolyOutOfDomain(0, stateZ)) + // L_{n-1}(z) + mstore(STATE_L_N_MINUS_ONE_AT_Z_SLOT, evaluateLagrangePolyOutOfDomain(sub(DOMAIN_SIZE, 1), stateZ)) + // L_0(z) * PI + let stateT := mulmod(mload(STATE_L_0_AT_Z_SLOT), mload(PROOF_PUBLIC_INPUT), R_MOD) + + // Compute main gate contribution + let result := mulmod(stateT, mload(PROOF_GATE_SELECTORS_0_OPENING_AT_Z_SLOT), R_MOD) + + // Compute permutation contribution + result := addmod(result, permutationQuotientContribution(), R_MOD) + + // Compute lookup contribution + result := addmod(result, lookupQuotientContribution(), R_MOD) + + // Check that r(z) + r_0 = t(z) * Z_H(z) + result := addmod(mload(PROOF_LINEARISATION_POLY_OPENING_AT_Z_SLOT), result, R_MOD) + + let vanishing := addmod(mload(STATE_Z_IN_DOMAIN_SIZE), sub(R_MOD, 1), R_MOD) + let lhs := mulmod(mload(PROOF_QUOTIENT_POLY_OPENING_AT_Z_SLOT), vanishing, R_MOD) + if iszero(eq(lhs, result)) { + revertWithMessage(27, "invalid quotient evaluation") + } + } + + /// @notice Evaluating L_{polyNum}(at) out of domain + /// @dev L_i is a Lagrange polynomial for our domain such that: + /// L_i(omega^i) = 1 and L_i(omega^j) = 0 for all j != i + function evaluateLagrangePolyOutOfDomain(polyNum, at) -> res { + let omegaPower := 1 + if polyNum { + omegaPower := modexp(OMEGA, polyNum) + } + + res := addmod(modexp(at, DOMAIN_SIZE), sub(R_MOD, 1), R_MOD) + + // Vanishing polynomial can not be zero at point `at` + if iszero(res) { + revertWithMessage(28, "invalid vanishing polynomial") + } + res := mulmod(res, omegaPower, R_MOD) + let denominator := addmod(at, sub(R_MOD, omegaPower), R_MOD) + denominator := mulmod(denominator, DOMAIN_SIZE, R_MOD) + denominator := modexp(denominator, sub(R_MOD, 2)) + res := mulmod(res, denominator, R_MOD) + } + + /// @notice Compute permutation contribution to linearisation polynomial's constant term + function permutationQuotientContribution() -> res { + // res = alpha^4 * z_perm(z*omega) + res := mulmod( + mload(STATE_POWER_OF_ALPHA_4_SLOT), + mload(PROOF_COPY_PERMUTATION_GRAND_PRODUCT_OPENING_AT_Z_OMEGA_SLOT), + R_MOD + ) + + { + let gamma := mload(STATE_GAMMA_SLOT) + let beta := mload(STATE_BETA_SLOT) + + let factorMultiplier + { + // res *= sigma_0(z) * beta + gamma + a(z) + factorMultiplier := mulmod(mload(PROOF_COPY_PERMUTATION_POLYS_0_OPENING_AT_Z_SLOT), beta, R_MOD) + factorMultiplier := addmod(factorMultiplier, gamma, R_MOD) + factorMultiplier := addmod( + factorMultiplier, + mload(PROOF_STATE_POLYS_0_OPENING_AT_Z_SLOT), + R_MOD + ) + res := mulmod(res, factorMultiplier, R_MOD) + } + { + // res *= sigma_1(z) * beta + gamma + b(z) + factorMultiplier := mulmod(mload(PROOF_COPY_PERMUTATION_POLYS_1_OPENING_AT_Z_SLOT), beta, R_MOD) + factorMultiplier := addmod(factorMultiplier, gamma, R_MOD) + factorMultiplier := addmod( + factorMultiplier, + mload(PROOF_STATE_POLYS_1_OPENING_AT_Z_SLOT), + R_MOD + ) + res := mulmod(res, factorMultiplier, R_MOD) + } + { + // res *= sigma_2(z) * beta + gamma + c(z) + factorMultiplier := mulmod(mload(PROOF_COPY_PERMUTATION_POLYS_2_OPENING_AT_Z_SLOT), beta, R_MOD) + factorMultiplier := addmod(factorMultiplier, gamma, R_MOD) + factorMultiplier := addmod( + factorMultiplier, + mload(PROOF_STATE_POLYS_2_OPENING_AT_Z_SLOT), + R_MOD + ) + res := mulmod(res, factorMultiplier, R_MOD) + } + + // res *= sigma_3(z) + gamma + res := mulmod(res, addmod(mload(PROOF_STATE_POLYS_3_OPENING_AT_Z_SLOT), gamma, R_MOD), R_MOD) + } + + // res = -res + res := sub(R_MOD, res) + + // -= L_0(z) * alpha^5 + let l0AtZ := mload(STATE_L_0_AT_Z_SLOT) + l0AtZ := mulmod(l0AtZ, mload(STATE_POWER_OF_ALPHA_5_SLOT), R_MOD) + res := addmod(res, sub(R_MOD, l0AtZ), R_MOD) + } + + /// @notice Compute lookup contribution to linearisation polynomial's constant term + function lookupQuotientContribution() -> res { + let betaLookup := mload(STATE_BETA_LOOKUP_SLOT) + let gammaLookup := mload(STATE_GAMMA_LOOKUP_SLOT) + let betaPlusOne := addmod(betaLookup, 1, R_MOD) + let betaGamma := mulmod(betaPlusOne, gammaLookup, R_MOD) + + mstore(STATE_BETA_PLUS_ONE_SLOT, betaPlusOne) + mstore(STATE_BETA_GAMMA_PLUS_GAMMA_SLOT, betaGamma) + + // res = alpha^6 * (s(z*omega) * beta' + gamma' (beta' + 1)) * z_lookup(z*omega) + res := mulmod(mload(PROOF_LOOKUP_S_POLY_OPENING_AT_Z_OMEGA_SLOT), betaLookup, R_MOD) + res := addmod(res, betaGamma, R_MOD) + res := mulmod(res, mload(PROOF_LOOKUP_GRAND_PRODUCT_OPENING_AT_Z_OMEGA_SLOT), R_MOD) + res := mulmod(res, mload(STATE_POWER_OF_ALPHA_6_SLOT), R_MOD) + + // res *= z - omega^{n-1} + { + let lastOmega := modexp(OMEGA, sub(DOMAIN_SIZE, 1)) + let zMinusLastOmega := addmod(mload(STATE_Z_SLOT), sub(R_MOD, lastOmega), R_MOD) + mstore(STATE_Z_MINUS_LAST_OMEGA_SLOT, zMinusLastOmega) + res := mulmod(res, zMinusLastOmega, R_MOD) + } + + // res -= alpha^7 * L_{0}(z) + { + let intermediateValue := mulmod( + mload(STATE_L_0_AT_Z_SLOT), + mload(STATE_POWER_OF_ALPHA_7_SLOT), + R_MOD + ) + res := addmod(res, sub(R_MOD, intermediateValue), R_MOD) + } + + // res -= alpha^8 * L_{n-1}(z) * (gamma' (beta' + 1))^{n-1} + { + let lnMinusOneAtZ := mload(STATE_L_N_MINUS_ONE_AT_Z_SLOT) + let betaGammaPowered := modexp(betaGamma, sub(DOMAIN_SIZE, 1)) + let alphaPower8 := mload(STATE_POWER_OF_ALPHA_8_SLOT) + + let subtrahend := mulmod(mulmod(lnMinusOneAtZ, betaGammaPowered, R_MOD), alphaPower8, R_MOD) + res := addmod(res, sub(R_MOD, subtrahend), R_MOD) + } + } + + /// @notice Compute main gate contribution to linearisation polynomial commitment multiplied by v + function mainGateLinearisationContributionWithV( + dest, + stateOpening0AtZ, + stateOpening1AtZ, + stateOpening2AtZ, + stateOpening3AtZ + ) { + // += a(z) * [q_a] + pointMulIntoDest(VK_GATE_SETUP_0_X_SLOT, stateOpening0AtZ, dest) + // += b(z) * [q_b] + pointMulAndAddIntoDest(VK_GATE_SETUP_1_X_SLOT, stateOpening1AtZ, dest) + // += c(z) * [q_c] + pointMulAndAddIntoDest(VK_GATE_SETUP_2_X_SLOT, stateOpening2AtZ, dest) + // += d(z) * [q_d] + pointMulAndAddIntoDest(VK_GATE_SETUP_3_X_SLOT, stateOpening3AtZ, dest) + // += a(z) * b(z) * [q_ab] + pointMulAndAddIntoDest(VK_GATE_SETUP_4_X_SLOT, mulmod(stateOpening0AtZ, stateOpening1AtZ, R_MOD), dest) + // += a(z) * c(z) * [q_ac] + pointMulAndAddIntoDest(VK_GATE_SETUP_5_X_SLOT, mulmod(stateOpening0AtZ, stateOpening2AtZ, R_MOD), dest) + // += [q_const] + pointAddAssign(dest, VK_GATE_SETUP_6_X_SLOT) + // += d(z*omega) * [q_{d_next}] + pointMulAndAddIntoDest(VK_GATE_SETUP_7_X_SLOT, mload(PROOF_STATE_POLYS_3_OPENING_AT_Z_OMEGA_SLOT), dest) + + // *= v * main_gate_selector(z) + let coeff := mulmod(mload(PROOF_GATE_SELECTORS_0_OPENING_AT_Z_SLOT), mload(STATE_V_SLOT), R_MOD) + pointMulIntoDest(dest, coeff, dest) + } + + /// @notice Compute custom gate contribution to linearisation polynomial commitment multiplied by v + function addAssignRescueCustomGateLinearisationContributionWithV( + dest, + stateOpening0AtZ, + stateOpening1AtZ, + stateOpening2AtZ, + stateOpening3AtZ + ) { + let accumulator + let intermediateValue + // = alpha * (a(z)^2 - b(z)) + accumulator := mulmod(stateOpening0AtZ, stateOpening0AtZ, R_MOD) + accumulator := addmod(accumulator, sub(R_MOD, stateOpening1AtZ), R_MOD) + accumulator := mulmod(accumulator, mload(STATE_ALPHA_SLOT), R_MOD) + // += alpha^2 * (b(z)^2 - c(z)) + intermediateValue := mulmod(stateOpening1AtZ, stateOpening1AtZ, R_MOD) + intermediateValue := addmod(intermediateValue, sub(R_MOD, stateOpening2AtZ), R_MOD) + intermediateValue := mulmod(intermediateValue, mload(STATE_POWER_OF_ALPHA_2_SLOT), R_MOD) + accumulator := addmod(accumulator, intermediateValue, R_MOD) + // += alpha^3 * (c(z) * a(z) - d(z)) + intermediateValue := mulmod(stateOpening2AtZ, stateOpening0AtZ, R_MOD) + intermediateValue := addmod(intermediateValue, sub(R_MOD, stateOpening3AtZ), R_MOD) + intermediateValue := mulmod(intermediateValue, mload(STATE_POWER_OF_ALPHA_3_SLOT), R_MOD) + accumulator := addmod(accumulator, intermediateValue, R_MOD) + + // *= v * [custom_gate_selector] + accumulator := mulmod(accumulator, mload(STATE_V_SLOT), R_MOD) + pointMulAndAddIntoDest(VK_GATE_SELECTORS_1_X_SLOT, accumulator, dest) + } + + /// @notice Compute copy-permutation contribution to linearisation polynomial commitment multiplied by v + function addAssignPermutationLinearisationContributionWithV( + dest, + stateOpening0AtZ, + stateOpening1AtZ, + stateOpening2AtZ, + stateOpening3AtZ + ) { + // alpha^4 + let factor := mload(STATE_POWER_OF_ALPHA_4_SLOT) + // Calculate the factor + { + // *= (a(z) + beta * z + gamma) + let zMulBeta := mulmod(mload(STATE_Z_SLOT), mload(STATE_BETA_SLOT), R_MOD) + let gamma := mload(STATE_GAMMA_SLOT) + + let intermediateValue := addmod(addmod(zMulBeta, gamma, R_MOD), stateOpening0AtZ, R_MOD) + factor := mulmod(factor, intermediateValue, R_MOD) + + // (b(z) + beta * z * k0 + gamma) + intermediateValue := addmod( + addmod(mulmod(zMulBeta, NON_RESIDUES_0, R_MOD), gamma, R_MOD), + stateOpening1AtZ, + R_MOD + ) + factor := mulmod(factor, intermediateValue, R_MOD) + + // (c(z) + beta * z * k1 + gamma) + intermediateValue := addmod( + addmod(mulmod(zMulBeta, NON_RESIDUES_1, R_MOD), gamma, R_MOD), + stateOpening2AtZ, + R_MOD + ) + factor := mulmod(factor, intermediateValue, R_MOD) + + // (d(z) + beta * z * k2 + gamma) + intermediateValue := addmod( + addmod(mulmod(zMulBeta, NON_RESIDUES_2, R_MOD), gamma, R_MOD), + stateOpening3AtZ, + R_MOD + ) + factor := mulmod(factor, intermediateValue, R_MOD) + } + + // += alpha^5 * L_0(z) + let l0AtZ := mload(STATE_L_0_AT_Z_SLOT) + factor := addmod(factor, mulmod(l0AtZ, mload(STATE_POWER_OF_ALPHA_5_SLOT), R_MOD), R_MOD) + + // Here we can optimize one scalar multiplication by aggregating coefficients near [z_perm] during + // computing [F] + // We will sum them and add and make one scalar multiplication: (coeff1 + coeff2) * [z_perm] + factor := mulmod(factor, mload(STATE_V_SLOT), R_MOD) + mstore(COPY_PERMUTATION_FIRST_AGGREGATED_COMMITMENT_COEFF, factor) + + // alpha^4 * beta * z_perm(z*omega) + factor := mulmod(mload(STATE_POWER_OF_ALPHA_4_SLOT), mload(STATE_BETA_SLOT), R_MOD) + factor := mulmod(factor, mload(PROOF_COPY_PERMUTATION_GRAND_PRODUCT_OPENING_AT_Z_OMEGA_SLOT), R_MOD) + { + // *= (a(z) + beta * sigma_0(z) + gamma) + let beta := mload(STATE_BETA_SLOT) + let gamma := mload(STATE_GAMMA_SLOT) + + let intermediateValue := addmod( + addmod( + mulmod(mload(PROOF_COPY_PERMUTATION_POLYS_0_OPENING_AT_Z_SLOT), beta, R_MOD), + gamma, + R_MOD + ), + stateOpening0AtZ, + R_MOD + ) + factor := mulmod(factor, intermediateValue, R_MOD) + + // *= (b(z) + beta * sigma_1(z) + gamma) + intermediateValue := addmod( + addmod( + mulmod(mload(PROOF_COPY_PERMUTATION_POLYS_1_OPENING_AT_Z_SLOT), beta, R_MOD), + gamma, + R_MOD + ), + stateOpening1AtZ, + R_MOD + ) + factor := mulmod(factor, intermediateValue, R_MOD) + + // *= (c(z) + beta * sigma_2(z) + gamma) + intermediateValue := addmod( + addmod( + mulmod(mload(PROOF_COPY_PERMUTATION_POLYS_2_OPENING_AT_Z_SLOT), beta, R_MOD), + gamma, + R_MOD + ), + stateOpening2AtZ, + R_MOD + ) + factor := mulmod(factor, intermediateValue, R_MOD) + } + + // *= v * [sigma_3] + factor := mulmod(factor, mload(STATE_V_SLOT), R_MOD) + pointMulIntoDest(VK_PERMUTATION_3_X_SLOT, factor, QUERIES_BUFFER_POINT_SLOT) + + pointSubAssign(dest, QUERIES_BUFFER_POINT_SLOT) + } + + /// @notice Compute lookup contribution to linearisation polynomial commitment multiplied by v + function addAssignLookupLinearisationContributionWithV( + dest, + stateOpening0AtZ, + stateOpening1AtZ, + stateOpening2AtZ + ) { + // alpha^6 * v * z_lookup(z*omega) * (z - omega^{n-1}) * [s] + let factor := mload(PROOF_LOOKUP_GRAND_PRODUCT_OPENING_AT_Z_OMEGA_SLOT) + factor := mulmod(factor, mload(STATE_POWER_OF_ALPHA_6_SLOT), R_MOD) + factor := mulmod(factor, mload(STATE_Z_MINUS_LAST_OMEGA_SLOT), R_MOD) + factor := mulmod(factor, mload(STATE_V_SLOT), R_MOD) + + // Here we can optimize one scalar multiplication by aggregating coefficients near [s] during + // computing [F] + // We will sum them and add and make one scalar multiplication: (coeff1 + coeff2) * [s] + mstore(LOOKUP_S_FIRST_AGGREGATED_COMMITMENT_COEFF, factor) + + // gamma(1 + beta) + t(x) + beta * t(x*omega) + factor := mload(PROOF_LOOKUP_T_POLY_OPENING_AT_Z_OMEGA_SLOT) + factor := mulmod(factor, mload(STATE_BETA_LOOKUP_SLOT), R_MOD) + factor := addmod(factor, mload(PROOF_LOOKUP_T_POLY_OPENING_AT_Z_SLOT), R_MOD) + factor := addmod(factor, mload(STATE_BETA_GAMMA_PLUS_GAMMA_SLOT), R_MOD) + + // *= (gamma + f(z)) + // We should use fact that f(x) = + // lookup_selector(x) * (a(x) + eta * b(x) + eta^2 * c(x) + eta^3 * table_type(x)) + // to restore f(z) + let fReconstructed + { + fReconstructed := stateOpening0AtZ + let eta := mload(STATE_ETA_SLOT) + let currentEta := eta + + fReconstructed := addmod(fReconstructed, mulmod(currentEta, stateOpening1AtZ, R_MOD), R_MOD) + currentEta := mulmod(currentEta, eta, R_MOD) + fReconstructed := addmod(fReconstructed, mulmod(currentEta, stateOpening2AtZ, R_MOD), R_MOD) + currentEta := mulmod(currentEta, eta, R_MOD) + + // add type of table + fReconstructed := addmod( + fReconstructed, + mulmod(mload(PROOF_LOOKUP_TABLE_TYPE_POLY_OPENING_AT_Z_SLOT), currentEta, R_MOD), + R_MOD + ) + fReconstructed := mulmod(fReconstructed, mload(PROOF_LOOKUP_SELECTOR_POLY_OPENING_AT_Z_SLOT), R_MOD) + fReconstructed := addmod(fReconstructed, mload(STATE_GAMMA_LOOKUP_SLOT), R_MOD) + } + // *= -alpha^6 * (beta + 1) * (z - omega^{n-1}) + factor := mulmod(factor, fReconstructed, R_MOD) + factor := mulmod(factor, mload(STATE_BETA_PLUS_ONE_SLOT), R_MOD) + factor := sub(R_MOD, factor) + factor := mulmod(factor, mload(STATE_POWER_OF_ALPHA_6_SLOT), R_MOD) + + factor := mulmod(factor, mload(STATE_Z_MINUS_LAST_OMEGA_SLOT), R_MOD) + + // += alpha^7 * L_0(z) + factor := addmod( + factor, + mulmod(mload(STATE_L_0_AT_Z_SLOT), mload(STATE_POWER_OF_ALPHA_7_SLOT), R_MOD), + R_MOD + ) + + // += alpha^8 * L_{n-1}(z) + factor := addmod( + factor, + mulmod(mload(STATE_L_N_MINUS_ONE_AT_Z_SLOT), mload(STATE_POWER_OF_ALPHA_8_SLOT), R_MOD), + R_MOD + ) + + // Here we can optimize one scalar multiplication by aggregating coefficients near [z_lookup] during + // computing [F] + // We will sum them and add and make one scalar multiplication: (coeff1 + coeff2) * [z_lookup] + factor := mulmod(factor, mload(STATE_V_SLOT), R_MOD) + mstore(LOOKUP_GRAND_PRODUCT_FIRST_AGGREGATED_COMMITMENT_COEFF, factor) + } + + /*////////////////////////////////////////////////////////////// + 4. Prepare queries + //////////////////////////////////////////////////////////////*/ + + /// @dev Here we compute the first and second parts of batched polynomial commitment + /// We use the formula: + /// [D0] = [t_0] + z^n * [t_1] + z^{2n} * [t_2] + z^{3n} * [t_3] + /// and + /// [D1] = main_gate_selector(z) * ( \ + /// a(z) * [q_a] + b(z) * [q_b] + c(z) * [q_c] + d(z) * [q_d] + | - main gate contribution + /// a(z) * b(z) * [q_ab] + a(z) * c(z) * [q_ac] + | + /// [q_const] + d(z*omega) * [q_{d_next}]) / + /// + /// + alpha * [custom_gate_selector] * ( \ + /// (a(z)^2 - b(z)) + | - custom gate contribution + /// (b(z)^2 - c(z)) * alpha + | + /// (a(z)*c(z) - d(z)) * alpha^2 ) / + /// + /// + alpha^4 * [z_perm] * \ + /// (a(z) + beta * z + gamma) * | + /// (b(z) + beta * z * k0 + gamma) * | + /// (c(z) + beta * z * k1 + gamma) * | + /// (d(z) + beta * z * k2 + gamma) | - permutation contribution + /// - alpha^4 * z_perm(z*omega) * beta * [sigma_3] * | + /// (a(z) + beta * sigma_0(z) + gamma) * | + /// (b(z) + beta * sigma_1(z) + gamma) * | + /// (c(z) + beta * sigma_2(z) + gamma) * | + /// + alpha^5 * L_0(z) * [z_perm] / + /// + /// - alpha^6 * (1 + beta') * (gamma' + f(z)) * (z - omega^{n-1}) * \ + /// (gamma'(1 + beta') + t(z) + beta' * t(z*omega)) * [z_lookup] | + /// + alpha^6 * z_lookup(z*omega) * (z - omega^{n-1}) * [s] | - lookup contribution + /// + alpha^7 * L_0(z) * [z_lookup] | + /// + alpha^8 * L_{n-1}(z) * [z_lookup] / + function prepareQueries() { + // Calculate [D0] + { + let zInDomainSize := mload(STATE_Z_IN_DOMAIN_SIZE) + let currentZ := zInDomainSize + + mstore(QUERIES_AT_Z_0_X_SLOT, mload(PROOF_QUOTIENT_POLY_PARTS_0_X_SLOT)) + mstore(QUERIES_AT_Z_0_Y_SLOT, mload(PROOF_QUOTIENT_POLY_PARTS_0_Y_SLOT)) + + pointMulAndAddIntoDest(PROOF_QUOTIENT_POLY_PARTS_1_X_SLOT, currentZ, QUERIES_AT_Z_0_X_SLOT) + currentZ := mulmod(currentZ, zInDomainSize, R_MOD) + + pointMulAndAddIntoDest(PROOF_QUOTIENT_POLY_PARTS_2_X_SLOT, currentZ, QUERIES_AT_Z_0_X_SLOT) + currentZ := mulmod(currentZ, zInDomainSize, R_MOD) + + pointMulAndAddIntoDest(PROOF_QUOTIENT_POLY_PARTS_3_X_SLOT, currentZ, QUERIES_AT_Z_0_X_SLOT) + } + + // Calculate v * [D1] + // We are going to multiply all the points in the sum by v to save + // one scalar multiplication during [F] computation + { + let stateOpening0AtZ := mload(PROOF_STATE_POLYS_0_OPENING_AT_Z_SLOT) + let stateOpening1AtZ := mload(PROOF_STATE_POLYS_1_OPENING_AT_Z_SLOT) + let stateOpening2AtZ := mload(PROOF_STATE_POLYS_2_OPENING_AT_Z_SLOT) + let stateOpening3AtZ := mload(PROOF_STATE_POLYS_3_OPENING_AT_Z_SLOT) + + mainGateLinearisationContributionWithV( + QUERIES_AT_Z_1_X_SLOT, + stateOpening0AtZ, + stateOpening1AtZ, + stateOpening2AtZ, + stateOpening3AtZ + ) + + addAssignRescueCustomGateLinearisationContributionWithV( + QUERIES_AT_Z_1_X_SLOT, + stateOpening0AtZ, + stateOpening1AtZ, + stateOpening2AtZ, + stateOpening3AtZ + ) + + addAssignPermutationLinearisationContributionWithV( + QUERIES_AT_Z_1_X_SLOT, + stateOpening0AtZ, + stateOpening1AtZ, + stateOpening2AtZ, + stateOpening3AtZ + ) + + addAssignLookupLinearisationContributionWithV( + QUERIES_AT_Z_1_X_SLOT, + stateOpening0AtZ, + stateOpening1AtZ, + stateOpening2AtZ + ) + } + + // Also we should restore [t] for future computations + // [t] = [col_0] + eta*[col_1] + eta^2*[col_2] + eta^3*[col_3] + { + mstore(QUERIES_T_POLY_AGGREGATED_X_SLOT, mload(VK_LOOKUP_TABLE_0_X_SLOT)) + mstore(QUERIES_T_POLY_AGGREGATED_Y_SLOT, mload(VK_LOOKUP_TABLE_0_Y_SLOT)) + + let eta := mload(STATE_ETA_SLOT) + let currentEta := eta + + pointMulAndAddIntoDest(VK_LOOKUP_TABLE_1_X_SLOT, currentEta, QUERIES_T_POLY_AGGREGATED_X_SLOT) + currentEta := mulmod(currentEta, eta, R_MOD) + + pointMulAndAddIntoDest(VK_LOOKUP_TABLE_2_X_SLOT, currentEta, QUERIES_T_POLY_AGGREGATED_X_SLOT) + currentEta := mulmod(currentEta, eta, R_MOD) + + pointMulAndAddIntoDest(VK_LOOKUP_TABLE_3_X_SLOT, currentEta, QUERIES_T_POLY_AGGREGATED_X_SLOT) + } + } + + /*////////////////////////////////////////////////////////////// + 5. Prepare aggregated commitment + //////////////////////////////////////////////////////////////*/ + + /// @dev Here we compute aggregated commitment for the final pairing + /// We use the formula: + /// [E] = ( t(z) + v * r(z) + /// + v^2*a(z) + v^3*b(z) + v^4*c(z) + v^5*d(z) + /// + v^6*main_gate_selector(z) + /// + v^7*sigma_0(z) + v^8*sigma_1(z) + v^9*sigma_2(z) + /// + v^10*t(z) + v^11*lookup_selector(z) + v^12*table_type(z) + /// + u * (v^13*z_perm(z*omega) + v^14*d(z*omega) + /// + v^15*s(z*omega) + v^16*z_lookup(z*omega) + v^17*t(z*omega) + /// ) + /// ) * [1] + /// and + /// [F] = [D0] + v * [D1] + /// + v^2*[a] + v^3*[b] + v^4*[c] + v^5*[d] + /// + v^6*[main_gate_selector] + /// + v^7*[sigma_0] + v^8*[sigma_1] + v^9*[sigma_2] + /// + v^10*[t] + v^11*[lookup_selector] + v^12*[table_type] + /// + u * ( v^13*[z_perm] + v^14*[d] + /// + v^15*[s] + v^16*[z_lookup] + v^17*[t] + /// ) + function prepareAggregatedCommitment() { + // Here we compute parts of [E] and [F] without u multiplier + let aggregationChallenge := 1 + let firstDCoeff + let firstTCoeff + + mstore(AGGREGATED_AT_Z_X_SLOT, mload(QUERIES_AT_Z_0_X_SLOT)) + mstore(AGGREGATED_AT_Z_Y_SLOT, mload(QUERIES_AT_Z_0_Y_SLOT)) + let aggregatedOpeningAtZ := mload(PROOF_QUOTIENT_POLY_OPENING_AT_Z_SLOT) + { + function updateAggregationChallenge( + queriesCommitmentPoint, + valueAtZ, + curAggregationChallenge, + curAggregatedOpeningAtZ + ) -> newAggregationChallenge, newAggregatedOpeningAtZ { + newAggregationChallenge := mulmod(curAggregationChallenge, mload(STATE_V_SLOT), R_MOD) + pointMulAndAddIntoDest(queriesCommitmentPoint, newAggregationChallenge, AGGREGATED_AT_Z_X_SLOT) + newAggregatedOpeningAtZ := addmod( + curAggregatedOpeningAtZ, + mulmod(newAggregationChallenge, mload(valueAtZ), R_MOD), + R_MOD + ) + } + + // We don't need to multiply by v, because we have already computed v * [D1] + pointAddIntoDest(AGGREGATED_AT_Z_X_SLOT, QUERIES_AT_Z_1_X_SLOT, AGGREGATED_AT_Z_X_SLOT) + aggregationChallenge := mulmod(aggregationChallenge, mload(STATE_V_SLOT), R_MOD) + aggregatedOpeningAtZ := addmod( + aggregatedOpeningAtZ, + mulmod(aggregationChallenge, mload(PROOF_LINEARISATION_POLY_OPENING_AT_Z_SLOT), R_MOD), + R_MOD + ) + + aggregationChallenge, aggregatedOpeningAtZ := updateAggregationChallenge( + PROOF_STATE_POLYS_0_X_SLOT, + PROOF_STATE_POLYS_0_OPENING_AT_Z_SLOT, + aggregationChallenge, + aggregatedOpeningAtZ + ) + aggregationChallenge, aggregatedOpeningAtZ := updateAggregationChallenge( + PROOF_STATE_POLYS_1_X_SLOT, + PROOF_STATE_POLYS_1_OPENING_AT_Z_SLOT, + aggregationChallenge, + aggregatedOpeningAtZ + ) + aggregationChallenge, aggregatedOpeningAtZ := updateAggregationChallenge( + PROOF_STATE_POLYS_2_X_SLOT, + PROOF_STATE_POLYS_2_OPENING_AT_Z_SLOT, + aggregationChallenge, + aggregatedOpeningAtZ + ) + + // Here we can optimize one scalar multiplication by aggregating coefficients near [d] + // We will sum them and add and make one scalar multiplication: (coeff1 + coeff2) * [d] + aggregationChallenge := mulmod(aggregationChallenge, mload(STATE_V_SLOT), R_MOD) + firstDCoeff := aggregationChallenge + aggregatedOpeningAtZ := addmod( + aggregatedOpeningAtZ, + mulmod(aggregationChallenge, mload(PROOF_STATE_POLYS_3_OPENING_AT_Z_SLOT), R_MOD), + R_MOD + ) + + aggregationChallenge, aggregatedOpeningAtZ := updateAggregationChallenge( + VK_GATE_SELECTORS_0_X_SLOT, + PROOF_GATE_SELECTORS_0_OPENING_AT_Z_SLOT, + aggregationChallenge, + aggregatedOpeningAtZ + ) + aggregationChallenge, aggregatedOpeningAtZ := updateAggregationChallenge( + VK_PERMUTATION_0_X_SLOT, + PROOF_COPY_PERMUTATION_POLYS_0_OPENING_AT_Z_SLOT, + aggregationChallenge, + aggregatedOpeningAtZ + ) + aggregationChallenge, aggregatedOpeningAtZ := updateAggregationChallenge( + VK_PERMUTATION_1_X_SLOT, + PROOF_COPY_PERMUTATION_POLYS_1_OPENING_AT_Z_SLOT, + aggregationChallenge, + aggregatedOpeningAtZ + ) + aggregationChallenge, aggregatedOpeningAtZ := updateAggregationChallenge( + VK_PERMUTATION_2_X_SLOT, + PROOF_COPY_PERMUTATION_POLYS_2_OPENING_AT_Z_SLOT, + aggregationChallenge, + aggregatedOpeningAtZ + ) + + // Here we can optimize one scalar multiplication by aggregating coefficients near [t] + // We will sum them and add and make one scalar multiplication: (coeff1 + coeff2) * [t] + aggregationChallenge := mulmod(aggregationChallenge, mload(STATE_V_SLOT), R_MOD) + firstTCoeff := aggregationChallenge + aggregatedOpeningAtZ := addmod( + aggregatedOpeningAtZ, + mulmod(aggregationChallenge, mload(PROOF_LOOKUP_T_POLY_OPENING_AT_Z_SLOT), R_MOD), + R_MOD + ) + + aggregationChallenge, aggregatedOpeningAtZ := updateAggregationChallenge( + VK_LOOKUP_SELECTOR_X_SLOT, + PROOF_LOOKUP_SELECTOR_POLY_OPENING_AT_Z_SLOT, + aggregationChallenge, + aggregatedOpeningAtZ + ) + aggregationChallenge, aggregatedOpeningAtZ := updateAggregationChallenge( + VK_LOOKUP_TABLE_TYPE_X_SLOT, + PROOF_LOOKUP_TABLE_TYPE_POLY_OPENING_AT_Z_SLOT, + aggregationChallenge, + aggregatedOpeningAtZ + ) + } + mstore(AGGREGATED_OPENING_AT_Z_SLOT, aggregatedOpeningAtZ) + + // Here we compute parts of [E] and [F] with u multiplier + aggregationChallenge := mulmod(aggregationChallenge, mload(STATE_V_SLOT), R_MOD) + + let copyPermutationCoeff := addmod( + mload(COPY_PERMUTATION_FIRST_AGGREGATED_COMMITMENT_COEFF), + mulmod(aggregationChallenge, mload(STATE_U_SLOT), R_MOD), + R_MOD + ) + + pointMulIntoDest( + PROOF_COPY_PERMUTATION_GRAND_PRODUCT_X_SLOT, + copyPermutationCoeff, + AGGREGATED_AT_Z_OMEGA_X_SLOT + ) + let aggregatedOpeningAtZOmega := mulmod( + mload(PROOF_COPY_PERMUTATION_GRAND_PRODUCT_OPENING_AT_Z_OMEGA_SLOT), + aggregationChallenge, + R_MOD + ) + + { + function updateAggregationChallenge( + queriesCommitmentPoint, + valueAtZ_Omega, + previousCoeff, + curAggregationChallenge, + curAggregatedOpeningAtZ_Omega + ) -> newAggregationChallenge, newAggregatedOpeningAtZ_Omega { + newAggregationChallenge := mulmod(curAggregationChallenge, mload(STATE_V_SLOT), R_MOD) + let finalCoeff := addmod( + previousCoeff, + mulmod(newAggregationChallenge, mload(STATE_U_SLOT), R_MOD), + R_MOD + ) + pointMulAndAddIntoDest(queriesCommitmentPoint, finalCoeff, AGGREGATED_AT_Z_OMEGA_X_SLOT) + newAggregatedOpeningAtZ_Omega := addmod( + curAggregatedOpeningAtZ_Omega, + mulmod(newAggregationChallenge, mload(valueAtZ_Omega), R_MOD), + R_MOD + ) + } + + aggregationChallenge, aggregatedOpeningAtZOmega := updateAggregationChallenge( + PROOF_STATE_POLYS_3_X_SLOT, + PROOF_STATE_POLYS_3_OPENING_AT_Z_OMEGA_SLOT, + firstDCoeff, + aggregationChallenge, + aggregatedOpeningAtZOmega + ) + aggregationChallenge, aggregatedOpeningAtZOmega := updateAggregationChallenge( + PROOF_LOOKUP_S_POLY_X_SLOT, + PROOF_LOOKUP_S_POLY_OPENING_AT_Z_OMEGA_SLOT, + mload(LOOKUP_S_FIRST_AGGREGATED_COMMITMENT_COEFF), + aggregationChallenge, + aggregatedOpeningAtZOmega + ) + aggregationChallenge, aggregatedOpeningAtZOmega := updateAggregationChallenge( + PROOF_LOOKUP_GRAND_PRODUCT_X_SLOT, + PROOF_LOOKUP_GRAND_PRODUCT_OPENING_AT_Z_OMEGA_SLOT, + mload(LOOKUP_GRAND_PRODUCT_FIRST_AGGREGATED_COMMITMENT_COEFF), + aggregationChallenge, + aggregatedOpeningAtZOmega + ) + aggregationChallenge, aggregatedOpeningAtZOmega := updateAggregationChallenge( + QUERIES_T_POLY_AGGREGATED_X_SLOT, + PROOF_LOOKUP_T_POLY_OPENING_AT_Z_OMEGA_SLOT, + firstTCoeff, + aggregationChallenge, + aggregatedOpeningAtZOmega + ) + } + mstore(AGGREGATED_OPENING_AT_Z_OMEGA_SLOT, aggregatedOpeningAtZOmega) + + // Now we can merge both parts and get [E] and [F] + let u := mload(STATE_U_SLOT) + + // [F] + pointAddIntoDest( + AGGREGATED_AT_Z_X_SLOT, + AGGREGATED_AT_Z_OMEGA_X_SLOT, + PAIRING_PAIR_WITH_GENERATOR_X_SLOT + ) + + // [E] = (aggregatedOpeningAtZ + u * aggregatedOpeningAtZOmega) * [1] + let aggregatedValue := addmod( + mulmod(mload(AGGREGATED_OPENING_AT_Z_OMEGA_SLOT), u, R_MOD), + mload(AGGREGATED_OPENING_AT_Z_SLOT), + R_MOD + ) + + mstore(PAIRING_BUFFER_POINT_X_SLOT, 1) + mstore(PAIRING_BUFFER_POINT_Y_SLOT, 2) + pointMulIntoDest(PAIRING_BUFFER_POINT_X_SLOT, aggregatedValue, PAIRING_BUFFER_POINT_X_SLOT) + } + + /*////////////////////////////////////////////////////////////// + 5. Pairing + //////////////////////////////////////////////////////////////*/ + + /// @notice Checks the final pairing + /// @dev We should check the equation: + /// e([W] + u * [W'], [x]_2) = e(z * [W] + u * z * omega * [W'] + [F] - [E], [1]_2), + /// where [F] and [E] were computed previously + /// + /// Also we need to check that e([P1], [x]_2) = e([P2], [1]_2) + /// if we have the recursive part of the proof + /// where [P1] and [P2] are parts of the recursive proof + /// + /// We can aggregate both pairings into one for gas optimization: + /// e([W] + u * [W'] + u^2 * [P1], [x]_2) = + /// e(z * [W] + u * z * omega * [W'] + [F] - [E] + u^2 * [P2], [1]_2) + /// + /// u is a valid challenge for such aggregation, + /// because [P1] and [P2] are used in PI + function finalPairing() { + let u := mload(STATE_U_SLOT) + let z := mload(STATE_Z_SLOT) + let zOmega := mulmod(mload(STATE_Z_SLOT), OMEGA, R_MOD) + + // [F] - [E] + pointSubAssign(PAIRING_PAIR_WITH_GENERATOR_X_SLOT, PAIRING_BUFFER_POINT_X_SLOT) + + // +z * [W] + u * z * omega * [W'] + pointMulAndAddIntoDest(PROOF_OPENING_PROOF_AT_Z_X_SLOT, z, PAIRING_PAIR_WITH_GENERATOR_X_SLOT) + pointMulAndAddIntoDest( + PROOF_OPENING_PROOF_AT_Z_OMEGA_X_SLOT, + mulmod(zOmega, u, R_MOD), + PAIRING_PAIR_WITH_GENERATOR_X_SLOT + ) + + // [W] + u * [W'] + mstore(PAIRING_PAIR_WITH_X_X_SLOT, mload(PROOF_OPENING_PROOF_AT_Z_X_SLOT)) + mstore(PAIRING_PAIR_WITH_X_Y_SLOT, mload(PROOF_OPENING_PROOF_AT_Z_Y_SLOT)) + pointMulAndAddIntoDest(PROOF_OPENING_PROOF_AT_Z_OMEGA_X_SLOT, u, PAIRING_PAIR_WITH_X_X_SLOT) + pointNegate(PAIRING_PAIR_WITH_X_X_SLOT) + + // Add recursive proof part if needed + if mload(VK_RECURSIVE_FLAG_SLOT) { + let uu := mulmod(u, u, R_MOD) + pointMulAndAddIntoDest(PROOF_RECURSIVE_PART_P1_X_SLOT, uu, PAIRING_PAIR_WITH_GENERATOR_X_SLOT) + pointMulAndAddIntoDest(PROOF_RECURSIVE_PART_P2_X_SLOT, uu, PAIRING_PAIR_WITH_X_X_SLOT) + } + + // Calculate pairing + { + mstore(0x000, mload(PAIRING_PAIR_WITH_GENERATOR_X_SLOT)) + mstore(0x020, mload(PAIRING_PAIR_WITH_GENERATOR_Y_SLOT)) + + mstore(0x040, G2_ELEMENTS_0_X1) + mstore(0x060, G2_ELEMENTS_0_X2) + mstore(0x080, G2_ELEMENTS_0_Y1) + mstore(0x0a0, G2_ELEMENTS_0_Y2) + + mstore(0x0c0, mload(PAIRING_PAIR_WITH_X_X_SLOT)) + mstore(0x0e0, mload(PAIRING_PAIR_WITH_X_Y_SLOT)) + + mstore(0x100, G2_ELEMENTS_1_X1) + mstore(0x120, G2_ELEMENTS_1_X2) + mstore(0x140, G2_ELEMENTS_1_Y1) + mstore(0x160, G2_ELEMENTS_1_Y2) + + let success := staticcall(gas(), 8, 0, 0x180, 0x00, 0x20) + if iszero(success) { + revertWithMessage(32, "finalPairing: precompile failure") + } + if iszero(mload(0)) { + revertWithMessage(29, "finalPairing: pairing failure") + } + } + } + + /*////////////////////////////////////////////////////////////// + Verification + //////////////////////////////////////////////////////////////*/ + + // Step 1: Load the proof and check the correctness of its parts + loadProof() + + // Step 2: Recompute all the challenges with the transcript + initializeTranscript() + + // Step 3: Check the quotient equality + verifyQuotientEvaluation() + + // Step 4: Compute queries [D0] and v * [D1] + prepareQueries() + + // Step 5: Compute [E] and [F] + prepareAggregatedCommitment() + + // Step 6: Check the final pairing with aggregated recursive proof + finalPairing() + + mstore(0, true) + return(0, 32) + } + } +} diff --git a/contracts/l1-contracts/state-transition/chain-deps/DiamondInit.sol b/contracts/l1-contracts/state-transition/chain-deps/DiamondInit.sol new file mode 100644 index 0000000..663cf26 --- /dev/null +++ b/contracts/l1-contracts/state-transition/chain-deps/DiamondInit.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {Diamond} from "../libraries/Diamond.sol"; +import {ZkSyncHyperchainBase} from "./facets/ZkSyncHyperchainBase.sol"; +import {L2_TO_L1_LOG_SERIALIZE_SIZE, MAX_GAS_PER_TRANSACTION} from "../../common/Config.sol"; +import {InitializeData, IDiamondInit} from "../chain-interfaces/IDiamondInit.sol"; +import {ZeroAddress, TooMuchGas} from "../../common/L1ContractErrors.sol"; + +/// @author Matter Labs +/// @dev The contract is used only once to initialize the diamond proxy. +/// @dev The deployment process takes care of this contract's initialization. +contract DiamondInit is ZkSyncHyperchainBase, IDiamondInit { + /// @dev Initialize the implementation to prevent any possibility of a Parity hack. + constructor() reentrancyGuardInitializer {} + + /// @notice hyperchain diamond contract initialization + /// @return Magic 32 bytes, which indicates that the contract logic is expected to be used as a diamond proxy + /// initializer + function initialize(InitializeData calldata _initializeData) external reentrancyGuardInitializer returns (bytes32) { + if (address(_initializeData.verifier) == address(0)) { + revert ZeroAddress(); + } + if (_initializeData.admin == address(0)) { + revert ZeroAddress(); + } + if (_initializeData.validatorTimelock == address(0)) { + revert ZeroAddress(); + } + if (_initializeData.priorityTxMaxGasLimit > MAX_GAS_PER_TRANSACTION) { + revert TooMuchGas(); + } + if (_initializeData.bridgehub == address(0)) { + revert ZeroAddress(); + } + if (_initializeData.stateTransitionManager == address(0)) { + revert ZeroAddress(); + } + if (_initializeData.baseToken == address(0)) { + revert ZeroAddress(); + } + if (_initializeData.baseTokenBridge == address(0)) { + revert ZeroAddress(); + } + if (_initializeData.blobVersionedHashRetriever == address(0)) { + revert ZeroAddress(); + } + + s.chainId = _initializeData.chainId; + s.bridgehub = _initializeData.bridgehub; + s.stateTransitionManager = _initializeData.stateTransitionManager; + s.baseToken = _initializeData.baseToken; + s.baseTokenBridge = _initializeData.baseTokenBridge; + s.protocolVersion = _initializeData.protocolVersion; + + s.verifier = _initializeData.verifier; + s.admin = _initializeData.admin; + s.validators[_initializeData.validatorTimelock] = true; + + s.storedBatchHashes[0] = _initializeData.storedBatchZero; + s.__DEPRECATED_verifierParams = _initializeData.verifierParams; + s.l2BootloaderBytecodeHash = _initializeData.l2BootloaderBytecodeHash; + s.l2DefaultAccountBytecodeHash = _initializeData.l2DefaultAccountBytecodeHash; + s.priorityTxMaxGasLimit = _initializeData.priorityTxMaxGasLimit; + s.feeParams = _initializeData.feeParams; + s.blobVersionedHashRetriever = _initializeData.blobVersionedHashRetriever; + + // While this does not provide a protection in the production, it is needed for local testing + // Length of the L2Log encoding should not be equal to the length of other L2Logs' tree nodes preimages + assert(L2_TO_L1_LOG_SERIALIZE_SIZE != 2 * 32); + + return Diamond.DIAMOND_INIT_SUCCESS_RETURN_VALUE; + } +} diff --git a/contracts/l1-contracts/state-transition/chain-deps/DiamondProxy.sol b/contracts/l1-contracts/state-transition/chain-deps/DiamondProxy.sol new file mode 100644 index 0000000..db29da1 --- /dev/null +++ b/contracts/l1-contracts/state-transition/chain-deps/DiamondProxy.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +// solhint-disable gas-custom-errors + +import {Diamond} from "../libraries/Diamond.sol"; + +/// @title Diamond Proxy Contract (EIP-2535) +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +contract DiamondProxy { + constructor(uint256 _chainId, Diamond.DiamondCutData memory _diamondCut) { + // Check that the contract is deployed on the expected chain. + // Thus, the contract deployed by the same Create2 factory on the different chain will have different addresses! + require(_chainId == block.chainid, "pr"); + Diamond.diamondCut(_diamondCut); + } + + /// @dev 1. Find the facet for the function that is called. + /// @dev 2. Delegate the execution to the found facet via `delegatecall`. + fallback() external payable { + Diamond.DiamondStorage storage diamondStorage = Diamond.getDiamondStorage(); + // Check whether the data contains a "full" selector or it is empty. + // Required because Diamond proxy finds a facet by function signature, + // which is not defined for data length in range [1, 3]. + require(msg.data.length >= 4 || msg.data.length == 0, "Ut"); + // Get facet from function selector + Diamond.SelectorToFacet memory facet = diamondStorage.selectorToFacet[msg.sig]; + address facetAddress = facet.facetAddress; + + require(facetAddress != address(0), "F"); // Proxy has no facet for this selector + require(!diamondStorage.isFrozen || !facet.isFreezable, "q1"); // Facet is frozen + + assembly { + // The pointer to the free memory slot + let ptr := mload(0x40) + // Copy function signature and arguments from calldata at zero position into memory at pointer position + calldatacopy(ptr, 0, calldatasize()) + // Delegatecall method of the implementation contract returns 0 on error + let result := delegatecall(gas(), facetAddress, ptr, calldatasize(), 0, 0) + // Get the size of the last return data + let size := returndatasize() + // Copy the size length of bytes from return data at zero position to pointer position + returndatacopy(ptr, 0, size) + // Depending on the result value + switch result + case 0 { + // End execution and revert state changes + revert(ptr, size) + } + default { + // Return data with length of size at pointers position + return(ptr, size) + } + } + } +} diff --git a/contracts/l1-contracts/state-transition/chain-deps/ZkSyncHyperchainStorage.sol b/contracts/l1-contracts/state-transition/chain-deps/ZkSyncHyperchainStorage.sol new file mode 100644 index 0000000..a06921f --- /dev/null +++ b/contracts/l1-contracts/state-transition/chain-deps/ZkSyncHyperchainStorage.sol @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {IVerifier, VerifierParams} from "../chain-interfaces/IVerifier.sol"; +import {PriorityQueue} from "../../state-transition/libraries/PriorityQueue.sol"; + +/// @notice Indicates whether an upgrade is initiated and if yes what type +/// @param None Upgrade is NOT initiated +/// @param Transparent Fully transparent upgrade is initiated, upgrade data is publicly known +/// @param Shadow Shadow upgrade is initiated, upgrade data is hidden +enum UpgradeState { + None, + Transparent, + Shadow +} + +/// @dev Logically separated part of the storage structure, which is responsible for everything related to proxy +/// upgrades and diamond cuts +/// @param proposedUpgradeHash The hash of the current upgrade proposal, zero if there is no active proposal +/// @param state Indicates whether an upgrade is initiated and if yes what type +/// @param securityCouncil Address which has the permission to approve instant upgrades (expected to be a Gnosis +/// multisig) +/// @param approvedBySecurityCouncil Indicates whether the security council has approved the upgrade +/// @param proposedUpgradeTimestamp The timestamp when the upgrade was proposed, zero if there are no active proposals +/// @param currentProposalId The serial number of proposed upgrades, increments when proposing a new one +struct UpgradeStorage { + bytes32 proposedUpgradeHash; + UpgradeState state; + address securityCouncil; + bool approvedBySecurityCouncil; + uint40 proposedUpgradeTimestamp; + uint40 currentProposalId; +} + +/// @notice The struct that describes whether users will be charged for pubdata for L1->L2 transactions. +/// @param Rollup The users are charged for pubdata & it is priced based on the gas price on Ethereum. +/// @param Validium The pubdata is considered free with regard to the L1 gas price. +enum PubdataPricingMode { + Rollup, + Validium +} + +/// @notice The fee params for L1->L2 transactions for the network. +/// @param pubdataPricingMode How the users will charged for pubdata in L1->L2 transactions. +/// @param batchOverheadL1Gas The amount of L1 gas required to process the batch (except for the calldata). +/// @param maxPubdataPerBatch The maximal number of pubdata that can be emitted per batch. +/// @param priorityTxMaxPubdata The maximal amount of pubdata a priority transaction is allowed to publish. +/// It can be slightly less than maxPubdataPerBatch in order to have some margin for the bootloader execution. +/// @param minimalL2GasPrice The minimal L2 gas price to be used by L1->L2 transactions. It should represent +/// the price that a single unit of compute costs. +struct FeeParams { + PubdataPricingMode pubdataPricingMode; + uint32 batchOverheadL1Gas; + uint32 maxPubdataPerBatch; + uint32 maxL2GasPerBatch; + uint32 priorityTxMaxPubdata; + uint64 minimalL2GasPrice; +} + +/// @dev storing all storage variables for hyperchain diamond facets +/// NOTE: It is used in a proxy, so it is possible to add new variables to the end +/// but NOT to modify already existing variables or change their order. +/// NOTE: variables prefixed with '__DEPRECATED_' are deprecated and shouldn't be used. +/// Their presence is maintained for compatibility and to prevent storage collision. +// solhint-disable-next-line gas-struct-packing +struct ZkSyncHyperchainStorage { + /// @dev Storage of variables needed for deprecated diamond cut facet + uint256[7] __DEPRECATED_diamondCutStorage; + /// @notice Address which will exercise critical changes to the Diamond Proxy (upgrades, freezing & unfreezing). Replaced by STM + address __DEPRECATED_governor; + /// @notice Address that the governor proposed as one that will replace it + address __DEPRECATED_pendingGovernor; + /// @notice List of permitted validators + mapping(address validatorAddress => bool isValidator) validators; + /// @dev Verifier contract. Used to verify aggregated proof for batches + IVerifier verifier; + /// @notice Total number of executed batches i.e. batches[totalBatchesExecuted] points at the latest executed batch + /// (batch 0 is genesis) + uint256 totalBatchesExecuted; + /// @notice Total number of proved batches i.e. batches[totalBatchesProved] points at the latest proved batch + uint256 totalBatchesVerified; + /// @notice Total number of committed batches i.e. batches[totalBatchesCommitted] points at the latest committed + /// batch + uint256 totalBatchesCommitted; + /// @dev Stored hashed StoredBatch for batch number + mapping(uint256 batchNumber => bytes32 batchHash) storedBatchHashes; + /// @dev Stored root hashes of L2 -> L1 logs + mapping(uint256 batchNumber => bytes32 l2LogsRootHash) l2LogsRootHashes; + /// @dev Container that stores transactions requested from L1 + PriorityQueue.Queue priorityQueue; + /// @dev The smart contract that manages the list with permission to call contract functions + address __DEPRECATED_allowList; + VerifierParams __DEPRECATED_verifierParams; + /// @notice Bytecode hash of bootloader program. + /// @dev Used as an input to zkp-circuit. + bytes32 l2BootloaderBytecodeHash; + /// @notice Bytecode hash of default account (bytecode for EOA). + /// @dev Used as an input to zkp-circuit. + bytes32 l2DefaultAccountBytecodeHash; + /// @dev Indicates that the porter may be touched on L2 transactions. + /// @dev Used as an input to zkp-circuit. + bool zkPorterIsAvailable; + /// @dev The maximum number of the L2 gas that a user can request for L1 -> L2 transactions + /// @dev This is the maximum number of L2 gas that is available for the "body" of the transaction, i.e. + /// without overhead for proving the batch. + uint256 priorityTxMaxGasLimit; + /// @dev Storage of variables needed for upgrade facet + UpgradeStorage __DEPRECATED_upgrades; + /// @dev A mapping L2 batch number => message number => flag. + /// @dev The L2 -> L1 log is sent for every withdrawal, so this mapping is serving as + /// a flag to indicate that the message was already processed. + /// @dev Used to indicate that eth withdrawal was already processed + mapping(uint256 l2BatchNumber => mapping(uint256 l2ToL1MessageNumber => bool isFinalized)) isEthWithdrawalFinalized; + /// @dev The most recent withdrawal time and amount reset + uint256 __DEPRECATED_lastWithdrawalLimitReset; + /// @dev The accumulated withdrawn amount during the withdrawal limit window + uint256 __DEPRECATED_withdrawnAmountInWindow; + /// @dev A mapping user address => the total deposited amount by the user + mapping(address => uint256) __DEPRECATED_totalDepositedAmountPerUser; + /// @dev Stores the protocol version. Note, that the protocol version may not only encompass changes to the + /// smart contracts, but also to the node behavior. + uint256 protocolVersion; + /// @dev Hash of the system contract upgrade transaction. If 0, then no upgrade transaction needs to be done. + bytes32 l2SystemContractsUpgradeTxHash; + /// @dev Batch number where the upgrade transaction has happened. If 0, then no upgrade transaction has happened + /// yet. + uint256 l2SystemContractsUpgradeBatchNumber; + /// @dev Address which will exercise non-critical changes to the Diamond Proxy (changing validator set & unfreezing) + address admin; + /// @notice Address that the admin proposed as one that will replace admin role + address pendingAdmin; + /// @dev Fee params used to derive gasPrice for the L1->L2 transactions. For L2 transactions, + /// the bootloader gives enough freedom to the operator. + FeeParams feeParams; + /// @dev Address of the blob versioned hash getter smart contract used for EIP-4844 versioned hashes. + address blobVersionedHashRetriever; + /// @dev The chainId of the chain + uint256 chainId; + /// @dev The address of the bridgehub + address bridgehub; + /// @dev The address of the StateTransitionManager + address stateTransitionManager; + /// @dev The address of the baseToken contract. Eth is address(1) + address baseToken; + /// @dev The address of the baseTokenbridge. Eth also uses the shared bridge + address baseTokenBridge; + /// @notice gasPriceMultiplier for each baseToken, so that each L1->L2 transaction pays for its transaction on the destination + /// we multiply by the nominator, and divide by the denominator + uint128 baseTokenGasPriceMultiplierNominator; + uint128 baseTokenGasPriceMultiplierDenominator; + /// @dev The optional address of the contract that has to be used for transaction filtering/whitelisting + address transactionFilterer; +} diff --git a/contracts/l1-contracts/state-transition/chain-deps/facets/Admin.sol b/contracts/l1-contracts/state-transition/chain-deps/facets/Admin.sol new file mode 100644 index 0000000..9f98c00 --- /dev/null +++ b/contracts/l1-contracts/state-transition/chain-deps/facets/Admin.sol @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {IAdmin} from "../../chain-interfaces/IAdmin.sol"; +import {Diamond} from "../../libraries/Diamond.sol"; +import {MAX_GAS_PER_TRANSACTION} from "../../../common/Config.sol"; +import {FeeParams, PubdataPricingMode} from "../ZkSyncHyperchainStorage.sol"; +import {ZkSyncHyperchainBase} from "./ZkSyncHyperchainBase.sol"; +import {IStateTransitionManager} from "../../IStateTransitionManager.sol"; +import {Unauthorized, TooMuchGas, PriorityTxPubdataExceedsMaxPubDataPerBatch, InvalidPubdataPricingMode, ProtocolIdMismatch, ChainAlreadyLive, HashMismatch, ProtocolIdNotGreater, DenominatorIsZero, DiamondAlreadyFrozen, DiamondNotFrozen} from "../../../common/L1ContractErrors.sol"; + +// While formally the following import is not used, it is needed to inherit documentation from it +import {IZkSyncHyperchainBase} from "../../chain-interfaces/IZkSyncHyperchainBase.sol"; + +/// @title Admin Contract controls access rights for contract management. +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +contract AdminFacet is ZkSyncHyperchainBase, IAdmin { + /// @inheritdoc IZkSyncHyperchainBase + string public constant override getName = "AdminFacet"; + + /// @inheritdoc IAdmin + function setPendingAdmin(address _newPendingAdmin) external onlyAdmin { + // Save previous value into the stack to put it into the event later + address oldPendingAdmin = s.pendingAdmin; + // Change pending admin + s.pendingAdmin = _newPendingAdmin; + emit NewPendingAdmin(oldPendingAdmin, _newPendingAdmin); + } + + /// @inheritdoc IAdmin + function acceptAdmin() external { + address pendingAdmin = s.pendingAdmin; + // Only proposed by current admin address can claim the admin rights + if (msg.sender != pendingAdmin) { + revert Unauthorized(msg.sender); + } + + address previousAdmin = s.admin; + s.admin = pendingAdmin; + delete s.pendingAdmin; + + emit NewPendingAdmin(pendingAdmin, address(0)); + emit NewAdmin(previousAdmin, pendingAdmin); + } + + /// @inheritdoc IAdmin + function setValidator(address _validator, bool _active) external onlyStateTransitionManager { + s.validators[_validator] = _active; + emit ValidatorStatusUpdate(_validator, _active); + } + + /// @inheritdoc IAdmin + function setPorterAvailability(bool _zkPorterIsAvailable) external onlyStateTransitionManager { + // Change the porter availability + s.zkPorterIsAvailable = _zkPorterIsAvailable; + emit IsPorterAvailableStatusUpdate(_zkPorterIsAvailable); + } + + /// @inheritdoc IAdmin + function setPriorityTxMaxGasLimit(uint256 _newPriorityTxMaxGasLimit) external onlyStateTransitionManager { + if (_newPriorityTxMaxGasLimit > MAX_GAS_PER_TRANSACTION) { + revert TooMuchGas(); + } + + uint256 oldPriorityTxMaxGasLimit = s.priorityTxMaxGasLimit; + s.priorityTxMaxGasLimit = _newPriorityTxMaxGasLimit; + emit NewPriorityTxMaxGasLimit(oldPriorityTxMaxGasLimit, _newPriorityTxMaxGasLimit); + } + + /// @inheritdoc IAdmin + function changeFeeParams(FeeParams calldata _newFeeParams) external onlyAdminOrStateTransitionManager { + // Double checking that the new fee params are valid, i.e. + // the maximal pubdata per batch is not less than the maximal pubdata per priority transaction. + if (_newFeeParams.maxPubdataPerBatch < _newFeeParams.priorityTxMaxPubdata) { + revert PriorityTxPubdataExceedsMaxPubDataPerBatch(); + } + + FeeParams memory oldFeeParams = s.feeParams; + + // we cannot change pubdata pricing mode + if (_newFeeParams.pubdataPricingMode != oldFeeParams.pubdataPricingMode) { + revert InvalidPubdataPricingMode(); + } + + s.feeParams = _newFeeParams; + + emit NewFeeParams(oldFeeParams, _newFeeParams); + } + + /// @inheritdoc IAdmin + function setTokenMultiplier(uint128 _nominator, uint128 _denominator) external onlyAdminOrStateTransitionManager { + if (_denominator == 0) { + revert DenominatorIsZero(); + } + uint128 oldNominator = s.baseTokenGasPriceMultiplierNominator; + uint128 oldDenominator = s.baseTokenGasPriceMultiplierDenominator; + + s.baseTokenGasPriceMultiplierNominator = _nominator; + s.baseTokenGasPriceMultiplierDenominator = _denominator; + + emit NewBaseTokenMultiplier(oldNominator, oldDenominator, _nominator, _denominator); + } + + /// @inheritdoc IAdmin + function setPubdataPricingMode(PubdataPricingMode _pricingMode) external onlyAdmin { + // Validium mode can be set only before the first batch is processed + if (s.totalBatchesCommitted != 0) { + revert ChainAlreadyLive(); + } + s.feeParams.pubdataPricingMode = _pricingMode; + emit ValidiumModeStatusUpdate(_pricingMode); + } + + function setTransactionFilterer(address _transactionFilterer) external onlyAdmin { + address oldTransactionFilterer = s.transactionFilterer; + s.transactionFilterer = _transactionFilterer; + emit NewTransactionFilterer(oldTransactionFilterer, _transactionFilterer); + } + + /*////////////////////////////////////////////////////////////// + UPGRADE EXECUTION + //////////////////////////////////////////////////////////////*/ + + /// @inheritdoc IAdmin + function upgradeChainFromVersion( + uint256 _oldProtocolVersion, + Diamond.DiamondCutData calldata _diamondCut + ) external onlyAdminOrStateTransitionManager { + bytes32 cutHashInput = keccak256(abi.encode(_diamondCut)); + bytes32 upgradeCutHash = IStateTransitionManager(s.stateTransitionManager).upgradeCutHash(_oldProtocolVersion); + if (cutHashInput != upgradeCutHash) { + revert HashMismatch(upgradeCutHash, cutHashInput); + } + + if (s.protocolVersion != _oldProtocolVersion) { + revert ProtocolIdMismatch(s.protocolVersion, _oldProtocolVersion); + } + Diamond.diamondCut(_diamondCut); + emit ExecuteUpgrade(_diamondCut); + if (s.protocolVersion <= _oldProtocolVersion) { + revert ProtocolIdNotGreater(); + } + } + + /// @inheritdoc IAdmin + function executeUpgrade(Diamond.DiamondCutData calldata _diamondCut) external onlyStateTransitionManager { + Diamond.diamondCut(_diamondCut); + emit ExecuteUpgrade(_diamondCut); + } + + /*////////////////////////////////////////////////////////////// + CONTRACT FREEZING + //////////////////////////////////////////////////////////////*/ + + /// @inheritdoc IAdmin + function freezeDiamond() external onlyStateTransitionManager { + Diamond.DiamondStorage storage diamondStorage = Diamond.getDiamondStorage(); + + // diamond proxy is frozen already + if (diamondStorage.isFrozen) { + revert DiamondAlreadyFrozen(); + } + diamondStorage.isFrozen = true; + + emit Freeze(); + } + + /// @inheritdoc IAdmin + function unfreezeDiamond() external onlyStateTransitionManager { + Diamond.DiamondStorage storage diamondStorage = Diamond.getDiamondStorage(); + + // diamond proxy is not frozen + if (!diamondStorage.isFrozen) { + revert DiamondNotFrozen(); + } + diamondStorage.isFrozen = false; + + emit Unfreeze(); + } +} diff --git a/contracts/l1-contracts/state-transition/chain-deps/facets/Executor.sol b/contracts/l1-contracts/state-transition/chain-deps/facets/Executor.sol new file mode 100644 index 0000000..0799564 --- /dev/null +++ b/contracts/l1-contracts/state-transition/chain-deps/facets/Executor.sol @@ -0,0 +1,778 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {ZkSyncHyperchainBase} from "./ZkSyncHyperchainBase.sol"; +import {COMMIT_TIMESTAMP_NOT_OLDER, COMMIT_TIMESTAMP_APPROXIMATION_DELTA, EMPTY_STRING_KECCAK, L2_TO_L1_LOG_SERIALIZE_SIZE, MAX_L2_TO_L1_LOGS_COMMITMENT_BYTES, PACKED_L2_BLOCK_TIMESTAMP_MASK, PUBLIC_INPUT_SHIFT, POINT_EVALUATION_PRECOMPILE_ADDR} from "../../../common/Config.sol"; +import {IExecutor, L2_LOG_ADDRESS_OFFSET, L2_LOG_KEY_OFFSET, L2_LOG_VALUE_OFFSET, SystemLogKey, LogProcessingOutput, PubdataSource, BLS_MODULUS, PUBDATA_COMMITMENT_SIZE, PUBDATA_COMMITMENT_CLAIMED_VALUE_OFFSET, PUBDATA_COMMITMENT_COMMITMENT_OFFSET, MAX_NUMBER_OF_BLOBS, TOTAL_BLOBS_IN_COMMITMENT, BLOB_SIZE_BYTES} from "../../chain-interfaces/IExecutor.sol"; +import {PriorityQueue, PriorityOperation} from "../../libraries/PriorityQueue.sol"; +import {UncheckedMath} from "../../../common/libraries/UncheckedMath.sol"; +import {UnsafeBytes} from "../../../common/libraries/UnsafeBytes.sol"; +import {L2_BOOTLOADER_ADDRESS, L2_TO_L1_MESSENGER_SYSTEM_CONTRACT_ADDR, L2_SYSTEM_CONTEXT_SYSTEM_CONTRACT_ADDR, L2_PUBDATA_CHUNK_PUBLISHER_ADDR} from "../../../common/L2ContractAddresses.sol"; +import {PubdataPricingMode} from "../ZkSyncHyperchainStorage.sol"; +import {IStateTransitionManager} from "../../IStateTransitionManager.sol"; +import {BatchNumberMismatch, TimeNotReached, TooManyBlobs, ValueMismatch, InvalidPubdataMode, InvalidPubdataLength, HashMismatch, NonIncreasingTimestamp, TimestampError, InvalidLogSender, TxHashMismatch, UnexpectedSystemLog, MissingSystemLogs, LogAlreadyProcessed, InvalidProtocolVersion, CanOnlyProcessOneBatch, BatchHashMismatch, UpgradeBatchNumberIsNotZero, NonSequentialBatch, CantExecuteUnprovenBatches, SystemLogsSizeTooBig, InvalidNumberOfBlobs, VerifiedBatchesExceedsCommittedBatches, InvalidProof, RevertedBatchNotAfterNewLastBatch, CantRevertExecutedBatch, PointEvalFailed, EmptyBlobVersionHash, NonEmptyBlobVersionHash, BlobHashCommitmentError, CalldataLengthTooBig, InvalidPubdataHash, L2TimestampTooBig, PriorityOperationsRollingHashMismatch, PubdataCommitmentsEmpty, PointEvalCallFailed, PubdataCommitmentsTooBig, InvalidPubdataCommitmentsSize} from "../../../common/L1ContractErrors.sol"; + +// While formally the following import is not used, it is needed to inherit documentation from it +import {IZkSyncHyperchainBase} from "../../chain-interfaces/IZkSyncHyperchainBase.sol"; + +/// @title ZKsync hyperchain Executor contract capable of processing events emitted in the ZKsync hyperchain protocol. +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +contract ExecutorFacet is ZkSyncHyperchainBase, IExecutor { + using UncheckedMath for uint256; + using PriorityQueue for PriorityQueue.Queue; + + /// @inheritdoc IZkSyncHyperchainBase + string public constant override getName = "ExecutorFacet"; + + /// @dev Process one batch commit using the previous batch StoredBatchInfo + /// @dev returns new batch StoredBatchInfo + /// @notice Does not change storage + function _commitOneBatch( + StoredBatchInfo memory _previousBatch, + CommitBatchInfo calldata _newBatch, + bytes32 _expectedSystemContractUpgradeTxHash + ) internal view returns (StoredBatchInfo memory) { + // only commit next batch + if (_newBatch.batchNumber != _previousBatch.batchNumber + 1) { + revert BatchNumberMismatch(_previousBatch.batchNumber + 1, _newBatch.batchNumber); + } + + uint8 pubdataSource = uint8(bytes1(_newBatch.pubdataCommitments[0])); + PubdataPricingMode pricingMode = s.feeParams.pubdataPricingMode; + if ( + pricingMode != PubdataPricingMode.Validium && + pubdataSource != uint8(PubdataSource.Calldata) && + pubdataSource != uint8(PubdataSource.Blob) + ) { + revert InvalidPubdataMode(); + } + + // Check that batch contain all meta information for L2 logs. + // Get the chained hash of priority transaction hashes. + LogProcessingOutput memory logOutput = _processL2Logs(_newBatch, _expectedSystemContractUpgradeTxHash); + + bytes32[] memory blobCommitments = new bytes32[](MAX_NUMBER_OF_BLOBS); + if (pricingMode == PubdataPricingMode.Validium) { + // skipping data validation for validium, we just check that the data is empty + if (_newBatch.pubdataCommitments.length != 1) { + revert CalldataLengthTooBig(); + } + for (uint8 i = uint8(SystemLogKey.BLOB_ONE_HASH_KEY); i <= uint8(SystemLogKey.BLOB_SIX_HASH_KEY); ++i) { + logOutput.blobHashes[i - uint8(SystemLogKey.BLOB_ONE_HASH_KEY)] = bytes32(0); + } + } else if (pubdataSource == uint8(PubdataSource.Blob)) { + // In this scenario, pubdataCommitments is a list of: opening point (16 bytes) || claimed value (32 bytes) || commitment (48 bytes) || proof (48 bytes)) = 144 bytes + blobCommitments = _verifyBlobInformation(_newBatch.pubdataCommitments[1:], logOutput.blobHashes); + } else if (pubdataSource == uint8(PubdataSource.Calldata)) { + // In this scenario pubdataCommitments is actual pubdata consisting of l2 to l1 logs, l2 to l1 message, compressed smart contract bytecode, and compressed state diffs + if (_newBatch.pubdataCommitments.length > BLOB_SIZE_BYTES) { + revert InvalidPubdataLength(); + } + bytes32 pubdataHash = keccak256(_newBatch.pubdataCommitments[1:_newBatch.pubdataCommitments.length - 32]); + if (logOutput.pubdataHash != pubdataHash) { + revert InvalidPubdataHash(pubdataHash, logOutput.pubdataHash); + } + blobCommitments[0] = bytes32( + _newBatch.pubdataCommitments[_newBatch.pubdataCommitments.length - 32:_newBatch + .pubdataCommitments + .length] + ); + } + + if (_previousBatch.batchHash != logOutput.previousBatchHash) { + revert HashMismatch(logOutput.previousBatchHash, _previousBatch.batchHash); + } + // Check that the priority operation hash in the L2 logs is as expected + if (logOutput.chainedPriorityTxsHash != _newBatch.priorityOperationsHash) { + revert HashMismatch(logOutput.chainedPriorityTxsHash, _newBatch.priorityOperationsHash); + } + // Check that the number of processed priority operations is as expected + if (logOutput.numberOfLayer1Txs != _newBatch.numberOfLayer1Txs) { + revert ValueMismatch(logOutput.numberOfLayer1Txs, _newBatch.numberOfLayer1Txs); + } + + // Check the timestamp of the new batch + _verifyBatchTimestamp(logOutput.packedBatchAndL2BlockTimestamp, _newBatch.timestamp, _previousBatch.timestamp); + + // Create batch commitment for the proof verification + bytes32 commitment = _createBatchCommitment( + _newBatch, + logOutput.stateDiffHash, + blobCommitments, + logOutput.blobHashes + ); + + return + StoredBatchInfo({ + batchNumber: _newBatch.batchNumber, + batchHash: _newBatch.newStateRoot, + indexRepeatedStorageChanges: _newBatch.indexRepeatedStorageChanges, + numberOfLayer1Txs: _newBatch.numberOfLayer1Txs, + priorityOperationsHash: _newBatch.priorityOperationsHash, + l2LogsTreeRoot: logOutput.l2LogsTreeRoot, + timestamp: _newBatch.timestamp, + commitment: commitment + }); + } + + /// @notice checks that the timestamps of both the new batch and the new L2 block are correct. + /// @param _packedBatchAndL2BlockTimestamp - packed batch and L2 block timestamp in a format of batchTimestamp * 2**128 + l2BatchTimestamp + /// @param _expectedBatchTimestamp - expected batch timestamp + /// @param _previousBatchTimestamp - the timestamp of the previous batch + function _verifyBatchTimestamp( + uint256 _packedBatchAndL2BlockTimestamp, + uint256 _expectedBatchTimestamp, + uint256 _previousBatchTimestamp + ) internal view { + // Check that the timestamp that came from the system context is expected + uint256 batchTimestamp = _packedBatchAndL2BlockTimestamp >> 128; + if (batchTimestamp != _expectedBatchTimestamp) { + revert TimestampError(); + } + + // While the fact that _previousBatchTimestamp < batchTimestamp is already checked on L2, + // we double check it here for clarity + if (_previousBatchTimestamp >= batchTimestamp) { + revert NonIncreasingTimestamp(); + } + + uint256 lastL2BlockTimestamp = _packedBatchAndL2BlockTimestamp & PACKED_L2_BLOCK_TIMESTAMP_MASK; + + // All L2 blocks have timestamps within the range of [batchTimestamp, lastL2BlockTimestamp]. + // So here we need to only double check that: + // - The timestamp of the batch is not too small. + // - The timestamp of the last L2 block is not too big. + // New batch timestamp is too small + if (block.timestamp - COMMIT_TIMESTAMP_NOT_OLDER > batchTimestamp) { + revert TimeNotReached(batchTimestamp, block.timestamp - COMMIT_TIMESTAMP_NOT_OLDER); + } + // The last L2 block timestamp is too big + if (lastL2BlockTimestamp > block.timestamp + COMMIT_TIMESTAMP_APPROXIMATION_DELTA) { + revert L2TimestampTooBig(); + } + } + + /// @dev Check that L2 logs are proper and batch contain all meta information for them + /// @dev The logs processed here should line up such that only one log for each key from the + /// SystemLogKey enum in Constants.sol is processed per new batch. + /// @dev Data returned from here will be used to form the batch commitment. + function _processL2Logs( + CommitBatchInfo calldata _newBatch, + bytes32 _expectedSystemContractUpgradeTxHash + ) internal pure returns (LogProcessingOutput memory logOutput) { + // Copy L2 to L1 logs into memory. + bytes memory emittedL2Logs = _newBatch.systemLogs; + + logOutput.blobHashes = new bytes32[](MAX_NUMBER_OF_BLOBS); + + // Used as bitmap to set/check log processing happens exactly once. + // See SystemLogKey enum in Constants.sol for ordering. + uint256 processedLogs = 0; + + // linear traversal of the logs + uint256 logsLength = emittedL2Logs.length; + for (uint256 i = 0; i < logsLength; i = i.uncheckedAdd(L2_TO_L1_LOG_SERIALIZE_SIZE)) { + // Extract the values to be compared to/used such as the log sender, key, and value + // slither-disable-next-line unused-return + (address logSender, ) = UnsafeBytes.readAddress(emittedL2Logs, i + L2_LOG_ADDRESS_OFFSET); + // slither-disable-next-line unused-return + (uint256 logKey, ) = UnsafeBytes.readUint256(emittedL2Logs, i + L2_LOG_KEY_OFFSET); + // slither-disable-next-line unused-return + (bytes32 logValue, ) = UnsafeBytes.readBytes32(emittedL2Logs, i + L2_LOG_VALUE_OFFSET); + + // Ensure that the log hasn't been processed already + if (_checkBit(processedLogs, uint8(logKey))) { + revert LogAlreadyProcessed(uint8(logKey)); + } + processedLogs = _setBit(processedLogs, uint8(logKey)); + + // Need to check that each log was sent by the correct address. + if (logKey == uint256(SystemLogKey.L2_TO_L1_LOGS_TREE_ROOT_KEY)) { + if (logSender != L2_TO_L1_MESSENGER_SYSTEM_CONTRACT_ADDR) { + revert InvalidLogSender(logSender, logKey); + } + logOutput.l2LogsTreeRoot = logValue; + } else if (logKey == uint256(SystemLogKey.TOTAL_L2_TO_L1_PUBDATA_KEY)) { + if (logSender != L2_TO_L1_MESSENGER_SYSTEM_CONTRACT_ADDR) { + revert InvalidLogSender(logSender, logKey); + } + logOutput.pubdataHash = logValue; + } else if (logKey == uint256(SystemLogKey.STATE_DIFF_HASH_KEY)) { + if (logSender != L2_TO_L1_MESSENGER_SYSTEM_CONTRACT_ADDR) { + revert InvalidLogSender(logSender, logKey); + } + logOutput.stateDiffHash = logValue; + } else if (logKey == uint256(SystemLogKey.PACKED_BATCH_AND_L2_BLOCK_TIMESTAMP_KEY)) { + if (logSender != L2_SYSTEM_CONTEXT_SYSTEM_CONTRACT_ADDR) { + revert InvalidLogSender(logSender, logKey); + } + logOutput.packedBatchAndL2BlockTimestamp = uint256(logValue); + } else if (logKey == uint256(SystemLogKey.PREV_BATCH_HASH_KEY)) { + if (logSender != L2_SYSTEM_CONTEXT_SYSTEM_CONTRACT_ADDR) { + revert InvalidLogSender(logSender, logKey); + } + logOutput.previousBatchHash = logValue; + } else if (logKey == uint256(SystemLogKey.CHAINED_PRIORITY_TXN_HASH_KEY)) { + if (logSender != L2_BOOTLOADER_ADDRESS) { + revert InvalidLogSender(logSender, logKey); + } + logOutput.chainedPriorityTxsHash = logValue; + } else if (logKey == uint256(SystemLogKey.NUMBER_OF_LAYER_1_TXS_KEY)) { + if (logSender != L2_BOOTLOADER_ADDRESS) { + revert InvalidLogSender(logSender, logKey); + } + logOutput.numberOfLayer1Txs = uint256(logValue); + } else if ( + logKey >= uint256(SystemLogKey.BLOB_ONE_HASH_KEY) && logKey <= uint256(SystemLogKey.BLOB_SIX_HASH_KEY) + ) { + if (logSender != L2_PUBDATA_CHUNK_PUBLISHER_ADDR) { + revert InvalidLogSender(logSender, logKey); + } + uint8 blobNumber = uint8(logKey) - uint8(SystemLogKey.BLOB_ONE_HASH_KEY); + + if (blobNumber >= MAX_NUMBER_OF_BLOBS) { + revert TooManyBlobs(); + } + + logOutput.blobHashes[blobNumber] = logValue; + } else if (logKey == uint256(SystemLogKey.EXPECTED_SYSTEM_CONTRACT_UPGRADE_TX_HASH_KEY)) { + if (logSender != L2_BOOTLOADER_ADDRESS) { + revert InvalidLogSender(logSender, logKey); + } + if (_expectedSystemContractUpgradeTxHash != logValue) { + revert TxHashMismatch(); + } + } else if (logKey > uint256(SystemLogKey.EXPECTED_SYSTEM_CONTRACT_UPGRADE_TX_HASH_KEY)) { + revert UnexpectedSystemLog(logKey); + } + } + + // We only require 13 logs to be checked, the 14th is if we are expecting a protocol upgrade + // Without the protocol upgrade we expect 13 logs: 2^13 - 1 = 8191 + // With the protocol upgrade we expect 14 logs: 2^14 - 1 = 16383 + if (_expectedSystemContractUpgradeTxHash == bytes32(0)) { + if (processedLogs != 8191) { + revert MissingSystemLogs(8191, processedLogs); + } + } else { + if (processedLogs != 16383) { + revert MissingSystemLogs(16383, processedLogs); + } + } + } + + /// @inheritdoc IExecutor + function commitBatches( + StoredBatchInfo calldata _lastCommittedBatchData, + CommitBatchInfo[] calldata _newBatchesData + ) external nonReentrant onlyValidator { + _commitBatches(_lastCommittedBatchData, _newBatchesData); + } + + /// @inheritdoc IExecutor + function commitBatchesSharedBridge( + uint256, // _chainId + StoredBatchInfo calldata _lastCommittedBatchData, + CommitBatchInfo[] calldata _newBatchesData + ) external nonReentrant onlyValidator { + _commitBatches(_lastCommittedBatchData, _newBatchesData); + } + + function _commitBatches( + StoredBatchInfo memory _lastCommittedBatchData, + CommitBatchInfo[] calldata _newBatchesData + ) internal { + // check that we have the right protocol version + // three comments: + // 1. A chain has to keep their protocol version up to date, as processing a block requires the latest or previous protocol version + // to solve this we will need to add the feature to create batches with only the protocol upgrade tx, without any other txs. + // 2. A chain might become out of sync if it launches while we are in the middle of a protocol upgrade. This would mean they cannot process their genesis upgrade + // as their protocolversion would be outdated, and they also cannot process the protocol upgrade tx as they have a pending upgrade. + // 3. The protocol upgrade is increased in the BaseZkSyncUpgrade, in the executor only the systemContractsUpgradeTxHash is checked + if (!IStateTransitionManager(s.stateTransitionManager).protocolVersionIsActive(s.protocolVersion)) { + revert InvalidProtocolVersion(); + } + // With the new changes for EIP-4844, namely the restriction on number of blobs per block, we only allow for a single batch to be committed at a time. + if (_newBatchesData.length != 1) { + revert CanOnlyProcessOneBatch(); + } + // Check that we commit batches after last committed batch + if (s.storedBatchHashes[s.totalBatchesCommitted] != _hashStoredBatchInfo(_lastCommittedBatchData)) { + // incorrect previous batch data + revert BatchHashMismatch( + s.storedBatchHashes[s.totalBatchesCommitted], + _hashStoredBatchInfo(_lastCommittedBatchData) + ); + } + + bytes32 systemContractsUpgradeTxHash = s.l2SystemContractsUpgradeTxHash; + // Upgrades are rarely done so we optimize a case with no active system contracts upgrade. + if (systemContractsUpgradeTxHash == bytes32(0) || s.l2SystemContractsUpgradeBatchNumber != 0) { + _commitBatchesWithoutSystemContractsUpgrade(_lastCommittedBatchData, _newBatchesData); + } else { + _commitBatchesWithSystemContractsUpgrade( + _lastCommittedBatchData, + _newBatchesData, + systemContractsUpgradeTxHash + ); + } + + s.totalBatchesCommitted = s.totalBatchesCommitted + _newBatchesData.length; + } + + /// @dev Commits new batches without any system contracts upgrade. + /// @param _lastCommittedBatchData The data of the last committed batch. + /// @param _newBatchesData An array of batch data that needs to be committed. + function _commitBatchesWithoutSystemContractsUpgrade( + StoredBatchInfo memory _lastCommittedBatchData, + CommitBatchInfo[] calldata _newBatchesData + ) internal { + // We disable this check because calldata array length is cheap. + // solhint-disable-next-line gas-length-in-loops + for (uint256 i = 0; i < _newBatchesData.length; i = i.uncheckedInc()) { + _lastCommittedBatchData = _commitOneBatch(_lastCommittedBatchData, _newBatchesData[i], bytes32(0)); + + s.storedBatchHashes[_lastCommittedBatchData.batchNumber] = _hashStoredBatchInfo(_lastCommittedBatchData); + emit BlockCommit( + _lastCommittedBatchData.batchNumber, + _lastCommittedBatchData.batchHash, + _lastCommittedBatchData.commitment + ); + } + } + + /// @dev Commits new batches with a system contracts upgrade transaction. + /// @param _lastCommittedBatchData The data of the last committed batch. + /// @param _newBatchesData An array of batch data that needs to be committed. + /// @param _systemContractUpgradeTxHash The transaction hash of the system contract upgrade. + function _commitBatchesWithSystemContractsUpgrade( + StoredBatchInfo memory _lastCommittedBatchData, + CommitBatchInfo[] calldata _newBatchesData, + bytes32 _systemContractUpgradeTxHash + ) internal { + // The system contract upgrade is designed to be executed atomically with the new bootloader, a default account, + // ZKP verifier, and other system parameters. Hence, we ensure that the upgrade transaction is + // carried out within the first batch committed after the upgrade. + + // While the logic of the contract ensures that the s.l2SystemContractsUpgradeBatchNumber is 0 when this function is called, + // this check is added just in case. Since it is a hot read, it does not incur noticeable gas cost. + if (s.l2SystemContractsUpgradeBatchNumber != 0) { + revert UpgradeBatchNumberIsNotZero(); + } + + // Save the batch number where the upgrade transaction was executed. + s.l2SystemContractsUpgradeBatchNumber = _newBatchesData[0].batchNumber; + + // We disable this check because calldata array length is cheap. + // solhint-disable-next-line gas-length-in-loops + for (uint256 i = 0; i < _newBatchesData.length; i = i.uncheckedInc()) { + // The upgrade transaction must only be included in the first batch. + bytes32 expectedUpgradeTxHash = i == 0 ? _systemContractUpgradeTxHash : bytes32(0); + _lastCommittedBatchData = _commitOneBatch( + _lastCommittedBatchData, + _newBatchesData[i], + expectedUpgradeTxHash + ); + + s.storedBatchHashes[_lastCommittedBatchData.batchNumber] = _hashStoredBatchInfo(_lastCommittedBatchData); + emit BlockCommit( + _lastCommittedBatchData.batchNumber, + _lastCommittedBatchData.batchHash, + _lastCommittedBatchData.commitment + ); + } + } + + /// @dev Pops the priority operations from the priority queue and returns a rolling hash of operations + function _collectOperationsFromPriorityQueue(uint256 _nPriorityOps) internal returns (bytes32 concatHash) { + concatHash = EMPTY_STRING_KECCAK; + + for (uint256 i = 0; i < _nPriorityOps; i = i.uncheckedInc()) { + PriorityOperation memory priorityOp = s.priorityQueue.popFront(); + concatHash = keccak256(abi.encode(concatHash, priorityOp.canonicalTxHash)); + } + } + + /// @dev Executes one batch + /// @dev 1. Processes all pending operations (Complete priority requests) + /// @dev 2. Finalizes batch on Ethereum + /// @dev _executedBatchIdx is an index in the array of the batches that we want to execute together + function _executeOneBatch(StoredBatchInfo memory _storedBatch, uint256 _executedBatchIdx) internal { + uint256 currentBatchNumber = _storedBatch.batchNumber; + if (currentBatchNumber != s.totalBatchesExecuted + _executedBatchIdx + 1) { + revert NonSequentialBatch(); + } + if (_hashStoredBatchInfo(_storedBatch) != s.storedBatchHashes[currentBatchNumber]) { + revert BatchHashMismatch(s.storedBatchHashes[currentBatchNumber], _hashStoredBatchInfo(_storedBatch)); + } + + bytes32 priorityOperationsHash = _collectOperationsFromPriorityQueue(_storedBatch.numberOfLayer1Txs); + if (priorityOperationsHash != _storedBatch.priorityOperationsHash) { + revert PriorityOperationsRollingHashMismatch(); + } + + // Save root hash of L2 -> L1 logs tree + s.l2LogsRootHashes[currentBatchNumber] = _storedBatch.l2LogsTreeRoot; + } + + /// @inheritdoc IExecutor + function executeBatchesSharedBridge( + uint256, + StoredBatchInfo[] calldata _batchesData + ) external nonReentrant onlyValidator { + _executeBatches(_batchesData); + } + + /// @inheritdoc IExecutor + function executeBatches(StoredBatchInfo[] calldata _batchesData) external nonReentrant onlyValidator { + _executeBatches(_batchesData); + } + + function _executeBatches(StoredBatchInfo[] calldata _batchesData) internal { + uint256 nBatches = _batchesData.length; + for (uint256 i = 0; i < nBatches; i = i.uncheckedInc()) { + _executeOneBatch(_batchesData[i], i); + emit BlockExecution(_batchesData[i].batchNumber, _batchesData[i].batchHash, _batchesData[i].commitment); + } + + uint256 newTotalBatchesExecuted = s.totalBatchesExecuted + nBatches; + s.totalBatchesExecuted = newTotalBatchesExecuted; + if (newTotalBatchesExecuted > s.totalBatchesVerified) { + revert CantExecuteUnprovenBatches(); + } + + uint256 batchWhenUpgradeHappened = s.l2SystemContractsUpgradeBatchNumber; + if (batchWhenUpgradeHappened != 0 && batchWhenUpgradeHappened <= newTotalBatchesExecuted) { + delete s.l2SystemContractsUpgradeTxHash; + delete s.l2SystemContractsUpgradeBatchNumber; + } + } + + /// @inheritdoc IExecutor + function proveBatches( + StoredBatchInfo calldata _prevBatch, + StoredBatchInfo[] calldata _committedBatches, + ProofInput calldata _proof + ) external nonReentrant onlyValidator { + _proveBatches(_prevBatch, _committedBatches, _proof); + } + + /// @inheritdoc IExecutor + function proveBatchesSharedBridge( + uint256, // _chainId + StoredBatchInfo calldata _prevBatch, + StoredBatchInfo[] calldata _committedBatches, + ProofInput calldata _proof + ) external nonReentrant onlyValidator { + _proveBatches(_prevBatch, _committedBatches, _proof); + } + + function _proveBatches( + StoredBatchInfo calldata _prevBatch, + StoredBatchInfo[] calldata _committedBatches, + ProofInput calldata _proof + ) internal { + // Save the variables into the stack to save gas on reading them later + uint256 currentTotalBatchesVerified = s.totalBatchesVerified; + uint256 committedBatchesLength = _committedBatches.length; + + // Initialize the array, that will be used as public input to the ZKP + uint256[] memory proofPublicInput = new uint256[](committedBatchesLength); + + // Check that the batch passed by the validator is indeed the first unverified batch + if (_hashStoredBatchInfo(_prevBatch) != s.storedBatchHashes[currentTotalBatchesVerified]) { + revert BatchHashMismatch( + s.storedBatchHashes[currentTotalBatchesVerified], + _hashStoredBatchInfo(_prevBatch) + ); + } + + bytes32 prevBatchCommitment = _prevBatch.commitment; + for (uint256 i = 0; i < committedBatchesLength; i = i.uncheckedInc()) { + currentTotalBatchesVerified = currentTotalBatchesVerified.uncheckedInc(); + if (_hashStoredBatchInfo(_committedBatches[i]) != s.storedBatchHashes[currentTotalBatchesVerified]) { + revert BatchHashMismatch( + s.storedBatchHashes[currentTotalBatchesVerified], + _hashStoredBatchInfo(_committedBatches[i]) + ); + } + + bytes32 currentBatchCommitment = _committedBatches[i].commitment; + proofPublicInput[i] = _getBatchProofPublicInput(prevBatchCommitment, currentBatchCommitment); + + prevBatchCommitment = currentBatchCommitment; + } + if (currentTotalBatchesVerified > s.totalBatchesCommitted) { + revert VerifiedBatchesExceedsCommittedBatches(); + } + + _verifyProof(proofPublicInput, _proof); + + emit BlocksVerification(s.totalBatchesVerified, currentTotalBatchesVerified); + s.totalBatchesVerified = currentTotalBatchesVerified; + } + + function _verifyProof(uint256[] memory proofPublicInput, ProofInput calldata _proof) internal view { + // We can only process 1 batch proof at a time. + if (proofPublicInput.length != 1) { + revert CanOnlyProcessOneBatch(); + } + + bool successVerifyProof = s.verifier.verify( + proofPublicInput, + _proof.serializedProof, + _proof.recursiveAggregationInput + ); + if (!successVerifyProof) { + revert InvalidProof(); + } + } + + /// @dev Gets zk proof public input + function _getBatchProofPublicInput( + bytes32 _prevBatchCommitment, + bytes32 _currentBatchCommitment + ) internal pure returns (uint256) { + return + uint256(keccak256(abi.encodePacked(_prevBatchCommitment, _currentBatchCommitment))) >> PUBLIC_INPUT_SHIFT; + } + + /// @inheritdoc IExecutor + function revertBatches(uint256 _newLastBatch) external nonReentrant onlyValidatorOrStateTransitionManager { + _revertBatches(_newLastBatch); + } + + /// @inheritdoc IExecutor + function revertBatchesSharedBridge(uint256, uint256 _newLastBatch) external nonReentrant onlyValidator { + _revertBatches(_newLastBatch); + } + + function _revertBatches(uint256 _newLastBatch) internal { + if (s.totalBatchesCommitted <= _newLastBatch) { + revert RevertedBatchNotAfterNewLastBatch(); + } + if (_newLastBatch < s.totalBatchesExecuted) { + revert CantRevertExecutedBatch(); + } + + if (_newLastBatch < s.totalBatchesVerified) { + s.totalBatchesVerified = _newLastBatch; + } + s.totalBatchesCommitted = _newLastBatch; + + // Reset the batch number of the executed system contracts upgrade transaction if the batch + // where the system contracts upgrade was committed is among the reverted batches. + if (s.l2SystemContractsUpgradeBatchNumber > _newLastBatch) { + delete s.l2SystemContractsUpgradeBatchNumber; + } + + emit BlocksRevert(s.totalBatchesCommitted, s.totalBatchesVerified, s.totalBatchesExecuted); + } + + /// @dev Creates batch commitment from its data + function _createBatchCommitment( + CommitBatchInfo calldata _newBatchData, + bytes32 _stateDiffHash, + bytes32[] memory _blobCommitments, + bytes32[] memory _blobHashes + ) internal view returns (bytes32) { + bytes32 passThroughDataHash = keccak256(_batchPassThroughData(_newBatchData)); + bytes32 metadataHash = keccak256(_batchMetaParameters()); + bytes32 auxiliaryOutputHash = keccak256( + _batchAuxiliaryOutput(_newBatchData, _stateDiffHash, _blobCommitments, _blobHashes) + ); + + return keccak256(abi.encode(passThroughDataHash, metadataHash, auxiliaryOutputHash)); + } + + function _batchPassThroughData(CommitBatchInfo calldata _batch) internal pure returns (bytes memory) { + return + abi.encodePacked( + // solhint-disable-next-line func-named-parameters + _batch.indexRepeatedStorageChanges, + _batch.newStateRoot, + uint64(0), // index repeated storage changes in zkPorter + bytes32(0) // zkPorter batch hash + ); + } + + function _batchMetaParameters() internal view returns (bytes memory) { + bytes32 l2DefaultAccountBytecodeHash = s.l2DefaultAccountBytecodeHash; + return + abi.encodePacked( + s.zkPorterIsAvailable, + s.l2BootloaderBytecodeHash, + l2DefaultAccountBytecodeHash, + // VM 1.5.0 requires us to pass the EVM simulator code hash. For now it is the same as the default account. + l2DefaultAccountBytecodeHash + ); + } + + function _batchAuxiliaryOutput( + CommitBatchInfo calldata _batch, + bytes32 _stateDiffHash, + bytes32[] memory _blobCommitments, + bytes32[] memory _blobHashes + ) internal pure returns (bytes memory) { + if (_batch.systemLogs.length > MAX_L2_TO_L1_LOGS_COMMITMENT_BYTES) { + revert SystemLogsSizeTooBig(); + } + + bytes32 l2ToL1LogsHash = keccak256(_batch.systemLogs); + + return + // solhint-disable-next-line func-named-parameters + abi.encodePacked( + l2ToL1LogsHash, + _stateDiffHash, + _batch.bootloaderHeapInitialContentsHash, + _batch.eventsQueueStateHash, + _encodeBlobAuxiliaryOutput(_blobCommitments, _blobHashes) + ); + } + + /// @dev Encodes the commitment to blobs to be used in the auxiliary output of the batch commitment + /// @param _blobCommitments - the commitments to the blobs + /// @param _blobHashes - the hashes of the blobs + /// @param blobAuxOutputWords - The circuit commitment to the blobs split into 32-byte words + function _encodeBlobAuxiliaryOutput( + bytes32[] memory _blobCommitments, + bytes32[] memory _blobHashes + ) internal pure returns (bytes32[] memory blobAuxOutputWords) { + // These invariants should be checked by the caller of this function, but we double check + // just in case. + if (_blobCommitments.length != MAX_NUMBER_OF_BLOBS || _blobHashes.length != MAX_NUMBER_OF_BLOBS) { + revert InvalidNumberOfBlobs(MAX_NUMBER_OF_BLOBS, _blobCommitments.length, _blobHashes.length); + } + + // for each blob we have: + // linear hash (hash of preimage from system logs) and + // output hash of blob commitments: keccak(versioned hash || opening point || evaluation value) + // These values will all be bytes32(0) when we submit pubdata via calldata instead of blobs. + // + // For now, only up to 6 blobs are supported by the contract, while 16 are required by the circuits. + // All the unfilled blobs will have their commitment as 0, including the case when we use only 1 blob. + + blobAuxOutputWords = new bytes32[](2 * TOTAL_BLOBS_IN_COMMITMENT); + + for (uint256 i = 0; i < MAX_NUMBER_OF_BLOBS; ++i) { + blobAuxOutputWords[i * 2] = _blobHashes[i]; + blobAuxOutputWords[i * 2 + 1] = _blobCommitments[i]; + } + } + + /// @notice Returns the keccak hash of the ABI-encoded StoredBatchInfo + function _hashStoredBatchInfo(StoredBatchInfo memory _storedBatchInfo) internal pure returns (bytes32) { + return keccak256(abi.encode(_storedBatchInfo)); + } + + /// @notice Returns true if the bit at index {_index} is 1 + function _checkBit(uint256 _bitMap, uint8 _index) internal pure returns (bool) { + return (_bitMap & (1 << _index)) > 0; + } + + /// @notice Sets the given bit in {_num} at index {_index} to 1. + function _setBit(uint256 _bitMap, uint8 _index) internal pure returns (uint256) { + return _bitMap | (1 << _index); + } + + /// @notice Calls the point evaluation precompile and verifies the output + /// Verify p(z) = y given commitment that corresponds to the polynomial p(x) and a KZG proof. + /// Also verify that the provided commitment matches the provided versioned_hash. + /// + function _pointEvaluationPrecompile( + bytes32 _versionedHash, + bytes32 _openingPoint, + bytes calldata _openingValueCommitmentProof + ) internal view { + bytes memory precompileInput = abi.encodePacked(_versionedHash, _openingPoint, _openingValueCommitmentProof); + + (bool success, bytes memory data) = POINT_EVALUATION_PRECOMPILE_ADDR.staticcall(precompileInput); + + // We verify that the point evaluation precompile call was successful by testing the latter 32 bytes of the + // response is equal to BLS_MODULUS as defined in https://eips.ethereum.org/EIPS/eip-4844#point-evaluation-precompile + if (!success) { + revert PointEvalCallFailed(precompileInput); + } + (, uint256 result) = abi.decode(data, (uint256, uint256)); + if (result != BLS_MODULUS) { + revert PointEvalFailed(abi.encode(result)); + } + } + + /// @dev Verifies that the blobs contain the correct data by calling the point evaluation precompile. For the precompile we need: + /// versioned hash || opening point || opening value || commitment || proof + /// the _pubdataCommitments will contain the last 4 values, the versioned hash is pulled from the BLOBHASH opcode + /// pubdataCommitments is a list of: opening point (16 bytes) || claimed value (32 bytes) || commitment (48 bytes) || proof (48 bytes)) = 144 bytes + function _verifyBlobInformation( + bytes calldata _pubdataCommitments, + bytes32[] memory _blobHashes + ) internal view returns (bytes32[] memory blobCommitments) { + uint256 versionedHashIndex = 0; + + if (_pubdataCommitments.length == 0) { + revert PubdataCommitmentsEmpty(); + } + if (_pubdataCommitments.length > PUBDATA_COMMITMENT_SIZE * MAX_NUMBER_OF_BLOBS) { + revert PubdataCommitmentsTooBig(); + } + if (_pubdataCommitments.length % PUBDATA_COMMITMENT_SIZE != 0) { + revert InvalidPubdataCommitmentsSize(); + } + blobCommitments = new bytes32[](MAX_NUMBER_OF_BLOBS); + + // We disable this check because calldata array length is cheap. + // solhint-disable-next-line gas-length-in-loops + for (uint256 i = 0; i < _pubdataCommitments.length; i += PUBDATA_COMMITMENT_SIZE) { + bytes32 blobVersionedHash = _getBlobVersionedHash(versionedHashIndex); + + if (blobVersionedHash == bytes32(0)) { + revert EmptyBlobVersionHash(versionedHashIndex); + } + + // First 16 bytes is the opening point. While we get the point as 16 bytes, the point evaluation precompile + // requires it to be 32 bytes. The blob commitment must use the opening point as 16 bytes though. + bytes32 openingPoint = bytes32( + uint256(uint128(bytes16(_pubdataCommitments[i:i + PUBDATA_COMMITMENT_CLAIMED_VALUE_OFFSET]))) + ); + + _pointEvaluationPrecompile( + blobVersionedHash, + openingPoint, + _pubdataCommitments[i + PUBDATA_COMMITMENT_CLAIMED_VALUE_OFFSET:i + PUBDATA_COMMITMENT_SIZE] + ); + + // Take the hash of the versioned hash || opening point || claimed value + blobCommitments[versionedHashIndex] = keccak256( + abi.encodePacked(blobVersionedHash, _pubdataCommitments[i:i + PUBDATA_COMMITMENT_COMMITMENT_OFFSET]) + ); + ++versionedHashIndex; + } + + // This check is required because we want to ensure that there aren't any extra blobs trying to be published. + // Calling the BLOBHASH opcode with an index > # blobs - 1 yields bytes32(0) + bytes32 versionedHash = _getBlobVersionedHash(versionedHashIndex); + if (versionedHash != bytes32(0)) { + revert NonEmptyBlobVersionHash(versionedHashIndex); + } + + // We verify that for each set of blobHash/blobCommitment are either both empty + // or there are values for both. + for (uint256 i = 0; i < MAX_NUMBER_OF_BLOBS; ++i) { + if ( + (_blobHashes[i] == bytes32(0) && blobCommitments[i] != bytes32(0)) || + (_blobHashes[i] != bytes32(0) && blobCommitments[i] == bytes32(0)) + ) { + revert BlobHashCommitmentError(i, _blobHashes[i] == bytes32(0), blobCommitments[i] == bytes32(0)); + } + } + } + + function _getBlobVersionedHash(uint256 _index) internal view virtual returns (bytes32 versionedHash) { + assembly { + versionedHash := blobhash(_index) + } + } +} diff --git a/contracts/l1-contracts/state-transition/chain-deps/facets/Getters.sol b/contracts/l1-contracts/state-transition/chain-deps/facets/Getters.sol new file mode 100644 index 0000000..9cb2a2d --- /dev/null +++ b/contracts/l1-contracts/state-transition/chain-deps/facets/Getters.sol @@ -0,0 +1,274 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {SafeCast} from "@openzeppelin/contracts-v4/utils/math/SafeCast.sol"; + +import {ZkSyncHyperchainBase} from "./ZkSyncHyperchainBase.sol"; +import {PubdataPricingMode} from "../ZkSyncHyperchainStorage.sol"; +import {VerifierParams} from "../../../state-transition/chain-interfaces/IVerifier.sol"; +import {Diamond} from "../../libraries/Diamond.sol"; +import {PriorityQueue, PriorityOperation} from "../../../state-transition/libraries/PriorityQueue.sol"; +import {UncheckedMath} from "../../../common/libraries/UncheckedMath.sol"; +import {IGetters} from "../../chain-interfaces/IGetters.sol"; +import {ILegacyGetters} from "../../chain-interfaces/ILegacyGetters.sol"; +import {InvalidSelector} from "../../../common/L1ContractErrors.sol"; +import {SemVer} from "../../../common/libraries/SemVer.sol"; + +// While formally the following import is not used, it is needed to inherit documentation from it +import {IZkSyncHyperchainBase} from "../../chain-interfaces/IZkSyncHyperchainBase.sol"; + +/// @title Getters Contract implements functions for getting contract state from outside the blockchain. +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +contract GettersFacet is ZkSyncHyperchainBase, IGetters, ILegacyGetters { + using UncheckedMath for uint256; + using PriorityQueue for PriorityQueue.Queue; + + /// @inheritdoc IZkSyncHyperchainBase + string public constant override getName = "GettersFacet"; + + /*////////////////////////////////////////////////////////////// + CUSTOM GETTERS + //////////////////////////////////////////////////////////////*/ + + /// @inheritdoc IGetters + function getVerifier() external view returns (address) { + return address(s.verifier); + } + + /// @inheritdoc IGetters + function getAdmin() external view returns (address) { + return s.admin; + } + + /// @inheritdoc IGetters + function getPendingAdmin() external view returns (address) { + return s.pendingAdmin; + } + + /// @inheritdoc IGetters + function getBridgehub() external view returns (address) { + return s.bridgehub; + } + + /// @inheritdoc IGetters + function getStateTransitionManager() external view returns (address) { + return s.stateTransitionManager; + } + + /// @inheritdoc IGetters + function getBaseToken() external view returns (address) { + return s.baseToken; + } + + /// @inheritdoc IGetters + function getBaseTokenBridge() external view returns (address) { + return s.baseTokenBridge; + } + + /// @inheritdoc IGetters + function baseTokenGasPriceMultiplierNominator() external view returns (uint128) { + return s.baseTokenGasPriceMultiplierNominator; + } + + /// @inheritdoc IGetters + function baseTokenGasPriceMultiplierDenominator() external view returns (uint128) { + return s.baseTokenGasPriceMultiplierDenominator; + } + + /// @inheritdoc IGetters + function getTotalBatchesCommitted() external view returns (uint256) { + return s.totalBatchesCommitted; + } + + /// @inheritdoc IGetters + function getTotalBatchesVerified() external view returns (uint256) { + return s.totalBatchesVerified; + } + + /// @inheritdoc IGetters + function getTotalBatchesExecuted() external view returns (uint256) { + return s.totalBatchesExecuted; + } + + /// @inheritdoc IGetters + function getTotalPriorityTxs() external view returns (uint256) { + return s.priorityQueue.getTotalPriorityTxs(); + } + + /// @inheritdoc IGetters + function getFirstUnprocessedPriorityTx() external view returns (uint256) { + return s.priorityQueue.getFirstUnprocessedPriorityTx(); + } + + /// @inheritdoc IGetters + function getPriorityQueueSize() external view returns (uint256) { + return s.priorityQueue.getSize(); + } + + /// @inheritdoc IGetters + function priorityQueueFrontOperation() external view returns (PriorityOperation memory) { + return s.priorityQueue.front(); + } + + /// @inheritdoc IGetters + function isValidator(address _address) external view returns (bool) { + return s.validators[_address]; + } + + /// @inheritdoc IGetters + function l2LogsRootHash(uint256 _batchNumber) external view returns (bytes32) { + return s.l2LogsRootHashes[_batchNumber]; + } + + /// @inheritdoc IGetters + function storedBatchHash(uint256 _batchNumber) external view returns (bytes32) { + return s.storedBatchHashes[_batchNumber]; + } + + /// @inheritdoc IGetters + function getL2BootloaderBytecodeHash() external view returns (bytes32) { + return s.l2BootloaderBytecodeHash; + } + + /// @inheritdoc IGetters + function getL2DefaultAccountBytecodeHash() external view returns (bytes32) { + return s.l2DefaultAccountBytecodeHash; + } + + /// @inheritdoc IGetters + function getVerifierParams() external view returns (VerifierParams memory) { + return s.__DEPRECATED_verifierParams; + } + + /// @inheritdoc IGetters + function getProtocolVersion() external view returns (uint256) { + return s.protocolVersion; + } + + /// @inheritdoc IGetters + function getSemverProtocolVersion() external view returns (uint32, uint32, uint32) { + // slither-disable-next-line unused-return + return SemVer.unpackSemVer(SafeCast.toUint96(s.protocolVersion)); + } + + /// @inheritdoc IGetters + function getL2SystemContractsUpgradeTxHash() external view returns (bytes32) { + return s.l2SystemContractsUpgradeTxHash; + } + + /// @inheritdoc IGetters + function getL2SystemContractsUpgradeBatchNumber() external view returns (uint256) { + return s.l2SystemContractsUpgradeBatchNumber; + } + + /// @inheritdoc IGetters + function isDiamondStorageFrozen() external view returns (bool) { + Diamond.DiamondStorage storage ds = Diamond.getDiamondStorage(); + return ds.isFrozen; + } + + /// @inheritdoc IGetters + function isFacetFreezable(address _facet) external view returns (bool isFreezable) { + Diamond.DiamondStorage storage ds = Diamond.getDiamondStorage(); + + // There is no direct way to get whether the facet address is freezable, + // so we get it from one of the selectors that are associated with the facet. + uint256 selectorsArrayLen = ds.facetToSelectors[_facet].selectors.length; + if (selectorsArrayLen != 0) { + bytes4 selector0 = ds.facetToSelectors[_facet].selectors[0]; + isFreezable = ds.selectorToFacet[selector0].isFreezable; + } + } + + /// @inheritdoc IGetters + function getPriorityTxMaxGasLimit() external view returns (uint256) { + return s.priorityTxMaxGasLimit; + } + + /// @inheritdoc IGetters + function isFunctionFreezable(bytes4 _selector) external view returns (bool) { + Diamond.DiamondStorage storage ds = Diamond.getDiamondStorage(); + if (ds.selectorToFacet[_selector].facetAddress == address(0)) { + revert InvalidSelector(_selector); + } + return ds.selectorToFacet[_selector].isFreezable; + } + + /// @inheritdoc IGetters + function isEthWithdrawalFinalized(uint256 _l2BatchNumber, uint256 _l2MessageIndex) external view returns (bool) { + return s.isEthWithdrawalFinalized[_l2BatchNumber][_l2MessageIndex]; + } + + /// @inheritdoc IGetters + function getPubdataPricingMode() external view returns (PubdataPricingMode) { + return s.feeParams.pubdataPricingMode; + } + + /*////////////////////////////////////////////////////////////// + DIAMOND LOUPE + //////////////////////////////////////////////////////////////*/ + + /// @inheritdoc IGetters + function facets() external view returns (Facet[] memory result) { + Diamond.DiamondStorage storage ds = Diamond.getDiamondStorage(); + + uint256 facetsLen = ds.facets.length; + result = new Facet[](facetsLen); + + for (uint256 i = 0; i < facetsLen; i = i.uncheckedInc()) { + address facetAddr = ds.facets[i]; + Diamond.FacetToSelectors memory facetToSelectors = ds.facetToSelectors[facetAddr]; + + result[i] = Facet(facetAddr, facetToSelectors.selectors); + } + } + + /// @inheritdoc IGetters + function facetFunctionSelectors(address _facet) external view returns (bytes4[] memory) { + Diamond.DiamondStorage storage ds = Diamond.getDiamondStorage(); + return ds.facetToSelectors[_facet].selectors; + } + + /// @inheritdoc IGetters + function facetAddresses() external view returns (address[] memory) { + Diamond.DiamondStorage storage ds = Diamond.getDiamondStorage(); + return ds.facets; + } + + /// @inheritdoc IGetters + function facetAddress(bytes4 _selector) external view returns (address) { + Diamond.DiamondStorage storage ds = Diamond.getDiamondStorage(); + return ds.selectorToFacet[_selector].facetAddress; + } + + /*////////////////////////////////////////////////////////////// + DEPRECATED METHODS + //////////////////////////////////////////////////////////////*/ + + /// @inheritdoc ILegacyGetters + function getTotalBlocksCommitted() external view returns (uint256) { + return s.totalBatchesCommitted; + } + + /// @inheritdoc ILegacyGetters + function getTotalBlocksVerified() external view returns (uint256) { + return s.totalBatchesVerified; + } + + /// @inheritdoc ILegacyGetters + function getTotalBlocksExecuted() external view returns (uint256) { + return s.totalBatchesExecuted; + } + + /// @inheritdoc ILegacyGetters + function storedBlockHash(uint256 _batchNumber) external view returns (bytes32) { + return s.storedBatchHashes[_batchNumber]; + } + + /// @inheritdoc ILegacyGetters + function getL2SystemContractsUpgradeBlockNumber() external view returns (uint256) { + return s.l2SystemContractsUpgradeBatchNumber; + } +} diff --git a/contracts/l1-contracts/state-transition/chain-deps/facets/Mailbox.sol b/contracts/l1-contracts/state-transition/chain-deps/facets/Mailbox.sol new file mode 100644 index 0000000..43f6b04 --- /dev/null +++ b/contracts/l1-contracts/state-transition/chain-deps/facets/Mailbox.sol @@ -0,0 +1,390 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {Math} from "@openzeppelin/contracts-v4/utils/math/Math.sol"; + +import {IMailbox} from "../../chain-interfaces/IMailbox.sol"; +import {ITransactionFilterer} from "../../chain-interfaces/ITransactionFilterer.sol"; +import {Merkle} from "../../libraries/Merkle.sol"; +import {PriorityQueue, PriorityOperation} from "../../libraries/PriorityQueue.sol"; +import {TransactionValidator} from "../../libraries/TransactionValidator.sol"; +import {WritePriorityOpParams, L2CanonicalTransaction, L2Message, L2Log, TxStatus, BridgehubL2TransactionRequest} from "../../../common/Messaging.sol"; +import {FeeParams, PubdataPricingMode} from "../ZkSyncHyperchainStorage.sol"; +import {UncheckedMath} from "../../../common/libraries/UncheckedMath.sol"; +import {L2ContractHelper} from "../../../common/libraries/L2ContractHelper.sol"; +import {AddressAliasHelper} from "../../../vendor/AddressAliasHelper.sol"; +import {ZkSyncHyperchainBase} from "./ZkSyncHyperchainBase.sol"; +import {REQUIRED_L2_GAS_PRICE_PER_PUBDATA, ETH_TOKEN_ADDRESS, L1_GAS_PER_PUBDATA_BYTE, L2_L1_LOGS_TREE_DEFAULT_LEAF_HASH, PRIORITY_OPERATION_L2_TX_TYPE, PRIORITY_EXPIRATION, MAX_NEW_FACTORY_DEPS} from "../../../common/Config.sol"; +import {L2_BOOTLOADER_ADDRESS, L2_TO_L1_MESSENGER_SYSTEM_CONTRACT_ADDR} from "../../../common/L2ContractAddresses.sol"; + +import {IL1SharedBridge} from "../../../bridge/interfaces/IL1SharedBridge.sol"; + +import {OnlyEraSupported, BatchNotExecuted, HashedLogIsDefault, BaseTokenGasPriceDenominatorNotSet, TransactionNotAllowed, GasPerPubdataMismatch, TooManyFactoryDeps, MsgValueTooLow} from "../../../common/L1ContractErrors.sol"; + +// While formally the following import is not used, it is needed to inherit documentation from it +import {IZkSyncHyperchainBase} from "../../chain-interfaces/IZkSyncHyperchainBase.sol"; + +/// @title ZKsync Mailbox contract providing interfaces for L1 <-> L2 interaction. +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +contract MailboxFacet is ZkSyncHyperchainBase, IMailbox { + using UncheckedMath for uint256; + using PriorityQueue for PriorityQueue.Queue; + + /// @inheritdoc IZkSyncHyperchainBase + string public constant override getName = "MailboxFacet"; + + /// @dev Era's chainID + uint256 internal immutable ERA_CHAIN_ID; + + constructor(uint256 _eraChainId) { + ERA_CHAIN_ID = _eraChainId; + } + + /// @inheritdoc IMailbox + function transferEthToSharedBridge() external onlyBaseTokenBridge { + if (s.chainId != ERA_CHAIN_ID) { + revert OnlyEraSupported(); + } + + uint256 amount = address(this).balance; + address baseTokenBridgeAddress = s.baseTokenBridge; + IL1SharedBridge(baseTokenBridgeAddress).receiveEth{value: amount}(ERA_CHAIN_ID); + } + + /// @notice when requesting transactions through the bridgehub + function bridgehubRequestL2Transaction( + BridgehubL2TransactionRequest calldata _request + ) external onlyBridgehub returns (bytes32 canonicalTxHash) { + canonicalTxHash = _requestL2TransactionSender(_request); + } + + /// @inheritdoc IMailbox + function proveL2MessageInclusion( + uint256 _batchNumber, + uint256 _index, + L2Message calldata _message, + bytes32[] calldata _proof + ) public view returns (bool) { + return _proveL2LogInclusion(_batchNumber, _index, _L2MessageToLog(_message), _proof); + } + + /// @inheritdoc IMailbox + function proveL2LogInclusion( + uint256 _batchNumber, + uint256 _index, + L2Log calldata _log, + bytes32[] calldata _proof + ) external view returns (bool) { + return _proveL2LogInclusion(_batchNumber, _index, _log, _proof); + } + + /// @inheritdoc IMailbox + function proveL1ToL2TransactionStatus( + bytes32 _l2TxHash, + uint256 _l2BatchNumber, + uint256 _l2MessageIndex, + uint16 _l2TxNumberInBatch, + bytes32[] calldata _merkleProof, + TxStatus _status + ) public view returns (bool) { + // Bootloader sends an L2 -> L1 log only after processing the L1 -> L2 transaction. + // Thus, we can verify that the L1 -> L2 transaction was included in the L2 batch with specified status. + // + // The semantics of such L2 -> L1 log is always: + // - sender = L2_BOOTLOADER_ADDRESS + // - key = hash(L1ToL2Transaction) + // - value = status of the processing transaction (1 - success & 0 - 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) + // - txNumberInBatch = number of transaction in the batch + L2Log memory l2Log = L2Log({ + l2ShardId: 0, + isService: true, + txNumberInBatch: _l2TxNumberInBatch, + sender: L2_BOOTLOADER_ADDRESS, + key: _l2TxHash, + value: bytes32(uint256(_status)) + }); + return _proveL2LogInclusion(_l2BatchNumber, _l2MessageIndex, l2Log, _merkleProof); + } + + /// @dev Prove that a specific L2 log was sent in a specific L2 batch number + function _proveL2LogInclusion( + uint256 _batchNumber, + uint256 _index, + L2Log memory _log, + bytes32[] calldata _proof + ) internal view returns (bool) { + if (_batchNumber > s.totalBatchesExecuted) { + revert BatchNotExecuted(_batchNumber); + } + + bytes32 hashedLog = keccak256( + // solhint-disable-next-line func-named-parameters + abi.encodePacked(_log.l2ShardId, _log.isService, _log.txNumberInBatch, _log.sender, _log.key, _log.value) + ); + // Check that hashed log is not the default one, + // otherwise it means that the value is out of range of sent L2 -> L1 logs + if (hashedLog == L2_L1_LOGS_TREE_DEFAULT_LEAF_HASH) { + revert HashedLogIsDefault(); + } + + // It is ok to not check length of `_proof` array, as length + // of leaf preimage (which is `L2_TO_L1_LOG_SERIALIZE_SIZE`) is not + // equal to the length of other nodes preimages (which are `2 * 32`) + + bytes32 calculatedRootHash = Merkle.calculateRoot(_proof, _index, hashedLog); + bytes32 actualRootHash = s.l2LogsRootHashes[_batchNumber]; + + return actualRootHash == calculatedRootHash; + } + + /// @dev Convert arbitrary-length message to the raw l2 log + function _L2MessageToLog(L2Message calldata _message) internal pure returns (L2Log memory) { + return + L2Log({ + l2ShardId: 0, + isService: true, + txNumberInBatch: _message.txNumberInBatch, + sender: L2_TO_L1_MESSENGER_SYSTEM_CONTRACT_ADDR, + key: bytes32(uint256(uint160(_message.sender))), + value: keccak256(_message.data) + }); + } + + /// @inheritdoc IMailbox + function l2TransactionBaseCost( + uint256 _gasPrice, + uint256 _l2GasLimit, + uint256 _l2GasPerPubdataByteLimit + ) public view returns (uint256) { + uint256 l2GasPrice = _deriveL2GasPrice(_gasPrice, _l2GasPerPubdataByteLimit); + return l2GasPrice * _l2GasLimit; + } + + /// @notice Derives the price for L2 gas in base token to be paid. + /// @param _l1GasPrice The gas price on L1 + /// @param _gasPerPubdata The price for each pubdata byte in L2 gas + /// @return The price of L2 gas in the base token + function _deriveL2GasPrice(uint256 _l1GasPrice, uint256 _gasPerPubdata) internal view returns (uint256) { + FeeParams memory feeParams = s.feeParams; + if (s.baseTokenGasPriceMultiplierDenominator == 0) { + revert BaseTokenGasPriceDenominatorNotSet(); + } + uint256 l1GasPriceConverted = (_l1GasPrice * s.baseTokenGasPriceMultiplierNominator) / + s.baseTokenGasPriceMultiplierDenominator; + uint256 pubdataPriceBaseToken; + if (feeParams.pubdataPricingMode == PubdataPricingMode.Rollup) { + // slither-disable-next-line divide-before-multiply + pubdataPriceBaseToken = L1_GAS_PER_PUBDATA_BYTE * l1GasPriceConverted; + } + + // slither-disable-next-line divide-before-multiply + uint256 batchOverheadBaseToken = uint256(feeParams.batchOverheadL1Gas) * l1GasPriceConverted; + uint256 fullPubdataPriceBaseToken = pubdataPriceBaseToken + + batchOverheadBaseToken / + uint256(feeParams.maxPubdataPerBatch); + + uint256 l2GasPrice = feeParams.minimalL2GasPrice + batchOverheadBaseToken / uint256(feeParams.maxL2GasPerBatch); + uint256 minL2GasPriceBaseToken = (fullPubdataPriceBaseToken + _gasPerPubdata - 1) / _gasPerPubdata; + + return Math.max(l2GasPrice, minL2GasPriceBaseToken); + } + + /// @inheritdoc IMailbox + function finalizeEthWithdrawal( + uint256 _l2BatchNumber, + uint256 _l2MessageIndex, + uint16 _l2TxNumberInBatch, + bytes calldata _message, + bytes32[] calldata _merkleProof + ) external nonReentrant { + if (s.chainId != ERA_CHAIN_ID) { + revert OnlyEraSupported(); + } + IL1SharedBridge(s.baseTokenBridge).finalizeWithdrawal({ + _chainId: ERA_CHAIN_ID, + _l2BatchNumber: _l2BatchNumber, + _l2MessageIndex: _l2MessageIndex, + _l2TxNumberInBatch: _l2TxNumberInBatch, + _message: _message, + _merkleProof: _merkleProof + }); + } + + /// @inheritdoc IMailbox + function requestL2Transaction( + address _contractL2, + uint256 _l2Value, + bytes calldata _calldata, + uint256 _l2GasLimit, + uint256 _l2GasPerPubdataByteLimit, + bytes[] calldata _factoryDeps, + address _refundRecipient + ) external payable returns (bytes32 canonicalTxHash) { + if (s.chainId != ERA_CHAIN_ID) { + revert OnlyEraSupported(); + } + canonicalTxHash = _requestL2TransactionSender( + BridgehubL2TransactionRequest({ + sender: msg.sender, + contractL2: _contractL2, + mintValue: msg.value, + l2Value: _l2Value, + l2GasLimit: _l2GasLimit, + l2Calldata: _calldata, + l2GasPerPubdataByteLimit: _l2GasPerPubdataByteLimit, + factoryDeps: _factoryDeps, + refundRecipient: _refundRecipient + }) + ); + IL1SharedBridge(s.baseTokenBridge).bridgehubDepositBaseToken{value: msg.value}( + s.chainId, + msg.sender, + ETH_TOKEN_ADDRESS, + msg.value + ); + } + + function _requestL2TransactionSender( + BridgehubL2TransactionRequest memory _request + ) internal nonReentrant returns (bytes32 canonicalTxHash) { + // Check that the transaction is allowed by the filterer (if the filterer is set). + if (s.transactionFilterer != address(0)) { + if ( + !ITransactionFilterer(s.transactionFilterer).isTransactionAllowed({ + sender: _request.sender, + contractL2: _request.contractL2, + mintValue: _request.mintValue, + l2Value: _request.l2Value, + l2Calldata: _request.l2Calldata, + refundRecipient: _request.refundRecipient + }) + ) { + revert TransactionNotAllowed(); + } + } + + // Enforcing that `_request.l2GasPerPubdataByteLimit` equals to a certain constant number. This is needed + // to ensure that users do not get used to using "exotic" numbers for _request.l2GasPerPubdataByteLimit, e.g. 1-2, etc. + // VERY IMPORTANT: nobody should rely on this constant to be fixed and every contract should give their users the ability to provide the + // ability to provide `_request.l2GasPerPubdataByteLimit` for each independent transaction. + // CHANGING THIS CONSTANT SHOULD BE A CLIENT-SIDE CHANGE. + if (_request.l2GasPerPubdataByteLimit != REQUIRED_L2_GAS_PRICE_PER_PUBDATA) { + revert GasPerPubdataMismatch(); + } + + WritePriorityOpParams memory params; + params.request = _request; + + canonicalTxHash = _requestL2Transaction(params); + } + + function _requestL2Transaction(WritePriorityOpParams memory _params) internal returns (bytes32 canonicalTxHash) { + BridgehubL2TransactionRequest memory request = _params.request; + + if (request.factoryDeps.length > MAX_NEW_FACTORY_DEPS) { + revert TooManyFactoryDeps(); + } + _params.txId = s.priorityQueue.getTotalPriorityTxs(); + + // Checking that the user provided enough ether to pay for the transaction. + _params.l2GasPrice = _deriveL2GasPrice(tx.gasprice, request.l2GasPerPubdataByteLimit); + uint256 baseCost = _params.l2GasPrice * request.l2GasLimit; + if (request.mintValue < baseCost + request.l2Value) { + revert MsgValueTooLow(baseCost + request.l2Value, request.mintValue); + } + + request.refundRecipient = AddressAliasHelper.actualRefundRecipient(request.refundRecipient, request.sender); + // Change the sender address if it is a smart contract to prevent address collision between L1 and L2. + // Please note, currently ZKsync address derivation is different from Ethereum one, but it may be changed in the future. + // solhint-disable avoid-tx-origin + // slither-disable-next-line tx-origin + if (request.sender != tx.origin) { + request.sender = AddressAliasHelper.applyL1ToL2Alias(request.sender); + } + // solhint-enable avoid-tx-origin + + // populate missing fields + _params.expirationTimestamp = uint64(block.timestamp + PRIORITY_EXPIRATION); // Safe to cast + + canonicalTxHash = _writePriorityOp(_params); + } + + function _serializeL2Transaction( + WritePriorityOpParams memory _priorityOpParams + ) internal pure returns (L2CanonicalTransaction memory transaction) { + BridgehubL2TransactionRequest memory request = _priorityOpParams.request; + transaction = L2CanonicalTransaction({ + txType: PRIORITY_OPERATION_L2_TX_TYPE, + from: uint256(uint160(request.sender)), + to: uint256(uint160(request.contractL2)), + gasLimit: request.l2GasLimit, + gasPerPubdataByteLimit: request.l2GasPerPubdataByteLimit, + maxFeePerGas: uint256(_priorityOpParams.l2GasPrice), + maxPriorityFeePerGas: uint256(0), + paymaster: uint256(0), + // Note, that the priority operation id is used as "nonce" for L1->L2 transactions + nonce: uint256(_priorityOpParams.txId), + value: request.l2Value, + reserved: [request.mintValue, uint256(uint160(request.refundRecipient)), 0, 0], + data: request.l2Calldata, + signature: new bytes(0), + factoryDeps: _hashFactoryDeps(request.factoryDeps), + paymasterInput: new bytes(0), + reservedDynamic: new bytes(0) + }); + } + + /// @notice Stores a transaction record in storage & send event about that + function _writePriorityOp( + WritePriorityOpParams memory _priorityOpParams + ) internal returns (bytes32 canonicalTxHash) { + L2CanonicalTransaction memory transaction = _serializeL2Transaction(_priorityOpParams); + + bytes memory transactionEncoding = abi.encode(transaction); + + TransactionValidator.validateL1ToL2Transaction( + transaction, + transactionEncoding, + s.priorityTxMaxGasLimit, + s.feeParams.priorityTxMaxPubdata + ); + + canonicalTxHash = keccak256(transactionEncoding); + + s.priorityQueue.pushBack( + PriorityOperation({ + canonicalTxHash: canonicalTxHash, + expirationTimestamp: _priorityOpParams.expirationTimestamp, + layer2Tip: uint192(0) // TODO: Restore after fee modeling will be stable. (SMA-1230) + }) + ); + + // Data that is needed for the operator to simulate priority queue offchain + // solhint-disable-next-line func-named-parameters + emit NewPriorityRequest( + _priorityOpParams.txId, + canonicalTxHash, + _priorityOpParams.expirationTimestamp, + transaction, + _priorityOpParams.request.factoryDeps + ); + } + + /// @notice Hashes the L2 bytecodes and returns them in the format in which they are processed by the bootloader + function _hashFactoryDeps(bytes[] memory _factoryDeps) internal pure returns (uint256[] memory hashedFactoryDeps) { + uint256 factoryDepsLen = _factoryDeps.length; + hashedFactoryDeps = new uint256[](factoryDepsLen); + for (uint256 i = 0; i < factoryDepsLen; i = i.uncheckedInc()) { + bytes32 hashedBytecode = L2ContractHelper.hashL2Bytecode(_factoryDeps[i]); + + // Store the resulting hash sequentially in bytes. + assembly { + mstore(add(hashedFactoryDeps, mul(add(i, 1), 32)), hashedBytecode) + } + } + } +} diff --git a/contracts/l1-contracts/state-transition/chain-deps/facets/ZkSyncHyperchainBase.sol b/contracts/l1-contracts/state-transition/chain-deps/facets/ZkSyncHyperchainBase.sol new file mode 100644 index 0000000..0910fca --- /dev/null +++ b/contracts/l1-contracts/state-transition/chain-deps/facets/ZkSyncHyperchainBase.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {ZkSyncHyperchainStorage} from "../ZkSyncHyperchainStorage.sol"; +import {ReentrancyGuard} from "../../../common/ReentrancyGuard.sol"; + +import {Unauthorized} from "../../../common/L1ContractErrors.sol"; + +/// @title Base contract containing functions accessible to the other facets. +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +contract ZkSyncHyperchainBase is ReentrancyGuard { + // slither-disable-next-line uninitialized-state + ZkSyncHyperchainStorage internal s; + + /// @notice Checks that the message sender is an active admin + modifier onlyAdmin() { + if (msg.sender != s.admin) { + revert Unauthorized(msg.sender); + } + _; + } + + /// @notice Checks if validator is active + modifier onlyValidator() { + if (!s.validators[msg.sender]) { + revert Unauthorized(msg.sender); + } + _; + } + + modifier onlyStateTransitionManager() { + if (msg.sender != s.stateTransitionManager) { + revert Unauthorized(msg.sender); + } + _; + } + + modifier onlyBridgehub() { + if (msg.sender != s.bridgehub) { + revert Unauthorized(msg.sender); + } + _; + } + + modifier onlyAdminOrStateTransitionManager() { + if (msg.sender != s.admin && msg.sender != s.stateTransitionManager) { + revert Unauthorized(msg.sender); + } + _; + } + + modifier onlyValidatorOrStateTransitionManager() { + if (!s.validators[msg.sender] && msg.sender != s.stateTransitionManager) { + revert Unauthorized(msg.sender); + } + _; + } + + modifier onlyBaseTokenBridge() { + if (msg.sender != s.baseTokenBridge) { + revert Unauthorized(msg.sender); + } + _; + } +} diff --git a/contracts/l1-contracts/state-transition/chain-interfaces/IAdmin.sol b/contracts/l1-contracts/state-transition/chain-interfaces/IAdmin.sol new file mode 100644 index 0000000..643c611 --- /dev/null +++ b/contracts/l1-contracts/state-transition/chain-interfaces/IAdmin.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.21; + +import {IZkSyncHyperchainBase} from "../chain-interfaces/IZkSyncHyperchainBase.sol"; + +import {Diamond} from "../libraries/Diamond.sol"; +import {FeeParams, PubdataPricingMode} from "../chain-deps/ZkSyncHyperchainStorage.sol"; + +/// @title The interface of the Admin Contract that controls access rights for contract management. +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +interface IAdmin is IZkSyncHyperchainBase { + /// @notice Starts the transfer of admin rights. Only the current admin can propose a new pending one. + /// @notice New admin can accept admin rights by calling `acceptAdmin` function. + /// @param _newPendingAdmin Address of the new admin + function setPendingAdmin(address _newPendingAdmin) external; + + /// @notice Accepts transfer of admin rights. Only pending admin can accept the role. + function acceptAdmin() external; + + /// @notice Change validator status (active or not active) + /// @param _validator Validator address + /// @param _active Active flag + function setValidator(address _validator, bool _active) external; + + /// @notice Change zk porter availability + /// @param _zkPorterIsAvailable The availability of zk porter shard + function setPorterAvailability(bool _zkPorterIsAvailable) external; + + /// @notice Change the max L2 gas limit for L1 -> L2 transactions + /// @param _newPriorityTxMaxGasLimit The maximum number of L2 gas that a user can request for L1 -> L2 transactions + function setPriorityTxMaxGasLimit(uint256 _newPriorityTxMaxGasLimit) external; + + /// @notice Change the fee params for L1->L2 transactions + /// @param _newFeeParams The new fee params + function changeFeeParams(FeeParams calldata _newFeeParams) external; + + /// @notice Change the token multiplier for L1->L2 transactions + function setTokenMultiplier(uint128 _nominator, uint128 _denominator) external; + + /// @notice Change the pubdata pricing mode before the first batch is processed + /// @param _pricingMode The new pubdata pricing mode + function setPubdataPricingMode(PubdataPricingMode _pricingMode) external; + + /// @notice Set the transaction filterer + function setTransactionFilterer(address _transactionFilterer) external; + + /// @notice Perform the upgrade from the current protocol version with the corresponding upgrade data + /// @param _protocolVersion The current protocol version from which upgrade is executed + /// @param _cutData The diamond cut parameters that is executed in the upgrade + function upgradeChainFromVersion(uint256 _protocolVersion, Diamond.DiamondCutData calldata _cutData) external; + + /// @notice Executes a proposed governor upgrade + /// @dev Only the current admin can execute the upgrade + /// @param _diamondCut The diamond cut parameters to be executed + function executeUpgrade(Diamond.DiamondCutData calldata _diamondCut) external; + + /// @notice Instantly pause the functionality of all freezable facets & their selectors + /// @dev Only the governance mechanism may freeze Diamond Proxy + function freezeDiamond() external; + + /// @notice Unpause the functionality of all freezable facets & their selectors + /// @dev Both the admin and the STM can unfreeze Diamond Proxy + function unfreezeDiamond() external; + + /// @notice Porter availability status changes + event IsPorterAvailableStatusUpdate(bool isPorterAvailable); + + /// @notice Validator's status changed + event ValidatorStatusUpdate(address indexed validatorAddress, bool isActive); + + /// @notice pendingAdmin is changed + /// @dev Also emitted when new admin is accepted and in this case, `newPendingAdmin` would be zero address + event NewPendingAdmin(address indexed oldPendingAdmin, address indexed newPendingAdmin); + + /// @notice Admin changed + event NewAdmin(address indexed oldAdmin, address indexed newAdmin); + + /// @notice Priority transaction max L2 gas limit changed + event NewPriorityTxMaxGasLimit(uint256 oldPriorityTxMaxGasLimit, uint256 newPriorityTxMaxGasLimit); + + /// @notice Fee params for L1->L2 transactions changed + event NewFeeParams(FeeParams oldFeeParams, FeeParams newFeeParams); + + /// @notice Validium mode status changed + event ValidiumModeStatusUpdate(PubdataPricingMode validiumMode); + + /// @notice The transaction filterer has been updated + event NewTransactionFilterer(address oldTransactionFilterer, address newTransactionFilterer); + + /// @notice BaseToken multiplier for L1->L2 transactions changed + event NewBaseTokenMultiplier( + uint128 oldNominator, + uint128 oldDenominator, + uint128 newNominator, + uint128 newDenominator + ); + + /// @notice Emitted when an upgrade is executed. + event ExecuteUpgrade(Diamond.DiamondCutData diamondCut); + + /// @notice Emitted when the contract is frozen. + event Freeze(); + + /// @notice Emitted when the contract is unfrozen. + event Unfreeze(); +} diff --git a/contracts/l1-contracts/state-transition/chain-interfaces/IDiamondInit.sol b/contracts/l1-contracts/state-transition/chain-interfaces/IDiamondInit.sol new file mode 100644 index 0000000..189ae69 --- /dev/null +++ b/contracts/l1-contracts/state-transition/chain-interfaces/IDiamondInit.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.21; + +import {IVerifier, VerifierParams} from "./IVerifier.sol"; +import {FeeParams} from "../chain-deps/ZkSyncHyperchainStorage.sol"; + +/// @param chainId the id of the chain +/// @param bridgehub the address of the bridgehub contract +/// @param stateTransitionManager contract's address +/// @param protocolVersion initial protocol version +/// @param validatorTimelock address of the validator timelock that delays execution +/// @param admin address who can manage the contract +/// @param baseToken address of the base token of the chain +/// @param baseTokenBridge address of the L1 shared bridge contract +/// @param storedBatchZero hash of the initial genesis batch +/// @param verifier address of Verifier contract +/// @param verifierParams Verifier config parameters that describes the circuit to be verified +/// @param l2BootloaderBytecodeHash The hash of bootloader L2 bytecode +/// @param l2DefaultAccountBytecodeHash The hash of default account L2 bytecode +/// @param priorityTxMaxGasLimit maximum number of the L2 gas that a user can request for L1 -> L2 transactions +/// @param feeParams Fee parameters to be used for L1->L2 transactions +/// @param blobVersionedHashRetriever Address of contract used to pull the blob versioned hash for a transaction. +// solhint-disable-next-line gas-struct-packing +struct InitializeData { + uint256 chainId; + address bridgehub; + address stateTransitionManager; + uint256 protocolVersion; + address admin; + address validatorTimelock; + address baseToken; + address baseTokenBridge; + bytes32 storedBatchZero; + IVerifier verifier; + VerifierParams verifierParams; + bytes32 l2BootloaderBytecodeHash; + bytes32 l2DefaultAccountBytecodeHash; + uint256 priorityTxMaxGasLimit; + FeeParams feeParams; + address blobVersionedHashRetriever; +} + +/// @param verifier address of Verifier contract +/// @param verifierParams Verifier config parameters that describes the circuit to be verified +/// @param l2BootloaderBytecodeHash The hash of bootloader L2 bytecode +/// @param l2DefaultAccountBytecodeHash The hash of default account L2 bytecode +/// @param priorityTxMaxGasLimit maximum number of the L2 gas that a user can request for L1 -> L2 transactions +/// @param feeParams Fee parameters to be used for L1->L2 transactions +/// @param blobVersionedHashRetriever Address of contract used to pull the blob versioned hash for a transaction. +struct InitializeDataNewChain { + IVerifier verifier; + VerifierParams verifierParams; + bytes32 l2BootloaderBytecodeHash; + bytes32 l2DefaultAccountBytecodeHash; + uint256 priorityTxMaxGasLimit; + FeeParams feeParams; + address blobVersionedHashRetriever; +} + +interface IDiamondInit { + function initialize(InitializeData calldata _initData) external returns (bytes32); +} diff --git a/contracts/l1-contracts/state-transition/chain-interfaces/IExecutor.sol b/contracts/l1-contracts/state-transition/chain-interfaces/IExecutor.sol new file mode 100644 index 0000000..d74e428 --- /dev/null +++ b/contracts/l1-contracts/state-transition/chain-interfaces/IExecutor.sol @@ -0,0 +1,221 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.21; + +import {IZkSyncHyperchainBase} from "./IZkSyncHyperchainBase.sol"; + +/// @dev Enum used by L2 System Contracts to differentiate logs. +enum SystemLogKey { + L2_TO_L1_LOGS_TREE_ROOT_KEY, + TOTAL_L2_TO_L1_PUBDATA_KEY, + STATE_DIFF_HASH_KEY, + PACKED_BATCH_AND_L2_BLOCK_TIMESTAMP_KEY, + PREV_BATCH_HASH_KEY, + CHAINED_PRIORITY_TXN_HASH_KEY, + NUMBER_OF_LAYER_1_TXS_KEY, + BLOB_ONE_HASH_KEY, + BLOB_TWO_HASH_KEY, + BLOB_THREE_HASH_KEY, + BLOB_FOUR_HASH_KEY, + BLOB_FIVE_HASH_KEY, + BLOB_SIX_HASH_KEY, + EXPECTED_SYSTEM_CONTRACT_UPGRADE_TX_HASH_KEY +} + +/// @dev Enum used to determine the source of pubdata. At first we will support calldata and blobs but this can be extended. +enum PubdataSource { + Calldata, + Blob +} + +struct LogProcessingOutput { + uint256 numberOfLayer1Txs; + bytes32 chainedPriorityTxsHash; + bytes32 previousBatchHash; + bytes32 pubdataHash; + bytes32 stateDiffHash; + bytes32 l2LogsTreeRoot; + uint256 packedBatchAndL2BlockTimestamp; + bytes32[] blobHashes; +} + +/// @dev Total number of bytes in a blob. Blob = 4096 field elements * 31 bytes per field element +/// @dev EIP-4844 defines it as 131_072 but we use 4096 * 31 within our circuits to always fit within a field element +/// @dev Our circuits will prove that a EIP-4844 blob and our internal blob are the same. +uint256 constant BLOB_SIZE_BYTES = 126_976; + +/// @dev Offset used to pull Address From Log. Equal to 4 (bytes for isService) +uint256 constant L2_LOG_ADDRESS_OFFSET = 4; + +/// @dev Offset used to pull Key From Log. Equal to 4 (bytes for isService) + 20 (bytes for address) +uint256 constant L2_LOG_KEY_OFFSET = 24; + +/// @dev Offset used to pull Value From Log. Equal to 4 (bytes for isService) + 20 (bytes for address) + 32 (bytes for key) +uint256 constant L2_LOG_VALUE_OFFSET = 56; + +/// @dev BLS Modulus value defined in EIP-4844 and the magic value returned from a successful call to the +/// point evaluation precompile +uint256 constant BLS_MODULUS = 52435875175126190479447740508185965837690552500527637822603658699938581184513; + +/// @dev Packed pubdata commitments. +/// @dev Format: list of: opening point (16 bytes) || claimed value (32 bytes) || commitment (48 bytes) || proof (48 bytes)) = 144 bytes +uint256 constant PUBDATA_COMMITMENT_SIZE = 144; + +/// @dev Offset in pubdata commitment of blobs for claimed value +uint256 constant PUBDATA_COMMITMENT_CLAIMED_VALUE_OFFSET = 16; + +/// @dev Offset in pubdata commitment of blobs for kzg commitment +uint256 constant PUBDATA_COMMITMENT_COMMITMENT_OFFSET = 48; + +/// @dev Max number of blobs currently supported +uint256 constant MAX_NUMBER_OF_BLOBS = 6; + +/// @dev The number of blobs that must be present in the commitment to a batch. +/// It represents the maximal number of blobs that circuits can support and can be larger +/// than the maximal number of blobs supported by the contract (`MAX_NUMBER_OF_BLOBS`). +uint256 constant TOTAL_BLOBS_IN_COMMITMENT = 16; + +/// @title The interface of the ZKsync Executor contract capable of processing events emitted in the ZKsync protocol. +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +interface IExecutor is IZkSyncHyperchainBase { + /// @notice Rollup batch stored data + /// @param batchNumber Rollup batch number + /// @param batchHash Hash of L2 batch + /// @param indexRepeatedStorageChanges The serial number of the shortcut index that's used as a unique identifier for storage keys that were used twice or more + /// @param numberOfLayer1Txs Number of priority operations to be processed + /// @param priorityOperationsHash Hash of all priority operations from this batch + /// @param l2LogsTreeRoot Root hash of tree that contains L2 -> L1 messages from this batch + /// @param timestamp Rollup batch timestamp, have the same format as Ethereum batch constant + /// @param commitment Verified input for the ZKsync circuit + // solhint-disable-next-line gas-struct-packing + struct StoredBatchInfo { + uint64 batchNumber; + bytes32 batchHash; + uint64 indexRepeatedStorageChanges; + uint256 numberOfLayer1Txs; + bytes32 priorityOperationsHash; + bytes32 l2LogsTreeRoot; + uint256 timestamp; + bytes32 commitment; + } + + /// @notice Data needed to commit new batch + /// @param batchNumber Number of the committed batch + /// @param timestamp Unix timestamp denoting the start of the batch execution + /// @param indexRepeatedStorageChanges The serial number of the shortcut index that's used as a unique identifier for storage keys that were used twice or more + /// @param newStateRoot The state root of the full state tree + /// @param numberOfLayer1Txs Number of priority operations to be processed + /// @param priorityOperationsHash Hash of all priority operations from this batch + /// @param bootloaderHeapInitialContentsHash Hash of the initial contents of the bootloader heap. In practice it serves as the commitment to the transactions in the batch. + /// @param eventsQueueStateHash Hash of the events queue state. In practice it serves as the commitment to the events in the batch. + /// @param systemLogs concatenation of all L2 -> L1 system logs in the batch + /// @param pubdataCommitments Packed pubdata commitments/data. + /// @dev pubdataCommitments format: This will always start with a 1 byte pubdataSource flag. Current allowed values are 0 (calldata) or 1 (blobs) + /// kzg: list of: opening point (16 bytes) || claimed value (32 bytes) || commitment (48 bytes) || proof (48 bytes) = 144 bytes + /// calldata: pubdataCommitments.length - 1 - 32 bytes of pubdata + /// and 32 bytes appended to serve as the blob commitment part for the aux output part of the batch commitment + /// @dev For 2 blobs we will be sending 288 bytes of calldata instead of the full amount for pubdata. + /// @dev When using calldata, we only need to send one blob commitment since the max number of bytes in calldata fits in a single blob and we can pull the + /// linear hash from the system logs + struct CommitBatchInfo { + uint64 batchNumber; + uint64 timestamp; + uint64 indexRepeatedStorageChanges; + bytes32 newStateRoot; + uint256 numberOfLayer1Txs; + bytes32 priorityOperationsHash; + bytes32 bootloaderHeapInitialContentsHash; + bytes32 eventsQueueStateHash; + bytes systemLogs; + bytes pubdataCommitments; + } + + /// @notice Recursive proof input data (individual commitments are constructed onchain) + struct ProofInput { + uint256[] recursiveAggregationInput; + uint256[] serializedProof; + } + + /// @notice Function called by the operator to commit new batches. It is responsible for: + /// - Verifying the correctness of their timestamps. + /// - Processing their L2->L1 logs. + /// - Storing batch commitments. + /// @param _lastCommittedBatchData Stored data of the last committed batch. + /// @param _newBatchesData Data of the new batches to be committed. + function commitBatches( + StoredBatchInfo calldata _lastCommittedBatchData, + CommitBatchInfo[] calldata _newBatchesData + ) external; + + /// @notice same as `commitBatches` but with the chainId so ValidatorTimelock can sort the inputs. + function commitBatchesSharedBridge( + uint256 _chainId, + StoredBatchInfo calldata _lastCommittedBatchData, + CommitBatchInfo[] calldata _newBatchesData + ) external; + + /// @notice Batches commitment verification. + /// @dev Only verifies batch commitments without any other processing. + /// @param _prevBatch Stored data of the last committed batch. + /// @param _committedBatches Stored data of the committed batches. + /// @param _proof The zero knowledge proof. + function proveBatches( + StoredBatchInfo calldata _prevBatch, + StoredBatchInfo[] calldata _committedBatches, + ProofInput calldata _proof + ) external; + + /// @notice same as `proveBatches` but with the chainId so ValidatorTimelock can sort the inputs. + function proveBatchesSharedBridge( + uint256 _chainId, + StoredBatchInfo calldata _prevBatch, + StoredBatchInfo[] calldata _committedBatches, + ProofInput calldata _proof + ) external; + + /// @notice The function called by the operator to finalize (execute) batches. It is responsible for: + /// - Processing all pending operations (commpleting priority requests). + /// - Finalizing this batch (i.e. allowing to withdraw funds from the system) + /// @param _batchesData Data of the batches to be executed. + function executeBatches(StoredBatchInfo[] calldata _batchesData) external; + + /// @notice same as `executeBatches` but with the chainId so ValidatorTimelock can sort the inputs. + function executeBatchesSharedBridge(uint256 _chainId, StoredBatchInfo[] calldata _batchesData) external; + + /// @notice Reverts unexecuted batches + /// @param _newLastBatch batch number after which batches should be reverted + /// NOTE: Doesn't delete the stored data about batches, but only decreases + /// counters that are responsible for the number of batches + function revertBatches(uint256 _newLastBatch) external; + + /// @notice same as `revertBatches` but with the chainId so ValidatorTimelock can sort the inputs. + function revertBatchesSharedBridge(uint256 _chainId, uint256 _newLastBatch) external; + + /// @notice Event emitted when a batch is committed + /// @param batchNumber Number of the batch committed + /// @param batchHash Hash of the L2 batch + /// @param commitment Calculated input for the ZKsync circuit + /// @dev It has the name "BlockCommit" and not "BatchCommit" due to backward compatibility considerations + event BlockCommit(uint256 indexed batchNumber, bytes32 indexed batchHash, bytes32 indexed commitment); + + /// @notice Event emitted when batches are verified + /// @param previousLastVerifiedBatch Batch number of the previous last verified batch + /// @param currentLastVerifiedBatch Batch number of the current last verified batch + /// @dev It has the name "BlocksVerification" and not "BatchesVerification" due to backward compatibility considerations + event BlocksVerification(uint256 indexed previousLastVerifiedBatch, uint256 indexed currentLastVerifiedBatch); + + /// @notice Event emitted when a batch is executed + /// @param batchNumber Number of the batch executed + /// @param batchHash Hash of the L2 batch + /// @param commitment Verified input for the ZKsync circuit + /// @dev It has the name "BlockExecution" and not "BatchExecution" due to backward compatibility considerations + event BlockExecution(uint256 indexed batchNumber, bytes32 indexed batchHash, bytes32 indexed commitment); + + /// @notice Event emitted when batches are reverted + /// @param totalBatchesCommitted Total number of committed batches after the revert + /// @param totalBatchesVerified Total number of verified batches after the revert + /// @param totalBatchesExecuted Total number of executed batches + /// @dev It has the name "BlocksRevert" and not "BatchesRevert" due to backward compatibility considerations + event BlocksRevert(uint256 totalBatchesCommitted, uint256 totalBatchesVerified, uint256 totalBatchesExecuted); +} diff --git a/contracts/l1-contracts/state-transition/chain-interfaces/IGetters.sol b/contracts/l1-contracts/state-transition/chain-interfaces/IGetters.sol new file mode 100644 index 0000000..4d06f9e --- /dev/null +++ b/contracts/l1-contracts/state-transition/chain-interfaces/IGetters.sol @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.21; + +import {PriorityOperation} from "../libraries/PriorityQueue.sol"; +import {VerifierParams} from "../chain-interfaces/IVerifier.sol"; +import {PubdataPricingMode} from "../chain-deps/ZkSyncHyperchainStorage.sol"; +import {IZkSyncHyperchainBase} from "./IZkSyncHyperchainBase.sol"; + +/// @title The interface of the Getters Contract that implements functions for getting contract state from outside the blockchain. +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +interface IGetters is IZkSyncHyperchainBase { + /*////////////////////////////////////////////////////////////// + CUSTOM GETTERS + //////////////////////////////////////////////////////////////*/ + + /// @return The address of the verifier smart contract + function getVerifier() external view returns (address); + + /// @return The address of the current admin + function getAdmin() external view returns (address); + + /// @return The address of the pending admin + function getPendingAdmin() external view returns (address); + + /// @return The address of the bridgehub + function getBridgehub() external view returns (address); + + /// @return The address of the state transition + function getStateTransitionManager() external view returns (address); + + /// @return The address of the base token + function getBaseToken() external view returns (address); + + /// @return The address of the base token bridge + function getBaseTokenBridge() external view returns (address); + + /// @return The total number of batches that were committed + function getTotalBatchesCommitted() external view returns (uint256); + + /// @return The total number of batches that were committed & verified + function getTotalBatchesVerified() external view returns (uint256); + + /// @return The total number of batches that were committed & verified & executed + function getTotalBatchesExecuted() external view returns (uint256); + + /// @return The total number of priority operations that were added to the priority queue, including all processed ones + function getTotalPriorityTxs() external view returns (uint256); + + /// @notice The function that returns the first unprocessed priority transaction. + /// @dev Returns zero if and only if no operations were processed from the queue. + /// @dev If all the transactions were processed, it will return the last processed index, so + /// in case exactly *unprocessed* transactions are needed, one should check that getPriorityQueueSize() is greater than 0. + /// @return Index of the oldest priority operation that wasn't processed yet + function getFirstUnprocessedPriorityTx() external view returns (uint256); + + /// @return The number of priority operations currently in the queue + function getPriorityQueueSize() external view returns (uint256); + + /// @return The first unprocessed priority operation from the queue + function priorityQueueFrontOperation() external view returns (PriorityOperation memory); + + /// @return Whether the address has a validator access + function isValidator(address _address) external view returns (bool); + + /// @return merkleRoot Merkle root of the tree with L2 logs for the selected batch + function l2LogsRootHash(uint256 _batchNumber) external view returns (bytes32 merkleRoot); + + /// @notice For unfinalized (non executed) batches may change + /// @dev returns zero for non-committed batches + /// @return The hash of committed L2 batch. + function storedBatchHash(uint256 _batchNumber) external view returns (bytes32); + + /// @return Bytecode hash of bootloader program. + function getL2BootloaderBytecodeHash() external view returns (bytes32); + + /// @return Bytecode hash of default account (bytecode for EOA). + function getL2DefaultAccountBytecodeHash() external view returns (bytes32); + + /// @return Verifier parameters. + /// @dev This function is deprecated and will soon be removed. + function getVerifierParams() external view returns (VerifierParams memory); + + /// @return Whether the diamond is frozen or not + function isDiamondStorageFrozen() external view returns (bool); + + /// @return The current packed protocol version. To access human-readable version, use `getSemverProtocolVersion` function. + function getProtocolVersion() external view returns (uint256); + + /// @return The tuple of (major, minor, patch) protocol version. + function getSemverProtocolVersion() external view returns (uint32, uint32, uint32); + + /// @return The upgrade system contract transaction hash, 0 if the upgrade is not initialized + function getL2SystemContractsUpgradeTxHash() external view returns (bytes32); + + /// @return The L2 batch number in which the upgrade transaction was processed. + /// @dev It is equal to 0 in the following two cases: + /// - No upgrade transaction has ever been processed. + /// - The upgrade transaction has been processed and the batch with such transaction has been + /// executed (i.e. finalized). + function getL2SystemContractsUpgradeBatchNumber() external view returns (uint256); + + /// @return The maximum number of L2 gas that a user can request for L1 -> L2 transactions + function getPriorityTxMaxGasLimit() external view returns (uint256); + + /// @return Whether a withdrawal has been finalized. + /// @param _l2BatchNumber The L2 batch number within which the withdrawal happened. + /// @param _l2MessageIndex The index of the L2->L1 message denoting the withdrawal. + function isEthWithdrawalFinalized(uint256 _l2BatchNumber, uint256 _l2MessageIndex) external view returns (bool); + + /// @return The pubdata pricing mode. + function getPubdataPricingMode() external view returns (PubdataPricingMode); + + /// @return the baseTokenGasPriceMultiplierNominator, used to compare the baseTokenPrice to ether for L1->L2 transactions + function baseTokenGasPriceMultiplierNominator() external view returns (uint128); + + /// @return the baseTokenGasPriceMultiplierDenominator, used to compare the baseTokenPrice to ether for L1->L2 transactions + function baseTokenGasPriceMultiplierDenominator() external view returns (uint128); + + /*////////////////////////////////////////////////////////////// + DIAMOND LOUPE + //////////////////////////////////////////////////////////////*/ + + /// @notice Faсet structure compatible with the EIP-2535 diamond loupe + /// @param addr The address of the facet contract + /// @param selectors The NON-sorted array with selectors associated with facet + struct Facet { + address addr; + bytes4[] selectors; + } + + /// @return result All facet addresses and their function selectors + function facets() external view returns (Facet[] memory); + + /// @return NON-sorted array with function selectors supported by a specific facet + function facetFunctionSelectors(address _facet) external view returns (bytes4[] memory); + + /// @return facets NON-sorted array of facet addresses supported on diamond + function facetAddresses() external view returns (address[] memory facets); + + /// @return facet The facet address associated with a selector. Zero if the selector is not added to the diamond + function facetAddress(bytes4 _selector) external view returns (address facet); + + /// @return Whether the selector can be frozen by the admin or always accessible + function isFunctionFreezable(bytes4 _selector) external view returns (bool); + + /// @return isFreezable Whether the facet can be frozen by the admin or always accessible + function isFacetFreezable(address _facet) external view returns (bool isFreezable); +} diff --git a/contracts/l1-contracts/state-transition/chain-interfaces/ILegacyGetters.sol b/contracts/l1-contracts/state-transition/chain-interfaces/ILegacyGetters.sol new file mode 100644 index 0000000..cb62f50 --- /dev/null +++ b/contracts/l1-contracts/state-transition/chain-interfaces/ILegacyGetters.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.21; + +import {IZkSyncHyperchainBase} from "./IZkSyncHyperchainBase.sol"; + +/// @author Matter Labs +/// @dev This interface contains getters for the ZKsync contract that should not be used, +/// but still are kept for backward compatibility. +/// @custom:security-contact security@matterlabs.dev +interface ILegacyGetters is IZkSyncHyperchainBase { + /// @return The total number of batches that were committed + /// @dev It is a *deprecated* method, please use `getTotalBatchesCommitted` instead + function getTotalBlocksCommitted() external view returns (uint256); + + /// @return The total number of batches that were committed & verified + /// @dev It is a *deprecated* method, please use `getTotalBatchesVerified` instead. + function getTotalBlocksVerified() external view returns (uint256); + + /// @return The total number of batches that were committed & verified & executed + /// @dev It is a *deprecated* method, please use `getTotalBatchesExecuted` instead. + function getTotalBlocksExecuted() external view returns (uint256); + + /// @notice For unfinalized (non executed) batches may change + /// @dev It is a *deprecated* method, please use `storedBatchHash` instead. + /// @dev returns zero for non-committed batches + /// @return The hash of committed L2 batch. + function storedBlockHash(uint256 _batchNumber) external view returns (bytes32); + + /// @return The L2 batch number in which the upgrade transaction was processed. + /// @dev It is a *deprecated* method, please use `getL2SystemContractsUpgradeBatchNumber` instead. + /// @dev It is equal to 0 in the following two cases: + /// - No upgrade transaction has ever been processed. + /// - The upgrade transaction has been processed and the batch with such transaction has been + /// executed (i.e. finalized). + function getL2SystemContractsUpgradeBlockNumber() external view returns (uint256); +} diff --git a/contracts/l1-contracts/state-transition/chain-interfaces/IMailbox.sol b/contracts/l1-contracts/state-transition/chain-interfaces/IMailbox.sol new file mode 100644 index 0000000..9daffeb --- /dev/null +++ b/contracts/l1-contracts/state-transition/chain-interfaces/IMailbox.sol @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.21; + +import {IZkSyncHyperchainBase} from "./IZkSyncHyperchainBase.sol"; +import {L2CanonicalTransaction, L2Log, L2Message, TxStatus, BridgehubL2TransactionRequest} from "../../common/Messaging.sol"; + +/// @title The interface of the ZKsync Mailbox contract that provides interfaces for L1 <-> L2 interaction. +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +interface IMailbox is IZkSyncHyperchainBase { + /// @notice Prove that a specific arbitrary-length message was sent in a specific L2 batch number + /// @param _batchNumber The executed L2 batch number in which the message appeared + /// @param _index The position in the L2 logs Merkle tree of the l2Log that was sent with the message + /// @param _message Information about the sent message: sender address, the message itself, tx index in the L2 batch where the message was sent + /// @param _proof Merkle proof for inclusion of L2 log that was sent with the message + /// @return Whether the proof is valid + function proveL2MessageInclusion( + uint256 _batchNumber, + uint256 _index, + L2Message calldata _message, + bytes32[] calldata _proof + ) external view returns (bool); + + /// @notice Prove that a specific L2 log was sent in a specific L2 batch + /// @param _batchNumber The executed L2 batch number in which the log appeared + /// @param _index The position of the l2log in the L2 logs Merkle tree + /// @param _log Information about the sent log + /// @param _proof Merkle proof for inclusion of the L2 log + /// @return Whether the proof is correct and L2 log is included in batch + function proveL2LogInclusion( + uint256 _batchNumber, + uint256 _index, + L2Log memory _log, + bytes32[] calldata _proof + ) external view returns (bool); + + /// @notice Prove that the L1 -> L2 transaction was processed with the specified status. + /// @param _l2TxHash The L2 canonical transaction hash + /// @param _l2BatchNumber The L2 batch number where the transaction was processed + /// @param _l2MessageIndex The position in the L2 logs Merkle tree of the l2Log that was sent with the message + /// @param _l2TxNumberInBatch The L2 transaction number in the batch, in which the log was sent + /// @param _merkleProof The Merkle proof of the processing L1 -> L2 transaction + /// @param _status The execution status of the L1 -> L2 transaction (true - success & 0 - fail) + /// @return Whether the proof is correct and the transaction was actually executed with provided status + /// NOTE: It may return `false` for incorrect proof, but it doesn't mean that the L1 -> L2 transaction has an opposite status! + function proveL1ToL2TransactionStatus( + bytes32 _l2TxHash, + uint256 _l2BatchNumber, + uint256 _l2MessageIndex, + uint16 _l2TxNumberInBatch, + bytes32[] calldata _merkleProof, + TxStatus _status + ) external view returns (bool); + + /// @notice Finalize the withdrawal and release funds + /// @param _l2BatchNumber The L2 batch number where the withdrawal was processed + /// @param _l2MessageIndex The position in the L2 logs Merkle tree of the l2Log that was sent with the message + /// @param _l2TxNumberInBatch The L2 transaction number in a batch, in which the log was sent + /// @param _message The L2 withdraw data, stored in an L2 -> L1 message + /// @param _merkleProof The Merkle proof of the inclusion L2 -> L1 message about withdrawal initialization + function finalizeEthWithdrawal( + uint256 _l2BatchNumber, + uint256 _l2MessageIndex, + uint16 _l2TxNumberInBatch, + bytes calldata _message, + bytes32[] calldata _merkleProof + ) external; + + /// @notice Request execution of L2 transaction from L1. + /// @param _contractL2 The L2 receiver address + /// @param _l2Value `msg.value` of L2 transaction + /// @param _calldata The input of the L2 transaction + /// @param _l2GasLimit Maximum amount of L2 gas that transaction can consume during execution on L2 + /// @param _l2GasPerPubdataByteLimit The maximum amount L2 gas that the operator may charge the user for single byte of pubdata. + /// @param _factoryDeps An array of L2 bytecodes that will be marked as known on L2 + /// @param _refundRecipient The address on L2 that will receive the refund for the transaction. + /// @dev If the L2 deposit finalization transaction fails, the `_refundRecipient` will receive the `_l2Value`. + /// Please note, the contract may change the refund recipient's address to eliminate sending funds to addresses out of control. + /// - If `_refundRecipient` is a contract on L1, the refund will be sent to the aliased `_refundRecipient`. + /// - If `_refundRecipient` is set to `address(0)` and the sender has NO deployed bytecode on L1, the refund will be sent to the `msg.sender` address. + /// - If `_refundRecipient` is set to `address(0)` and the sender has deployed bytecode on L1, the refund will be sent to the aliased `msg.sender` address. + /// @dev The address aliasing of L1 contracts as refund recipient on L2 is necessary to guarantee that the funds are controllable, + /// since address aliasing to the from address for the L2 tx will be applied if the L1 `msg.sender` is a contract. + /// Without address aliasing for L1 contracts as refund recipients they would not be able to make proper L2 tx requests + /// through the Mailbox to use or withdraw the funds from L2, and the funds would be lost. + /// @return canonicalTxHash The hash of the requested L2 transaction. This hash can be used to follow the transaction status + function requestL2Transaction( + address _contractL2, + uint256 _l2Value, + bytes calldata _calldata, + uint256 _l2GasLimit, + uint256 _l2GasPerPubdataByteLimit, + bytes[] calldata _factoryDeps, + address _refundRecipient + ) external payable returns (bytes32 canonicalTxHash); + + function bridgehubRequestL2Transaction( + BridgehubL2TransactionRequest calldata _request + ) external returns (bytes32 canonicalTxHash); + + /// @notice Estimates the cost in Ether of requesting execution of an L2 transaction from L1 + /// @param _gasPrice expected L1 gas price at which the user requests the transaction execution + /// @param _l2GasLimit Maximum amount of L2 gas that transaction can consume during execution on L2 + /// @param _l2GasPerPubdataByteLimit The maximum amount of L2 gas that the operator may charge the user for a single byte of pubdata. + /// @return The estimated ETH spent on L2 gas for the transaction + function l2TransactionBaseCost( + uint256 _gasPrice, + uint256 _l2GasLimit, + uint256 _l2GasPerPubdataByteLimit + ) external view returns (uint256); + + /// @notice transfer Eth to shared bridge as part of migration process + function transferEthToSharedBridge() external; + + /// @notice New priority request event. Emitted when a request is placed into the priority queue + /// @param txId Serial number of the priority operation + /// @param txHash keccak256 hash of encoded transaction representation + /// @param expirationTimestamp Timestamp up to which priority request should be processed + /// @param transaction The whole transaction structure that is requested to be executed on L2 + /// @param factoryDeps An array of bytecodes that were shown in the L1 public data. + /// Will be marked as known bytecodes in L2 + event NewPriorityRequest( + uint256 txId, + bytes32 txHash, + uint64 expirationTimestamp, + L2CanonicalTransaction transaction, + bytes[] factoryDeps + ); +} diff --git a/contracts/l1-contracts/state-transition/chain-interfaces/ITransactionFilterer.sol b/contracts/l1-contracts/state-transition/chain-interfaces/ITransactionFilterer.sol new file mode 100644 index 0000000..a3776ca --- /dev/null +++ b/contracts/l1-contracts/state-transition/chain-interfaces/ITransactionFilterer.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.21; + +/// @title The interface of the L1 -> L2 transaction filterer. +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +interface ITransactionFilterer { + /// @notice Check if the transaction is allowed + /// @param sender The sender of the transaction + /// @param contractL2 The L2 receiver address + /// @param mintValue The value of the L1 transaction + /// @param l2Value The msg.value of the L2 transaction + /// @param l2Calldata The calldata of the L2 transaction + /// @param refundRecipient The address to refund the excess value + /// @return Whether the transaction is allowed + function isTransactionAllowed( + address sender, + address contractL2, + uint256 mintValue, + uint256 l2Value, + bytes memory l2Calldata, + address refundRecipient + ) external view returns (bool); +} diff --git a/contracts/l1-contracts/state-transition/chain-interfaces/IVerifier.sol b/contracts/l1-contracts/state-transition/chain-interfaces/IVerifier.sol new file mode 100644 index 0000000..97872c3 --- /dev/null +++ b/contracts/l1-contracts/state-transition/chain-interfaces/IVerifier.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.21; + +/// @notice Part of the configuration parameters of ZKP circuits +struct VerifierParams { + bytes32 recursionNodeLevelVkHash; + bytes32 recursionLeafLevelVkHash; + bytes32 recursionCircuitsSetVksHash; +} + +/// @title The interface of the Verifier contract, responsible for the zero knowledge proof verification. +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +interface IVerifier { + /// @dev Verifies a zk-SNARK proof. + /// @return A boolean value indicating whether the zk-SNARK proof is valid. + /// Note: The function may revert execution instead of returning false in some cases. + function verify( + uint256[] calldata _publicInputs, + uint256[] calldata _proof, + uint256[] calldata _recursiveAggregationInput + ) external view returns (bool); + + /// @notice Calculates a keccak256 hash of the runtime loaded verification keys. + /// @return vkHash The keccak256 hash of the loaded verification keys. + function verificationKeyHash() external pure returns (bytes32); +} diff --git a/contracts/l1-contracts/state-transition/chain-interfaces/IZkSyncHyperchain.sol b/contracts/l1-contracts/state-transition/chain-interfaces/IZkSyncHyperchain.sol new file mode 100644 index 0000000..14aa123 --- /dev/null +++ b/contracts/l1-contracts/state-transition/chain-interfaces/IZkSyncHyperchain.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.21; + +import {IAdmin} from "./IAdmin.sol"; +import {IExecutor} from "./IExecutor.sol"; +import {IGetters} from "./IGetters.sol"; +import {IMailbox} from "./IMailbox.sol"; + +import {Diamond} from "../libraries/Diamond.sol"; + +interface IZkSyncHyperchain is IAdmin, IExecutor, IGetters, IMailbox { + // We need this structure for the server for now + event ProposeTransparentUpgrade( + Diamond.DiamondCutData diamondCut, + uint256 indexed proposalId, + bytes32 proposalSalt + ); +} diff --git a/contracts/l1-contracts/state-transition/chain-interfaces/IZkSyncHyperchainBase.sol b/contracts/l1-contracts/state-transition/chain-interfaces/IZkSyncHyperchainBase.sol new file mode 100644 index 0000000..24fe3b4 --- /dev/null +++ b/contracts/l1-contracts/state-transition/chain-interfaces/IZkSyncHyperchainBase.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: UNLICENSED +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.21; + +/// @title The interface of the ZKsync contract, responsible for the main ZKsync logic. +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +interface IZkSyncHyperchainBase { + /// @return Returns facet name. + function getName() external view returns (string memory); +} diff --git a/contracts/l1-contracts/state-transition/l2-deps/ISystemContext.sol b/contracts/l1-contracts/state-transition/l2-deps/ISystemContext.sol new file mode 100644 index 0000000..d09e764 --- /dev/null +++ b/contracts/l1-contracts/state-transition/l2-deps/ISystemContext.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: UNLICENSED +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.21; + +interface ISystemContext { + function setChainId(uint256 _newChainId) external; +} diff --git a/contracts/l1-contracts/state-transition/libraries/Diamond.sol b/contracts/l1-contracts/state-transition/libraries/Diamond.sol new file mode 100644 index 0000000..b43a673 --- /dev/null +++ b/contracts/l1-contracts/state-transition/libraries/Diamond.sol @@ -0,0 +1,330 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.21; + +import {SafeCast} from "@openzeppelin/contracts-v4/utils/math/SafeCast.sol"; +import {UncheckedMath} from "../../common/libraries/UncheckedMath.sol"; +import {NoFunctionsForDiamondCut, UndefinedDiamondCutAction, AddressHasNoCode, FacetExists, RemoveFunctionFacetAddressZero, SelectorsMustAllHaveSameFreezability, NonEmptyCalldata, ReplaceFunctionFacetAddressZero, RemoveFunctionFacetAddressNotZero, DelegateCallFailed} from "../../common/L1ContractErrors.sol"; + +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +/// @notice The helper library for managing the EIP-2535 diamond proxy. +library Diamond { + using UncheckedMath for uint256; + using SafeCast for uint256; + + /// @dev Magic value that should be returned by diamond cut initialize contracts. + /// @dev Used to distinguish calls to contracts that were supposed to be used as diamond initializer from other contracts. + bytes32 internal constant DIAMOND_INIT_SUCCESS_RETURN_VALUE = + 0x33774e659306e47509050e97cb651e731180a42d458212294d30751925c551a2; // keccak256("diamond.zksync.init") - 1 + + /// @dev Storage position of `DiamondStorage` structure. + bytes32 private constant DIAMOND_STORAGE_POSITION = + 0xc8fcad8db84d3cc18b4c41d551ea0ee66dd599cde068d998e57d5e09332c131b; // keccak256("diamond.standard.diamond.storage") - 1; + + event DiamondCut(FacetCut[] facetCuts, address initAddress, bytes initCalldata); + + /// @dev Utility struct that contains associated facet & meta information of selector + /// @param facetAddress address of the facet which is connected with selector + /// @param selectorPosition index in `FacetToSelectors.selectors` array, where is selector stored + /// @param isFreezable denotes whether the selector can be frozen. + struct SelectorToFacet { + address facetAddress; + uint16 selectorPosition; + bool isFreezable; + } + + /// @dev Utility struct that contains associated selectors & meta information of facet + /// @param selectors list of all selectors that belong to the facet + /// @param facetPosition index in `DiamondStorage.facets` array, where is facet stored + struct FacetToSelectors { + bytes4[] selectors; + uint16 facetPosition; + } + + /// @notice The structure that holds all diamond proxy associated parameters + /// @dev According to the EIP-2535 should be stored on a special storage key - `DIAMOND_STORAGE_POSITION` + /// @param selectorToFacet A mapping from the selector to the facet address and its meta information + /// @param facetToSelectors A mapping from facet address to its selectors with meta information + /// @param facets The array of all unique facet addresses that belong to the diamond proxy + /// @param isFrozen Denotes whether the diamond proxy is frozen and all freezable facets are not accessible + struct DiamondStorage { + mapping(bytes4 selector => SelectorToFacet selectorInfo) selectorToFacet; + mapping(address facetAddress => FacetToSelectors facetInfo) facetToSelectors; + address[] facets; + bool isFrozen; + } + + /// @dev Parameters for diamond changes that touch one of the facets + /// @param facet The address of facet that's affected by the cut + /// @param action The action that is made on the facet + /// @param isFreezable Denotes whether the facet & all their selectors can be frozen + /// @param selectors An array of unique selectors that belongs to the facet address + // solhint-disable-next-line gas-struct-packing + struct FacetCut { + address facet; + Action action; + bool isFreezable; + bytes4[] selectors; + } + + /// @dev Structure of the diamond proxy changes + /// @param facetCuts The set of changes (adding/removing/replacement) of implementation contracts + /// @param initAddress The address that's delegate called after setting up new facet changes + /// @param initCalldata Calldata for the delegate call to `initAddress` + struct DiamondCutData { + FacetCut[] facetCuts; + address initAddress; + bytes initCalldata; + } + + /// @dev Type of change over diamond: add/replace/remove facets + enum Action { + Add, + Replace, + Remove + } + + /// @return diamondStorage The pointer to the storage where all specific diamond proxy parameters stored + function getDiamondStorage() internal pure returns (DiamondStorage storage diamondStorage) { + bytes32 position = DIAMOND_STORAGE_POSITION; + assembly { + diamondStorage.slot := position + } + } + + /// @dev Add/replace/remove any number of selectors and optionally execute a function with delegatecall + /// @param _diamondCut Diamond's facet changes and the parameters to optional initialization delegatecall + function diamondCut(DiamondCutData memory _diamondCut) internal { + FacetCut[] memory facetCuts = _diamondCut.facetCuts; + address initAddress = _diamondCut.initAddress; + bytes memory initCalldata = _diamondCut.initCalldata; + uint256 facetCutsLength = facetCuts.length; + for (uint256 i = 0; i < facetCutsLength; i = i.uncheckedInc()) { + Action action = facetCuts[i].action; + address facet = facetCuts[i].facet; + bool isFacetFreezable = facetCuts[i].isFreezable; + bytes4[] memory selectors = facetCuts[i].selectors; + + if (selectors.length == 0) { + revert NoFunctionsForDiamondCut(); + } + + if (action == Action.Add) { + _addFunctions(facet, selectors, isFacetFreezable); + } else if (action == Action.Replace) { + _replaceFunctions(facet, selectors, isFacetFreezable); + } else if (action == Action.Remove) { + _removeFunctions(facet, selectors); + } else { + revert UndefinedDiamondCutAction(); + } + } + + _initializeDiamondCut(initAddress, initCalldata); + emit DiamondCut(facetCuts, initAddress, initCalldata); + } + + /// @dev Add new functions to the diamond proxy + /// NOTE: expect but NOT enforce that `_selectors` is NON-EMPTY array + function _addFunctions(address _facet, bytes4[] memory _selectors, bool _isFacetFreezable) private { + DiamondStorage storage ds = getDiamondStorage(); + + // Facet with no code cannot be added. + // This check also verifies that the facet does not have zero address, since it is the + // address with which 0x00000000 selector is associated. + if (_facet.code.length == 0) { + revert AddressHasNoCode(_facet); + } + + // Add facet to the list of facets if the facet address is new one + _saveFacetIfNew(_facet); + + uint256 selectorsLength = _selectors.length; + for (uint256 i = 0; i < selectorsLength; i = i.uncheckedInc()) { + bytes4 selector = _selectors[i]; + SelectorToFacet memory oldFacet = ds.selectorToFacet[selector]; + if (oldFacet.facetAddress != address(0)) { + revert FacetExists(selector, oldFacet.facetAddress); + } + + _addOneFunction(_facet, selector, _isFacetFreezable); + } + } + + /// @dev Change associated facets to already known function selectors + /// NOTE: expect but NOT enforce that `_selectors` is NON-EMPTY array + function _replaceFunctions(address _facet, bytes4[] memory _selectors, bool _isFacetFreezable) private { + DiamondStorage storage ds = getDiamondStorage(); + + // Facet with no code cannot be added. + // This check also verifies that the facet does not have zero address, since it is the + // address with which 0x00000000 selector is associated. + if (_facet.code.length == 0) { + revert AddressHasNoCode(_facet); + } + + uint256 selectorsLength = _selectors.length; + for (uint256 i = 0; i < selectorsLength; i = i.uncheckedInc()) { + bytes4 selector = _selectors[i]; + SelectorToFacet memory oldFacet = ds.selectorToFacet[selector]; + // it is impossible to replace the facet with zero address + if (oldFacet.facetAddress == address(0)) { + revert ReplaceFunctionFacetAddressZero(); + } + + _removeOneFunction(oldFacet.facetAddress, selector); + // Add facet to the list of facets if the facet address is a new one + _saveFacetIfNew(_facet); + _addOneFunction(_facet, selector, _isFacetFreezable); + } + } + + /// @dev Remove association with function and facet + /// NOTE: expect but NOT enforce that `_selectors` is NON-EMPTY array + function _removeFunctions(address _facet, bytes4[] memory _selectors) private { + DiamondStorage storage ds = getDiamondStorage(); + + // facet address must be zero + if (_facet != address(0)) { + revert RemoveFunctionFacetAddressNotZero(_facet); + } + + uint256 selectorsLength = _selectors.length; + for (uint256 i = 0; i < selectorsLength; i = i.uncheckedInc()) { + bytes4 selector = _selectors[i]; + SelectorToFacet memory oldFacet = ds.selectorToFacet[selector]; + // Can't delete a non-existent facet + if (oldFacet.facetAddress == address(0)) { + revert RemoveFunctionFacetAddressZero(); + } + + _removeOneFunction(oldFacet.facetAddress, selector); + } + } + + /// @dev Add address to the list of known facets if it is not on the list yet + /// NOTE: should be called ONLY before adding a new selector associated with the address + function _saveFacetIfNew(address _facet) private { + DiamondStorage storage ds = getDiamondStorage(); + + uint256 selectorsLength = ds.facetToSelectors[_facet].selectors.length; + // If there are no selectors associated with facet then save facet as new one + if (selectorsLength == 0) { + ds.facetToSelectors[_facet].facetPosition = ds.facets.length.toUint16(); + ds.facets.push(_facet); + } + } + + /// @dev Add one function to the already known facet + /// NOTE: It is expected but NOT enforced that: + /// - `_facet` is NON-ZERO address + /// - `_facet` is already stored address in `DiamondStorage.facets` + /// - `_selector` is NOT associated by another facet + function _addOneFunction(address _facet, bytes4 _selector, bool _isSelectorFreezable) private { + DiamondStorage storage ds = getDiamondStorage(); + + uint16 selectorPosition = (ds.facetToSelectors[_facet].selectors.length).toUint16(); + + // if selectorPosition is nonzero, it means it is not a new facet + // so the freezability of the first selector must be matched to _isSelectorFreezable + // so all the selectors in a facet will have the same freezability + if (selectorPosition != 0) { + bytes4 selector0 = ds.facetToSelectors[_facet].selectors[0]; + if (_isSelectorFreezable != ds.selectorToFacet[selector0].isFreezable) { + revert SelectorsMustAllHaveSameFreezability(); + } + } + + ds.selectorToFacet[_selector] = SelectorToFacet({ + facetAddress: _facet, + selectorPosition: selectorPosition, + isFreezable: _isSelectorFreezable + }); + ds.facetToSelectors[_facet].selectors.push(_selector); + } + + /// @dev Remove one associated function with facet + /// NOTE: It is expected but NOT enforced that `_facet` is NON-ZERO address + function _removeOneFunction(address _facet, bytes4 _selector) private { + DiamondStorage storage ds = getDiamondStorage(); + + // Get index of `FacetToSelectors.selectors` of the selector and last element of array + uint256 selectorPosition = ds.selectorToFacet[_selector].selectorPosition; + uint256 lastSelectorPosition = ds.facetToSelectors[_facet].selectors.length - 1; + + // If the selector is not at the end of the array then move the last element to the selector position + if (selectorPosition != lastSelectorPosition) { + bytes4 lastSelector = ds.facetToSelectors[_facet].selectors[lastSelectorPosition]; + + ds.facetToSelectors[_facet].selectors[selectorPosition] = lastSelector; + ds.selectorToFacet[lastSelector].selectorPosition = selectorPosition.toUint16(); + } + + // Remove last element from the selectors array + ds.facetToSelectors[_facet].selectors.pop(); + + // Finally, clean up the association with facet + delete ds.selectorToFacet[_selector]; + + // If there are no selectors for facet then remove the facet from the list of known facets + if (lastSelectorPosition == 0) { + _removeFacet(_facet); + } + } + + /// @dev remove facet from the list of known facets + /// NOTE: It is expected but NOT enforced that there are no selectors associated with `_facet` + function _removeFacet(address _facet) private { + DiamondStorage storage ds = getDiamondStorage(); + + // Get index of `DiamondStorage.facets` of the facet and last element of array + uint256 facetPosition = ds.facetToSelectors[_facet].facetPosition; + uint256 lastFacetPosition = ds.facets.length - 1; + + // If the facet is not at the end of the array then move the last element to the facet position + if (facetPosition != lastFacetPosition) { + address lastFacet = ds.facets[lastFacetPosition]; + + ds.facets[facetPosition] = lastFacet; + ds.facetToSelectors[lastFacet].facetPosition = facetPosition.toUint16(); + } + + // Remove last element from the facets array + ds.facets.pop(); + } + + /// @dev Delegates call to the initialization address with provided calldata + /// @dev Used as a final step of diamond cut to execute the logic of the initialization for changed facets + function _initializeDiamondCut(address _init, bytes memory _calldata) private { + if (_init == address(0)) { + // Non-empty calldata for zero address + if (_calldata.length != 0) { + revert NonEmptyCalldata(); + } + } else { + // Do not check whether `_init` is a contract since later we check that it returns data. + (bool success, bytes memory data) = _init.delegatecall(_calldata); + if (!success) { + // If the returndata is too small, we still want to produce some meaningful error + + if (data.length < 4) { + revert DelegateCallFailed(data); + } + + assembly { + revert(add(data, 0x20), mload(data)) + } + } + + // Check that called contract returns magic value to make sure that contract logic + // supposed to be used as diamond cut initializer. + if (data.length != 32) { + revert DelegateCallFailed(data); + } + if (abi.decode(data, (bytes32)) != DIAMOND_INIT_SUCCESS_RETURN_VALUE) { + revert DelegateCallFailed(data); + } + } + } +} diff --git a/contracts/l1-contracts/state-transition/libraries/LibMap.sol b/contracts/l1-contracts/state-transition/libraries/LibMap.sol new file mode 100644 index 0000000..2cbad0b --- /dev/null +++ b/contracts/l1-contracts/state-transition/libraries/LibMap.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.21; + +/// @notice Library for storage of packed unsigned integers. +/// @author Matter Labs +/// @dev This library is an adaptation of the corresponding Solady library (https://github.com/vectorized/solady/blob/main/src/utils/LibMap.sol) +/// @custom:security-contact security@matterlabs.dev +library LibMap { + /// @dev A uint32 map in storage. + struct Uint32Map { + mapping(uint256 packedIndex => uint256 eightPackedValues) map; + } + + /// @dev Retrieves the uint32 value at a specific index from the Uint32Map. + /// @param _map The Uint32Map instance containing the packed uint32 values. + /// @param _index The index of the uint32 value to retrieve. + /// @return result The uint32 value at the specified index. + function get(Uint32Map storage _map, uint256 _index) internal view returns (uint32 result) { + unchecked { + // Each storage slot can store 256 bits of data. + // As uint32 is 32 bits long, 8 uint32s can be packed into one storage slot. + // Hence, `_index / 8` is done to find the storage slot that contains the required uint32. + uint256 mapValue = _map.map[_index / 8]; + + // First three bits of the original `_index` denotes the position of the uint32 in that slot. + // So, '(_index & 7) * 32' is done to find the bit position of the uint32 in that storage slot. + uint256 bitOffset = (_index & 7) * 32; + + // Shift the bits to the right and retrieve the uint32 value. + result = uint32(mapValue >> bitOffset); + } + } + + /// @dev Updates the uint32 value at `_index` in `map`. + /// @param _map The Uint32Map instance containing the packed uint32 values. + /// @param _index The index of the uint32 value to set. + /// @param _value The new value at the specified index. + function set(Uint32Map storage _map, uint256 _index, uint32 _value) internal { + unchecked { + // Each storage slot can store 256 bits of data. + // As uint32 is 32 bits long, 8 uint32s can be packed into one storage slot. + // Hence, `_index / 8` is done to find the storage slot that contains the required uint32. + uint256 mapIndex = _index / 8; + uint256 mapValue = _map.map[mapIndex]; + + // First three bits of the original `_index` denotes the position of the uint32 in that slot. + // So, '(_index & 7) * 32' is done to find the bit position of the uint32 in that storage slot. + uint256 bitOffset = (_index & 7) * 32; + + // XORing a value A with B, and then with A again, gives the original value B. + // We will use this property to update the uint32 value in the slot. + + // Shift the bits to the right and retrieve the uint32 value. + uint32 oldValue = uint32(mapValue >> bitOffset); + + // Calculate the XOR of the new value and the existing value. + uint256 newValueXorOldValue = uint256(oldValue ^ _value); + + // Finally, we XOR the slot with the XOR of the new value and the existing value, + // shifted to its proper position. The XOR operation will effectively replace the old value with the new value. + _map.map[mapIndex] = (newValueXorOldValue << bitOffset) ^ mapValue; + } + } +} diff --git a/contracts/l1-contracts/state-transition/libraries/Merkle.sol b/contracts/l1-contracts/state-transition/libraries/Merkle.sol new file mode 100644 index 0000000..57701f3 --- /dev/null +++ b/contracts/l1-contracts/state-transition/libraries/Merkle.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.21; + +import {UncheckedMath} from "../../common/libraries/UncheckedMath.sol"; +import {MerklePathEmpty, MerklePathOutOfBounds, MerkleIndexOutOfBounds} from "../../common/L1ContractErrors.sol"; + +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +library Merkle { + using UncheckedMath for uint256; + + /// @dev Calculate Merkle root by the provided Merkle proof. + /// NOTE: When using this function, check that the _path length is equal to the tree height to prevent shorter/longer paths attack + /// @param _path Merkle path from the leaf to the root + /// @param _index Leaf index in the tree + /// @param _itemHash Hash of leaf content + /// @return The Merkle root + function calculateRoot( + bytes32[] calldata _path, + uint256 _index, + bytes32 _itemHash + ) internal pure returns (bytes32) { + uint256 pathLength = _path.length; + if (pathLength == 0) { + revert MerklePathEmpty(); + } + if (pathLength >= 256) { + revert MerklePathOutOfBounds(); + } + if (_index >= (1 << pathLength)) { + revert MerkleIndexOutOfBounds(); + } + + bytes32 currentHash = _itemHash; + for (uint256 i; i < pathLength; i = i.uncheckedInc()) { + currentHash = (_index % 2 == 0) + ? _efficientHash(currentHash, _path[i]) + : _efficientHash(_path[i], currentHash); + _index /= 2; + } + + return currentHash; + } + + /// @dev Keccak hash of the concatenation of two 32-byte words + function _efficientHash(bytes32 _lhs, bytes32 _rhs) private pure returns (bytes32 result) { + assembly { + mstore(0x00, _lhs) + mstore(0x20, _rhs) + result := keccak256(0x00, 0x40) + } + } +} diff --git a/contracts/l1-contracts/state-transition/libraries/PriorityQueue.sol b/contracts/l1-contracts/state-transition/libraries/PriorityQueue.sol new file mode 100644 index 0000000..141cd40 --- /dev/null +++ b/contracts/l1-contracts/state-transition/libraries/PriorityQueue.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.21; + +import {QueueIsEmpty} from "../../common/L1ContractErrors.sol"; + +/// @notice The structure that contains meta information of the L2 transaction that was requested from L1 +/// @dev The weird size of fields was selected specifically to minimize the structure storage size +/// @param canonicalTxHash Hashed L2 transaction data that is needed to process it +/// @param expirationTimestamp Expiration timestamp for this request (must be satisfied before) +/// @param layer2Tip Additional payment to the validator as an incentive to perform the operation +struct PriorityOperation { + bytes32 canonicalTxHash; + uint64 expirationTimestamp; + uint192 layer2Tip; +} + +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +/// @dev The library provides the API to interact with the priority queue container +/// @dev Order of processing operations from queue - FIFO (Fist in - first out) +library PriorityQueue { + using PriorityQueue for Queue; + + /// @notice Container that stores priority operations + /// @param data The inner mapping that saves priority operation by its index + /// @param head The pointer to the first unprocessed priority operation, equal to the tail if the queue is empty + /// @param tail The pointer to the free slot + struct Queue { + mapping(uint256 priorityOpId => PriorityOperation priorityOp) data; + uint256 tail; + uint256 head; + } + + /// @notice Returns zero if and only if no operations were processed from the queue + /// @return Index of the oldest priority operation that wasn't processed yet + function getFirstUnprocessedPriorityTx(Queue storage _queue) internal view returns (uint256) { + return _queue.head; + } + + /// @return The total number of priority operations that were added to the priority queue, including all processed ones + function getTotalPriorityTxs(Queue storage _queue) internal view returns (uint256) { + return _queue.tail; + } + + /// @return The total number of unprocessed priority operations in a priority queue + function getSize(Queue storage _queue) internal view returns (uint256) { + return uint256(_queue.tail - _queue.head); + } + + /// @return Whether the priority queue contains no operations + function isEmpty(Queue storage _queue) internal view returns (bool) { + return _queue.tail == _queue.head; + } + + /// @notice Add the priority operation to the end of the priority queue + function pushBack(Queue storage _queue, PriorityOperation memory _operation) internal { + // Save value into the stack to avoid double reading from the storage + uint256 tail = _queue.tail; + + _queue.data[tail] = _operation; + _queue.tail = tail + 1; + } + + /// @return The first unprocessed priority operation from the queue + function front(Queue storage _queue) internal view returns (PriorityOperation memory) { + // priority queue is empty + if (_queue.isEmpty()) { + revert QueueIsEmpty(); + } + + return _queue.data[_queue.head]; + } + + /// @notice Remove the first unprocessed priority operation from the queue + /// @return priorityOperation that was popped from the priority queue + function popFront(Queue storage _queue) internal returns (PriorityOperation memory priorityOperation) { + // priority queue is empty + if (_queue.isEmpty()) { + revert QueueIsEmpty(); + } + + // Save value into the stack to avoid double reading from the storage + uint256 head = _queue.head; + + priorityOperation = _queue.data[head]; + delete _queue.data[head]; + _queue.head = head + 1; + } +} diff --git a/contracts/l1-contracts/state-transition/libraries/TransactionValidator.sol b/contracts/l1-contracts/state-transition/libraries/TransactionValidator.sol new file mode 100644 index 0000000..f196053 --- /dev/null +++ b/contracts/l1-contracts/state-transition/libraries/TransactionValidator.sol @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.21; + +import {Math} from "@openzeppelin/contracts-v4/utils/math/Math.sol"; + +import {L2CanonicalTransaction} from "../../common/Messaging.sol"; +import {TX_SLOT_OVERHEAD_L2_GAS, MEMORY_OVERHEAD_GAS, L1_TX_INTRINSIC_L2_GAS, L1_TX_DELTA_544_ENCODING_BYTES, L1_TX_DELTA_FACTORY_DEPS_L2_GAS, L1_TX_MIN_L2_GAS_BASE, L1_TX_INTRINSIC_PUBDATA, L1_TX_DELTA_FACTORY_DEPS_PUBDATA} from "../../common/Config.sol"; +import {TooMuchGas, InvalidUpgradeTxn, UpgradeTxVerifyParam, PubdataGreaterThanLimit, ValidateTxnNotEnoughGas, TxnBodyGasLimitNotEnoughGas} from "../../common/L1ContractErrors.sol"; + +/// @title ZKsync Library for validating L1 -> L2 transactions +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +library TransactionValidator { + /// @dev Used to validate key properties of an L1->L2 transaction + /// @param _transaction The transaction to validate + /// @param _encoded The abi encoded bytes of the transaction + /// @param _priorityTxMaxGasLimit The max gas limit, generally provided from Storage.sol + /// @param _priorityTxMaxPubdata The maximal amount of pubdata that a single L1->L2 transaction can emit + function validateL1ToL2Transaction( + L2CanonicalTransaction memory _transaction, + bytes memory _encoded, + uint256 _priorityTxMaxGasLimit, + uint256 _priorityTxMaxPubdata + ) internal pure { + uint256 l2GasForTxBody = getTransactionBodyGasLimit(_transaction.gasLimit, _encoded.length); + + // Ensuring that the transaction is provable + if (l2GasForTxBody > _priorityTxMaxGasLimit) { + revert TooMuchGas(); + } + // Ensuring that the transaction cannot output more pubdata than is processable + if (l2GasForTxBody / _transaction.gasPerPubdataByteLimit > _priorityTxMaxPubdata) { + revert PubdataGreaterThanLimit(_priorityTxMaxPubdata, l2GasForTxBody / _transaction.gasPerPubdataByteLimit); + } + + // Ensuring that the transaction covers the minimal costs for its processing: + // hashing its content, publishing the factory dependencies, etc. + if ( + getMinimalPriorityTransactionGasLimit( + _encoded.length, + _transaction.factoryDeps.length, + _transaction.gasPerPubdataByteLimit + ) > l2GasForTxBody + ) { + revert ValidateTxnNotEnoughGas(); + } + } + + /// @dev Used to validate upgrade transactions + /// @param _transaction The transaction to validate + function validateUpgradeTransaction(L2CanonicalTransaction memory _transaction) internal pure { + // Restrict from to be within system contract range (0...2^16 - 1) + if (_transaction.from > type(uint16).max) { + revert InvalidUpgradeTxn(UpgradeTxVerifyParam.From); + } + if (_transaction.to > type(uint160).max) { + revert InvalidUpgradeTxn(UpgradeTxVerifyParam.To); + } + if (_transaction.paymaster != 0) { + revert InvalidUpgradeTxn(UpgradeTxVerifyParam.Paymaster); + } + if (_transaction.value != 0) { + revert InvalidUpgradeTxn(UpgradeTxVerifyParam.Value); + } + if (_transaction.maxFeePerGas != 0) { + revert InvalidUpgradeTxn(UpgradeTxVerifyParam.MaxFeePerGas); + } + if (_transaction.maxPriorityFeePerGas != 0) { + revert InvalidUpgradeTxn(UpgradeTxVerifyParam.MaxPriorityFeePerGas); + } + if (_transaction.reserved[0] != 0) { + revert InvalidUpgradeTxn(UpgradeTxVerifyParam.Reserved0); + } + if (_transaction.reserved[1] > type(uint160).max) { + revert InvalidUpgradeTxn(UpgradeTxVerifyParam.Reserved1); + } + if (_transaction.reserved[2] != 0) { + revert InvalidUpgradeTxn(UpgradeTxVerifyParam.Reserved2); + } + if (_transaction.reserved[3] != 0) { + revert InvalidUpgradeTxn(UpgradeTxVerifyParam.Reserved3); + } + if (_transaction.signature.length != 0) { + revert InvalidUpgradeTxn(UpgradeTxVerifyParam.Signature); + } + if (_transaction.paymasterInput.length != 0) { + revert InvalidUpgradeTxn(UpgradeTxVerifyParam.PaymasterInput); + } + if (_transaction.reservedDynamic.length != 0) { + revert InvalidUpgradeTxn(UpgradeTxVerifyParam.ReservedDynamic); + } + } + + /// @dev Calculates the approximate minimum gas limit required for executing a priority transaction. + /// @param _encodingLength The length of the priority transaction encoding in bytes. + /// @param _numberOfFactoryDependencies The number of new factory dependencies that will be added. + /// @param _l2GasPricePerPubdata The L2 gas price for publishing the priority transaction on L2. + /// @return The minimum gas limit required to execute the priority transaction. + /// Note: The calculation includes the main cost of the priority transaction, however, in reality, the operator can spend a little more gas on overheads. + function getMinimalPriorityTransactionGasLimit( + uint256 _encodingLength, + uint256 _numberOfFactoryDependencies, + uint256 _l2GasPricePerPubdata + ) internal pure returns (uint256) { + uint256 costForComputation; + { + // Adding the intrinsic cost for the transaction, i.e. auxiliary prices which cannot be easily accounted for + costForComputation = L1_TX_INTRINSIC_L2_GAS; + + // Taking into account the hashing costs that depend on the length of the transaction + // Note that L1_TX_DELTA_544_ENCODING_BYTES is the delta in the price for every 544 bytes of + // the transaction's encoding. It is taken as LCM between 136 and 32 (the length for each keccak256 round + // and the size of each new encoding word). + costForComputation += Math.ceilDiv(_encodingLength * L1_TX_DELTA_544_ENCODING_BYTES, 544); + + // Taking into the account the additional costs of providing new factory dependencies + costForComputation += _numberOfFactoryDependencies * L1_TX_DELTA_FACTORY_DEPS_L2_GAS; + + // There is a minimal amount of computational L2 gas that the transaction should cover + costForComputation = Math.max(costForComputation, L1_TX_MIN_L2_GAS_BASE); + } + + uint256 costForPubdata = 0; + { + // Adding the intrinsic cost for the transaction, i.e. auxiliary prices which cannot be easily accounted for + costForPubdata = L1_TX_INTRINSIC_PUBDATA * _l2GasPricePerPubdata; + + // Taking into the account the additional costs of providing new factory dependencies + costForPubdata += _numberOfFactoryDependencies * L1_TX_DELTA_FACTORY_DEPS_PUBDATA * _l2GasPricePerPubdata; + } + + return costForComputation + costForPubdata; + } + + /// @notice Based on the full L2 gas limit (that includes the batch overhead) and other + /// properties of the transaction, returns the l2GasLimit for the body of the transaction (the actual execution). + /// @param _totalGasLimit The L2 gas limit that includes both the overhead for processing the batch + /// and the L2 gas needed to process the transaction itself (i.e. the actual l2GasLimit that will be used for the transaction). + /// @param _encodingLength The length of the ABI-encoding of the transaction. + function getTransactionBodyGasLimit( + uint256 _totalGasLimit, + uint256 _encodingLength + ) internal pure returns (uint256 txBodyGasLimit) { + uint256 overhead = getOverheadForTransaction(_encodingLength); + + // provided gas limit doesn't cover transaction overhead + if (_totalGasLimit < overhead) { + revert TxnBodyGasLimitNotEnoughGas(); + } + unchecked { + // We enforce the fact that `_totalGasLimit >= overhead` explicitly above. + txBodyGasLimit = _totalGasLimit - overhead; + } + } + + /// @notice Based on the total L2 gas limit and several other parameters of the transaction + /// returns the part of the L2 gas that will be spent on the batch's overhead. + /// @dev The details of how this function works can be checked in the documentation + /// of the fee model of ZKsync. The appropriate comments are also present + /// in the Rust implementation description of function `get_maximal_allowed_overhead`. + /// @param _encodingLength The length of the binary encoding of the transaction in bytes + function getOverheadForTransaction( + uint256 _encodingLength + ) internal pure returns (uint256 batchOverheadForTransaction) { + // The overhead from taking up the transaction's slot + batchOverheadForTransaction = TX_SLOT_OVERHEAD_L2_GAS; + + // The overhead for occupying the bootloader memory can be derived from encoded_len + uint256 overheadForLength = MEMORY_OVERHEAD_GAS * _encodingLength; + batchOverheadForTransaction = Math.max(batchOverheadForTransaction, overheadForLength); + } +} diff --git a/contracts/l1-contracts/upgrades/BaseZkSyncUpgrade.sol b/contracts/l1-contracts/upgrades/BaseZkSyncUpgrade.sol new file mode 100644 index 0000000..4534884 --- /dev/null +++ b/contracts/l1-contracts/upgrades/BaseZkSyncUpgrade.sol @@ -0,0 +1,331 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {SafeCast} from "@openzeppelin/contracts-v4/utils/math/SafeCast.sol"; + +import {ZkSyncHyperchainBase} from "../state-transition/chain-deps/facets/ZkSyncHyperchainBase.sol"; +import {VerifierParams} from "../state-transition/chain-interfaces/IVerifier.sol"; +import {IVerifier} from "../state-transition/chain-interfaces/IVerifier.sol"; +import {L2ContractHelper} from "../common/libraries/L2ContractHelper.sol"; +import {TransactionValidator} from "../state-transition/libraries/TransactionValidator.sol"; +import {MAX_NEW_FACTORY_DEPS, SYSTEM_UPGRADE_L2_TX_TYPE, MAX_ALLOWED_MINOR_VERSION_DELTA} from "../common/Config.sol"; +import {L2CanonicalTransaction} from "../common/Messaging.sol"; +import {ProtocolVersionMinorDeltaTooBig, TimeNotReached, InvalidTxType, L2UpgradeNonceNotEqualToNewProtocolVersion, TooManyFactoryDeps, UnexpectedNumberOfFactoryDeps, ProtocolVersionTooSmall, PreviousUpgradeNotFinalized, PreviousUpgradeNotCleaned, L2BytecodeHashMismatch, PatchCantSetUpgradeTxn, PreviousProtocolMajorVersionNotZero, NewProtocolMajorVersionNotZero, PatchUpgradeCantSetDefaultAccount, PatchUpgradeCantSetBootloader} from "./ZkSyncUpgradeErrors.sol"; +import {SemVer} from "../common/libraries/SemVer.sol"; + +/// @notice The struct that represents the upgrade proposal. +/// @param l2ProtocolUpgradeTx The system upgrade transaction. +/// @param factoryDeps The list of factory deps for the l2ProtocolUpgradeTx. +/// @param bootloaderHash The hash of the new bootloader bytecode. If zero, it will not be updated. +/// @param defaultAccountHash The hash of the new default account bytecode. If zero, it will not be updated. +/// @param verifier The address of the new verifier. If zero, the verifier will not be updated. +/// @param verifierParams The new verifier params. If all of its fields are 0, the params will not be updated. +/// @param l1ContractsUpgradeCalldata Custom calldata for L1 contracts upgrade, it may be interpreted differently +/// in each upgrade. Usually empty. +/// @param postUpgradeCalldata Custom calldata for post upgrade hook, it may be interpreted differently in each +/// upgrade. Usually empty. +/// @param upgradeTimestamp The timestamp after which the upgrade can be executed. +/// @param newProtocolVersion The new version number for the protocol after this upgrade. Should be greater than +/// the previous protocol version. +struct ProposedUpgrade { + L2CanonicalTransaction l2ProtocolUpgradeTx; + bytes[] factoryDeps; + bytes32 bootloaderHash; + bytes32 defaultAccountHash; + address verifier; + VerifierParams verifierParams; + bytes l1ContractsUpgradeCalldata; + bytes postUpgradeCalldata; + uint256 upgradeTimestamp; + uint256 newProtocolVersion; +} + +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +/// @notice Interface to which all the upgrade implementations should adhere +abstract contract BaseZkSyncUpgrade is ZkSyncHyperchainBase { + /// @notice Changes the protocol version + event NewProtocolVersion(uint256 indexed previousProtocolVersion, uint256 indexed newProtocolVersion); + + /// @notice Сhanges to the bytecode that is used in L2 as a bootloader (start program) + event NewL2BootloaderBytecodeHash(bytes32 indexed previousBytecodeHash, bytes32 indexed newBytecodeHash); + + /// @notice Сhanges to the bytecode that is used in L2 as a default account + event NewL2DefaultAccountBytecodeHash(bytes32 indexed previousBytecodeHash, bytes32 indexed newBytecodeHash); + + /// @notice Verifier address changed + event NewVerifier(address indexed oldVerifier, address indexed newVerifier); + + /// @notice Verifier parameters changed + event NewVerifierParams(VerifierParams oldVerifierParams, VerifierParams newVerifierParams); + + /// @notice Notifies about complete upgrade + event UpgradeComplete(uint256 indexed newProtocolVersion, bytes32 indexed l2UpgradeTxHash, ProposedUpgrade upgrade); + + /// @notice The main function that will be provided by the upgrade proxy + /// @dev This is a virtual function and should be overridden by custom upgrade implementations. + /// @param _proposedUpgrade The upgrade to be executed. + /// @return txHash The hash of the L2 system contract upgrade transaction. + function upgrade(ProposedUpgrade calldata _proposedUpgrade) public virtual returns (bytes32 txHash) { + // Note that due to commitment delay, the timestamp of the L2 upgrade batch may be earlier than the timestamp + // of the L1 block at which the upgrade occurred. This means that using timestamp as a signifier of "upgraded" + // on the L2 side would be inaccurate. The effects of this "back-dating" of L2 upgrade batches will be reduced + // as the permitted delay window is reduced in the future. + if (block.timestamp < _proposedUpgrade.upgradeTimestamp) { + revert TimeNotReached(_proposedUpgrade.upgradeTimestamp, block.timestamp); + } + + (uint32 newMinorVersion, bool isPatchOnly) = _setNewProtocolVersion(_proposedUpgrade.newProtocolVersion); + _upgradeL1Contract(_proposedUpgrade.l1ContractsUpgradeCalldata); + _upgradeVerifier(_proposedUpgrade.verifier, _proposedUpgrade.verifierParams); + _setBaseSystemContracts(_proposedUpgrade.bootloaderHash, _proposedUpgrade.defaultAccountHash, isPatchOnly); + + txHash = _setL2SystemContractUpgrade( + _proposedUpgrade.l2ProtocolUpgradeTx, + _proposedUpgrade.factoryDeps, + newMinorVersion, + isPatchOnly + ); + + _postUpgrade(_proposedUpgrade.postUpgradeCalldata); + + emit UpgradeComplete(_proposedUpgrade.newProtocolVersion, txHash, _proposedUpgrade); + } + + /// @notice Change default account bytecode hash, that is used on L2 + /// @param _l2DefaultAccountBytecodeHash The hash of default account L2 bytecode + /// @param _patchOnly Whether only the patch part of the protocol version semver has changed + function _setL2DefaultAccountBytecodeHash(bytes32 _l2DefaultAccountBytecodeHash, bool _patchOnly) private { + if (_l2DefaultAccountBytecodeHash == bytes32(0)) { + return; + } + + if (_patchOnly) { + revert PatchUpgradeCantSetDefaultAccount(); + } + + L2ContractHelper.validateBytecodeHash(_l2DefaultAccountBytecodeHash); + + // Save previous value into the stack to put it into the event later + bytes32 previousDefaultAccountBytecodeHash = s.l2DefaultAccountBytecodeHash; + + // Change the default account bytecode hash + s.l2DefaultAccountBytecodeHash = _l2DefaultAccountBytecodeHash; + emit NewL2DefaultAccountBytecodeHash(previousDefaultAccountBytecodeHash, _l2DefaultAccountBytecodeHash); + } + + /// @notice Change bootloader bytecode hash, that is used on L2 + /// @param _l2BootloaderBytecodeHash The hash of bootloader L2 bytecode + /// @param _patchOnly Whether only the patch part of the protocol version semver has changed + function _setL2BootloaderBytecodeHash(bytes32 _l2BootloaderBytecodeHash, bool _patchOnly) private { + if (_l2BootloaderBytecodeHash == bytes32(0)) { + return; + } + + if (_patchOnly) { + revert PatchUpgradeCantSetBootloader(); + } + + L2ContractHelper.validateBytecodeHash(_l2BootloaderBytecodeHash); + + // Save previous value into the stack to put it into the event later + bytes32 previousBootloaderBytecodeHash = s.l2BootloaderBytecodeHash; + + // Change the bootloader bytecode hash + s.l2BootloaderBytecodeHash = _l2BootloaderBytecodeHash; + emit NewL2BootloaderBytecodeHash(previousBootloaderBytecodeHash, _l2BootloaderBytecodeHash); + } + + /// @notice Change the address of the verifier smart contract + /// @param _newVerifier Verifier smart contract address + function _setVerifier(IVerifier _newVerifier) private { + // An upgrade to the verifier must be done carefully to ensure there aren't batches in the committed state + // during the transition. If verifier is upgraded, it will immediately be used to prove all committed batches. + // Batches committed expecting the old verifier will fail. Ensure all committed batches are finalized before the + // verifier is upgraded. + if (_newVerifier == IVerifier(address(0))) { + return; + } + + IVerifier oldVerifier = s.verifier; + s.verifier = _newVerifier; + emit NewVerifier(address(oldVerifier), address(_newVerifier)); + } + + /// @notice Change the verifier parameters + /// @param _newVerifierParams New parameters for the verifier + function _setVerifierParams(VerifierParams calldata _newVerifierParams) private { + // An upgrade to the verifier params must be done carefully to ensure there aren't batches in the committed state + // during the transition. If verifier is upgraded, it will immediately be used to prove all committed batches. + // Batches committed expecting the old verifier params will fail. Ensure all committed batches are finalized before the + // verifier is upgraded. + if ( + _newVerifierParams.recursionNodeLevelVkHash == bytes32(0) && + _newVerifierParams.recursionLeafLevelVkHash == bytes32(0) && + _newVerifierParams.recursionCircuitsSetVksHash == bytes32(0) + ) { + return; + } + + VerifierParams memory oldVerifierParams = s.__DEPRECATED_verifierParams; + s.__DEPRECATED_verifierParams = _newVerifierParams; + emit NewVerifierParams(oldVerifierParams, _newVerifierParams); + } + + /// @notice Updates the verifier and the verifier params + /// @param _newVerifier The address of the new verifier. If 0, the verifier will not be updated. + /// @param _verifierParams The new verifier params. If all of the fields are 0, the params will not be updated. + function _upgradeVerifier(address _newVerifier, VerifierParams calldata _verifierParams) internal { + _setVerifier(IVerifier(_newVerifier)); + _setVerifierParams(_verifierParams); + } + + /// @notice Updates the bootloader hash and the hash of the default account + /// @param _bootloaderHash The hash of the new bootloader bytecode. If zero, it will not be updated. + /// @param _defaultAccountHash The hash of the new default account bytecode. If zero, it will not be updated. + /// @param _patchOnly Whether only the patch part of the protocol version semver has changed. + function _setBaseSystemContracts(bytes32 _bootloaderHash, bytes32 _defaultAccountHash, bool _patchOnly) internal { + _setL2BootloaderBytecodeHash(_bootloaderHash, _patchOnly); + _setL2DefaultAccountBytecodeHash(_defaultAccountHash, _patchOnly); + } + + /// @notice Sets the hash of the L2 system contract upgrade transaction for the next batch to be committed + /// @dev If the transaction is noop (i.e. its type is 0) it does nothing and returns 0. + /// @param _l2ProtocolUpgradeTx The L2 system contract upgrade transaction. + /// @param _factoryDeps The factory dependencies that are used by the transaction. + /// @param _newMinorProtocolVersion The new minor protocol version. It must be used as the `nonce` field + /// of the `_l2ProtocolUpgradeTx`. + /// @param _patchOnly Whether only the patch part of the protocol version semver has changed. + /// @return System contracts upgrade transaction hash. Zero if no upgrade transaction is set. + function _setL2SystemContractUpgrade( + L2CanonicalTransaction calldata _l2ProtocolUpgradeTx, + bytes[] calldata _factoryDeps, + uint32 _newMinorProtocolVersion, + bool _patchOnly + ) internal returns (bytes32) { + // If the type is 0, it is considered as noop and so will not be required to be executed. + if (_l2ProtocolUpgradeTx.txType == 0) { + return bytes32(0); + } + + if (_l2ProtocolUpgradeTx.txType != SYSTEM_UPGRADE_L2_TX_TYPE) { + revert InvalidTxType(_l2ProtocolUpgradeTx.txType); + } + if (_patchOnly) { + revert PatchCantSetUpgradeTxn(); + } + + bytes memory encodedTransaction = abi.encode(_l2ProtocolUpgradeTx); + + TransactionValidator.validateL1ToL2Transaction( + _l2ProtocolUpgradeTx, + encodedTransaction, + s.priorityTxMaxGasLimit, + s.feeParams.priorityTxMaxPubdata + ); + + TransactionValidator.validateUpgradeTransaction(_l2ProtocolUpgradeTx); + + // We want the hashes of l2 system upgrade transactions to be unique. + // This is why we require that the `nonce` field is unique to each upgrade. + if (_l2ProtocolUpgradeTx.nonce != _newMinorProtocolVersion) { + revert L2UpgradeNonceNotEqualToNewProtocolVersion(_l2ProtocolUpgradeTx.nonce, _newMinorProtocolVersion); + } + + _verifyFactoryDeps(_factoryDeps, _l2ProtocolUpgradeTx.factoryDeps); + + bytes32 l2ProtocolUpgradeTxHash = keccak256(encodedTransaction); + + s.l2SystemContractsUpgradeTxHash = l2ProtocolUpgradeTxHash; + + return l2ProtocolUpgradeTxHash; + } + + /// @notice Verifies that the factory deps correspond to the proper hashes + /// @param _factoryDeps The list of factory deps + /// @param _expectedHashes The list of expected bytecode hashes + function _verifyFactoryDeps(bytes[] calldata _factoryDeps, uint256[] calldata _expectedHashes) private pure { + if (_factoryDeps.length != _expectedHashes.length) { + revert UnexpectedNumberOfFactoryDeps(); + } + if (_factoryDeps.length > MAX_NEW_FACTORY_DEPS) { + revert TooManyFactoryDeps(); + } + uint256 length = _factoryDeps.length; + + for (uint256 i = 0; i < length; ++i) { + bytes32 bytecodeHash = L2ContractHelper.hashL2Bytecode(_factoryDeps[i]); + if (bytecodeHash != bytes32(_expectedHashes[i])) { + revert L2BytecodeHashMismatch(bytecodeHash, bytes32(_expectedHashes[i])); + } + } + } + + /// @notice Changes the protocol version + /// @param _newProtocolVersion The new protocol version + function _setNewProtocolVersion( + uint256 _newProtocolVersion + ) internal virtual returns (uint32 newMinorVersion, bool patchOnly) { + uint256 previousProtocolVersion = s.protocolVersion; + if (_newProtocolVersion <= previousProtocolVersion) { + revert ProtocolVersionTooSmall(); + } + // slither-disable-next-line unused-return + (uint32 previousMajorVersion, uint32 previousMinorVersion, ) = SemVer.unpackSemVer( + SafeCast.toUint96(previousProtocolVersion) + ); + if (previousMajorVersion != 0) { + revert PreviousProtocolMajorVersionNotZero(); + } + + uint32 newMajorVersion; + // slither-disable-next-line unused-return + (newMajorVersion, newMinorVersion, ) = SemVer.unpackSemVer(SafeCast.toUint96(_newProtocolVersion)); + if (newMajorVersion != 0) { + revert NewProtocolMajorVersionNotZero(); + } + + // Since `_newProtocolVersion > previousProtocolVersion`, and both old and new major version is 0, + // the difference between minor versions is >= 0. + uint256 minorDelta = newMinorVersion - previousMinorVersion; + + if (minorDelta == 0) { + patchOnly = true; + } + + // While this is implicitly enforced by other checks above, we still double check just in case + if (minorDelta > MAX_ALLOWED_MINOR_VERSION_DELTA) { + revert ProtocolVersionMinorDeltaTooBig(MAX_ALLOWED_MINOR_VERSION_DELTA, minorDelta); + } + + // If the minor version changes also, we need to ensure that the previous upgrade has been finalized. + // In case the minor version does not change, we permit to keep the old upgrade transaction in the system, but it + // must be ensured in the other parts of the upgrade that the upgrade transaction is not overridden. + if (!patchOnly) { + // If the previous upgrade had an L2 system upgrade transaction, we require that it is finalized. + // Note it is important to keep this check, as otherwise hyperchains might skip upgrades by overwriting + if (s.l2SystemContractsUpgradeTxHash != bytes32(0)) { + revert PreviousUpgradeNotFinalized(s.l2SystemContractsUpgradeTxHash); + } + if (s.l2SystemContractsUpgradeBatchNumber != 0) { + revert PreviousUpgradeNotCleaned(); + } + } + + s.protocolVersion = _newProtocolVersion; + emit NewProtocolVersion(previousProtocolVersion, _newProtocolVersion); + } + + /// @notice Placeholder function for custom logic for upgrading L1 contract. + /// Typically this function will never be used. + /// @param _customCallDataForUpgrade Custom data for an upgrade, which may be interpreted differently for each + /// upgrade. + function _upgradeL1Contract(bytes calldata _customCallDataForUpgrade) internal virtual {} + + /// @notice placeholder function for custom logic for post-upgrade logic. + /// Typically this function will never be used. + /// @param _customCallDataForUpgrade Custom data for an upgrade, which may be interpreted differently for each + /// upgrade. + function _postUpgrade(bytes calldata _customCallDataForUpgrade) internal virtual {} +} diff --git a/contracts/l1-contracts/upgrades/BaseZkSyncUpgradeGenesis.sol b/contracts/l1-contracts/upgrades/BaseZkSyncUpgradeGenesis.sol new file mode 100644 index 0000000..561f25d --- /dev/null +++ b/contracts/l1-contracts/upgrades/BaseZkSyncUpgradeGenesis.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {SafeCast} from "@openzeppelin/contracts-v4/utils/math/SafeCast.sol"; + +import {BaseZkSyncUpgrade} from "./BaseZkSyncUpgrade.sol"; +import {ProtocolVersionTooSmall, ProtocolVersionDeltaTooLarge, PreviousUpgradeNotFinalized, PreviousUpgradeBatchNotCleared, ProtocolMajorVersionNotZero} from "./ZkSyncUpgradeErrors.sol"; +import {MAX_ALLOWED_MINOR_VERSION_DELTA} from "../common/Config.sol"; +import {SemVer} from "../common/libraries/SemVer.sol"; + +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +/// @notice Interface to which all the upgrade implementations should adhere +abstract contract BaseZkSyncUpgradeGenesis is BaseZkSyncUpgrade { + /// @notice Changes the protocol version + /// @param _newProtocolVersion The new protocol version + function _setNewProtocolVersion( + uint256 _newProtocolVersion + ) internal override returns (uint32 newMinorVersion, bool patchOnly) { + uint256 previousProtocolVersion = s.protocolVersion; + if ( + // IMPORTANT Genesis Upgrade difference: Note this is the only thing change <= to < + _newProtocolVersion < previousProtocolVersion + ) { + revert ProtocolVersionTooSmall(); + } + // slither-disable-next-line unused-return + (uint32 previousMajorVersion, uint32 previousMinorVersion, ) = SemVer.unpackSemVer( + SafeCast.toUint96(previousProtocolVersion) + ); + + if (previousMajorVersion != 0) { + revert ProtocolMajorVersionNotZero(); + } + + uint32 newMajorVersion; + // slither-disable-next-line unused-return + (newMajorVersion, newMinorVersion, ) = SemVer.unpackSemVer(SafeCast.toUint96(_newProtocolVersion)); + if (newMajorVersion != 0) { + revert ProtocolMajorVersionNotZero(); + } + + // Since `_newProtocolVersion > previousProtocolVersion`, and both old and new major version is 0, + // the difference between minor versions is >= 0. + uint256 minorDelta = newMinorVersion - previousMinorVersion; + + // IMPORTANT Genesis Upgrade difference: We never set patchOnly to `true` to allow to put a system upgrade transaction there. + patchOnly = false; + + // While this is implicitly enforced by other checks above, we still double check just in case + if (minorDelta > MAX_ALLOWED_MINOR_VERSION_DELTA) { + revert ProtocolVersionDeltaTooLarge(minorDelta, MAX_ALLOWED_MINOR_VERSION_DELTA); + } + + // If the minor version changes also, we need to ensure that the previous upgrade has been finalized. + // In case the minor version does not change, we permit to keep the old upgrade transaction in the system, but it + // must be ensured in the other parts of the upgrade that the upgrade transaction is not overridden. + if (!patchOnly) { + // If the previous upgrade had an L2 system upgrade transaction, we require that it is finalized. + // Note it is important to keep this check, as otherwise hyperchains might skip upgrades by overwriting + if (s.l2SystemContractsUpgradeTxHash != bytes32(0)) { + revert PreviousUpgradeNotFinalized(s.l2SystemContractsUpgradeTxHash); + } + if (s.l2SystemContractsUpgradeBatchNumber != 0) { + revert PreviousUpgradeBatchNotCleared(); + } + } + + s.protocolVersion = _newProtocolVersion; + emit NewProtocolVersion(previousProtocolVersion, _newProtocolVersion); + } +} diff --git a/contracts/l1-contracts/upgrades/DefaultUpgrade.sol b/contracts/l1-contracts/upgrades/DefaultUpgrade.sol new file mode 100644 index 0000000..c6ebb18 --- /dev/null +++ b/contracts/l1-contracts/upgrades/DefaultUpgrade.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {Diamond} from "../state-transition/libraries/Diamond.sol"; +import {BaseZkSyncUpgrade, ProposedUpgrade} from "./BaseZkSyncUpgrade.sol"; + +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +contract DefaultUpgrade is BaseZkSyncUpgrade { + /// @notice The main function that will be called by the upgrade proxy. + /// @param _proposedUpgrade The upgrade to be executed. + function upgrade(ProposedUpgrade calldata _proposedUpgrade) public override returns (bytes32) { + super.upgrade(_proposedUpgrade); + return Diamond.DIAMOND_INIT_SUCCESS_RETURN_VALUE; + } +} diff --git a/contracts/l1-contracts/upgrades/GenesisUpgrade.sol b/contracts/l1-contracts/upgrades/GenesisUpgrade.sol new file mode 100644 index 0000000..5e0ee28 --- /dev/null +++ b/contracts/l1-contracts/upgrades/GenesisUpgrade.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {Diamond} from "../state-transition/libraries/Diamond.sol"; +import {BaseZkSyncUpgradeGenesis} from "./BaseZkSyncUpgradeGenesis.sol"; +import {ProposedUpgrade} from "./IDefaultUpgrade.sol"; + +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +contract GenesisUpgrade is BaseZkSyncUpgradeGenesis { + /// @notice The main function that will be called by the upgrade proxy. + /// @param _proposedUpgrade The upgrade to be executed. + function upgrade(ProposedUpgrade calldata _proposedUpgrade) public override returns (bytes32) { + super.upgrade(_proposedUpgrade); + return Diamond.DIAMOND_INIT_SUCCESS_RETURN_VALUE; + } +} diff --git a/contracts/l1-contracts/upgrades/IDefaultUpgrade.sol b/contracts/l1-contracts/upgrades/IDefaultUpgrade.sol new file mode 100644 index 0000000..1f52931 --- /dev/null +++ b/contracts/l1-contracts/upgrades/IDefaultUpgrade.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {ProposedUpgrade} from "./BaseZkSyncUpgrade.sol"; + +interface IDefaultUpgrade { + function upgrade(ProposedUpgrade calldata _upgrade) external returns (bytes32); +} diff --git a/contracts/l1-contracts/upgrades/ZkSyncUpgradeErrors.sol b/contracts/l1-contracts/upgrades/ZkSyncUpgradeErrors.sol new file mode 100644 index 0000000..b30c882 --- /dev/null +++ b/contracts/l1-contracts/upgrades/ZkSyncUpgradeErrors.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.21; + +// 0x7a47c9a2 +error InvalidChainId(); +// 0xd7f8c13e +error PreviousUpgradeBatchNotCleared(); +// 0x3c43ccce +error ProtocolMajorVersionNotZero(); +// 0xd7f50a9d +error PatchCantSetUpgradeTxn(); +// 0xd2c011d6 +error L2UpgradeNonceNotEqualToNewProtocolVersion(uint256 nonce, uint256 protocolVersion); +// 0xcb5e4247 +error L2BytecodeHashMismatch(bytes32 expected, bytes32 provided); +// 0x88d7b498 +error ProtocolVersionTooSmall(); +// 0x56d45b12 +error ProtocolVersionTooBig(); +// 0x5c598b60 +error PreviousProtocolMajorVersionNotZero(); +// 0x72ea85ad +error NewProtocolMajorVersionNotZero(); +// 0xd328c12a +error ProtocolVersionMinorDeltaTooBig(uint256 limit, uint256 proposed); +// 0xe1a9736b +error ProtocolVersionDeltaTooLarge(uint256 _proposedDelta, uint256 _maxDelta); +// 0x6d172ab2 +error ProtocolVersionShouldBeGreater(uint256 _oldProtocolVersion, uint256 _newProtocolVersion); +// 0x559cc34e +error PatchUpgradeCantSetDefaultAccount(); +// 0x962fd7d0 +error PatchUpgradeCantSetBootloader(); +// 0x101ba748 +error PreviousUpgradeNotFinalized(bytes32 txHash); +// 0xa0f47245 +error PreviousUpgradeNotCleaned(); +// 0x07218375 +error UnexpectedNumberOfFactoryDeps(); +// 0x76da24b9 +error TooManyFactoryDeps(); +// 0x5cb29523 +error InvalidTxType(uint256 txType); +// 0x08753982 +error TimeNotReached(uint256 expectedTimestamp, uint256 actualTimestamp); +// 0xd92e233d +error ZeroAddress(); diff --git a/contracts/l1-contracts/vendor/AddressAliasHelper.sol b/contracts/l1-contracts/vendor/AddressAliasHelper.sol new file mode 100644 index 0000000..ad80f34 --- /dev/null +++ b/contracts/l1-contracts/vendor/AddressAliasHelper.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright 2019-2021, Offchain Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.21; + +library AddressAliasHelper { + uint160 private constant offset = uint160(0x1111000000000000000000000000000000001111); + + /// @notice Utility function converts the address that submitted a tx + /// to the inbox on L1 to the msg.sender viewed on L2 + /// @param l1Address the address in the L1 that triggered the tx to L2 + /// @return l2Address L2 address as viewed in msg.sender + function applyL1ToL2Alias(address l1Address) internal pure returns (address l2Address) { + unchecked { + l2Address = address(uint160(l1Address) + offset); + } + } + + /// @notice Utility function that converts the msg.sender viewed on L2 to the + /// address that submitted a tx to the inbox on L1 + /// @param l2Address L2 address as viewed in msg.sender + /// @return l1Address the address in the L1 that triggered the tx to L2 + function undoL1ToL2Alias(address l2Address) internal pure returns (address l1Address) { + unchecked { + l1Address = address(uint160(l2Address) - offset); + } + } + + /// @notice Utility function used to calculate the correct refund recipient + /// @param _refundRecipient the address that should receive the refund + /// @param _prevMsgSender the address that triggered the tx to L2 + /// @return _recipient the corrected address that should receive the refund + function actualRefundRecipient( + address _refundRecipient, + address _prevMsgSender + ) internal view returns (address _recipient) { + if (_refundRecipient == address(0)) { + // If the `_refundRecipient` is not provided, we use the `_prevMsgSender` as the recipient. + // solhint-disable avoid-tx-origin + // slither-disable-next-line tx-origin + _recipient = _prevMsgSender == tx.origin + ? _prevMsgSender + : AddressAliasHelper.applyL1ToL2Alias(_prevMsgSender); + // solhint-enable avoid-tx-origin + } else if (_refundRecipient.code.length > 0) { + // If the `_refundRecipient` is a smart contract, we apply the L1 to L2 alias to prevent foot guns. + _recipient = AddressAliasHelper.applyL1ToL2Alias(_refundRecipient); + } else { + _recipient = _refundRecipient; + } + } +} diff --git a/contracts/l2-contracts/Dependencies.sol b/contracts/l2-contracts/Dependencies.sol new file mode 100644 index 0000000..8a606d4 --- /dev/null +++ b/contracts/l2-contracts/Dependencies.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.20; + +/* solhint-disable-next-line no-unused-import */ +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts-v4/proxy/transparent/TransparentUpgradeableProxy.sol"; +/* solhint-disable-next-line no-unused-import */ +import {ProxyAdmin} from "@openzeppelin/contracts-v4/proxy/transparent/ProxyAdmin.sol"; diff --git a/contracts/l2-contracts/ForceDeployUpgrader.sol b/contracts/l2-contracts/ForceDeployUpgrader.sol new file mode 100644 index 0000000..46f9e4f --- /dev/null +++ b/contracts/l2-contracts/ForceDeployUpgrader.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +pragma solidity 0.8.24; + +import {IContractDeployer, DEPLOYER_SYSTEM_CONTRACT} from "./L2ContractHelper.sol"; + +/// @custom:security-contact security@matterlabs.dev +/// @notice The contract that calls force deployment during the L2 system contract upgrade. +/// @notice It is supposed to be used as an implementation of the ComplexUpgrader. +contract ForceDeployUpgrader { + /// @notice A function that performs force deploy + /// @param _forceDeployments The force deployments to perform. + function forceDeploy(IContractDeployer.ForceDeployment[] calldata _forceDeployments) external payable { + IContractDeployer(DEPLOYER_SYSTEM_CONTRACT).forceDeployOnAddresses{value: msg.value}(_forceDeployments); + } +} diff --git a/contracts/l2-contracts/L2ContractHelper.sol b/contracts/l2-contracts/L2ContractHelper.sol new file mode 100644 index 0000000..842d4b4 --- /dev/null +++ b/contracts/l2-contracts/L2ContractHelper.sol @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.20; + +/** + * @author Matter Labs + * @custom:security-contact security@matterlabs.dev + * @notice Smart contract for sending arbitrary length messages to L1 + * @dev by default ZkSync can send fixed-length messages on L1. + * A fixed length message has 4 parameters `senderAddress`, `isService`, `key`, `value`, + * the first one is taken from the context, the other three are chosen by the sender. + * @dev To send a variable-length message we use this trick: + * - This system contract accepts an arbitrary length message and sends a fixed length message with + * parameters `senderAddress == this`, `isService == 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. + */ +interface IL2Messenger { + /// @notice Sends an arbitrary length message to L1. + /// @param _message The variable length message to be sent to L1. + /// @return Returns the keccak256 hashed value of the message. + function sendToL1(bytes memory _message) external returns (bytes32); +} + +/** + * @author Matter Labs + * @custom:security-contact security@matterlabs.dev + * @notice Interface for the contract that is used to deploy contracts on L2. + */ +interface IContractDeployer { + /// @notice A struct that describes a forced deployment on an address. + /// @param bytecodeHash The bytecode hash to put on an address. + /// @param newAddress The address on which to deploy the bytecodehash to. + /// @param callConstructor Whether to run the constructor on the force deployment. + /// @param value The `msg.value` with which to initialize a contract. + /// @param input The constructor calldata. + struct ForceDeployment { + bytes32 bytecodeHash; + address newAddress; + bool callConstructor; + uint256 value; + bytes input; + } + + /// @notice This method is to be used only during an upgrade to set bytecodes on specific addresses. + /// @param _deployParams A set of parameters describing force deployment. + function forceDeployOnAddresses(ForceDeployment[] calldata _deployParams) external payable; + + /// @notice Creates a new contract at a determined address using the `CREATE2` salt on L2 + /// @param _salt a unique value to create the deterministic address of the new contract + /// @param _bytecodeHash the bytecodehash of the new contract to be deployed + /// @param _input the calldata to be sent to the constructor of the new contract + function create2(bytes32 _salt, bytes32 _bytecodeHash, bytes calldata _input) external returns (address); +} + +/** + * @author Matter Labs + * @custom:security-contact security@matterlabs.dev + * @notice Interface for the contract that is used to simulate ETH on L2. + */ +interface IBaseToken { + /// @notice Allows the withdrawal of ETH to a given L1 receiver along with an additional message. + /// @param _l1Receiver The address on L1 to receive the withdrawn ETH. + /// @param _additionalData Additional message or data to be sent alongside the withdrawal. + function withdrawWithMessage(address _l1Receiver, bytes memory _additionalData) external payable; +} + +uint160 constant SYSTEM_CONTRACTS_OFFSET = 0x8000; // 2^15 + +address constant BOOTLOADER_ADDRESS = address(SYSTEM_CONTRACTS_OFFSET + 0x01); +address constant MSG_VALUE_SYSTEM_CONTRACT = address(SYSTEM_CONTRACTS_OFFSET + 0x09); +address constant DEPLOYER_SYSTEM_CONTRACT = address(SYSTEM_CONTRACTS_OFFSET + 0x06); + +IL2Messenger constant L2_MESSENGER = IL2Messenger(address(SYSTEM_CONTRACTS_OFFSET + 0x08)); + +IBaseToken constant L2_BASE_TOKEN_ADDRESS = IBaseToken(address(SYSTEM_CONTRACTS_OFFSET + 0x0a)); + +/** + * @author Matter Labs + * @custom:security-contact security@matterlabs.dev + * @notice Helper library for working with L2 contracts on L1. + */ +library L2ContractHelper { + /// @dev The prefix used to create CREATE2 addresses. + bytes32 private constant CREATE2_PREFIX = keccak256("zksyncCreate2"); + + /// @notice Sends L2 -> L1 arbitrary-long message through the system contract messenger. + /// @param _message Data to be sent to L1. + /// @return keccak256 hash of the sent message. + function sendMessageToL1(bytes memory _message) internal returns (bytes32) { + return L2_MESSENGER.sendToL1(_message); + } + + /// @notice Computes the create2 address for a Layer 2 contract. + /// @param _sender The address of the contract creator. + /// @param _salt The salt value to use in the create2 address computation. + /// @param _bytecodeHash The contract bytecode hash. + /// @param _constructorInputHash The keccak256 hash of the constructor input data. + /// @return The create2 address of the contract. + /// NOTE: L2 create2 derivation is different from L1 derivation! + function computeCreate2Address( + address _sender, + bytes32 _salt, + bytes32 _bytecodeHash, + bytes32 _constructorInputHash + ) internal pure returns (address) { + bytes32 senderBytes = bytes32(uint256(uint160(_sender))); + bytes32 data = keccak256( + // solhint-disable-next-line func-named-parameters + bytes.concat(CREATE2_PREFIX, senderBytes, _salt, _bytecodeHash, _constructorInputHash) + ); + + return address(uint160(uint256(data))); + } +} + +/// @notice Structure used to represent a ZKsync transaction. +struct Transaction { + // The type of the transaction. + uint256 txType; + // The caller. + uint256 from; + // The callee. + uint256 to; + // The gasLimit to pass with the transaction. + // It has the same meaning as Ethereum's gasLimit. + uint256 gasLimit; + // The maximum amount of gas the user is willing to pay for a byte of pubdata. + uint256 gasPerPubdataByteLimit; + // The maximum fee per gas that the user is willing to pay. + // It is akin to EIP1559's maxFeePerGas. + uint256 maxFeePerGas; + // The maximum priority fee per gas that the user is willing to pay. + // It is akin to EIP1559's maxPriorityFeePerGas. + uint256 maxPriorityFeePerGas; + // The transaction's paymaster. If there is no paymaster, it is equal to 0. + uint256 paymaster; + // The nonce of the transaction. + uint256 nonce; + // The value to pass with the transaction. + uint256 value; + // In the future, we might want to add some + // new fields to the struct. The `txData` struct + // is to be passed to account and any changes to its structure + // would mean a breaking change to these accounts. In order to prevent this, + // we should keep some fields as "reserved". + // It is also recommended that their length is fixed, since + // it would allow easier proof integration (in case we will need + // some special circuit for preprocessing transactions). + uint256[4] reserved; + // The transaction's calldata. + bytes data; + // The signature of the transaction. + bytes signature; + // The properly formatted hashes of bytecodes that must be published on L1 + // with the inclusion of this transaction. Note, that a bytecode has been published + // before, the user won't pay fees for its republishing. + bytes32[] factoryDeps; + // The input to the paymaster. + bytes paymasterInput; + // Reserved dynamic type for the future use-case. Using it should be avoided, + // But it is still here, just in case we want to enable some additional functionality. + bytes reservedDynamic; +} diff --git a/contracts/l2-contracts/SystemContractsCaller.sol b/contracts/l2-contracts/SystemContractsCaller.sol new file mode 100644 index 0000000..ba31367 --- /dev/null +++ b/contracts/l2-contracts/SystemContractsCaller.sol @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: MIT + +// solhint-disable one-contract-per-file +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.20; + +import {MSG_VALUE_SYSTEM_CONTRACT} from "./L2ContractHelper.sol"; + +address constant SYSTEM_CALL_CALL_ADDRESS = address((1 << 16) - 11); +/// @dev If the bitwise AND of the extraAbi[2] param when calling the MSG_VALUE_SIMULATOR +/// is non-zero, the call will be assumed to be a system one. +uint256 constant MSG_VALUE_SIMULATOR_IS_SYSTEM_BIT = 1; + +/// @notice The way to forward the calldata: +/// - Use the current heap (i.e. the same as on EVM). +/// - Use the auxiliary heap. +/// - Forward via a pointer +/// @dev Note, that currently, users do not have access to the auxiliary +/// heap and so the only type of forwarding that will be used by the users +/// are UseHeap and ForwardFatPointer for forwarding a slice of the current calldata +/// to the next call. +enum CalldataForwardingMode { + UseHeap, + ForwardFatPointer, + UseAuxHeap +} + +/// @notice Error thrown a cast from uint256 to u32 is not possible. +error U32CastOverflow(); + +library Utils { + function safeCastToU32(uint256 _x) internal pure returns (uint32) { + if (_x > type(uint32).max) { + revert U32CastOverflow(); + } + + return uint32(_x); + } +} + +/// @notice The library contains the functions to make system calls. +/// @dev A more detailed description of the library and its methods can be found in the `system-contracts` repo. +library SystemContractsCaller { + function systemCall(uint32 gasLimit, address to, uint256 value, bytes memory data) internal returns (bool success) { + address callAddr = SYSTEM_CALL_CALL_ADDRESS; + + uint32 dataStart; + assembly { + dataStart := add(data, 0x20) + } + uint32 dataLength = Utils.safeCastToU32(data.length); + + uint256 farCallAbi = getFarCallABI({ + dataOffset: 0, + memoryPage: 0, + dataStart: dataStart, + dataLength: dataLength, + gasPassed: gasLimit, + // Only rollup is supported for now + shardId: 0, + forwardingMode: CalldataForwardingMode.UseHeap, + isConstructorCall: false, + isSystemCall: true + }); + + if (value == 0) { + // Doing the system call directly + assembly { + success := call(to, callAddr, 0, 0, farCallAbi, 0, 0) + } + } else { + address msgValueSimulator = MSG_VALUE_SYSTEM_CONTRACT; + // We need to supply the mask to the MsgValueSimulator to denote + // that the call should be a system one. + uint256 forwardMask = MSG_VALUE_SIMULATOR_IS_SYSTEM_BIT; + + assembly { + success := call(msgValueSimulator, callAddr, value, to, farCallAbi, forwardMask, 0) + } + } + } + + function systemCallWithReturndata( + uint32 gasLimit, + address to, + uint128 value, + bytes memory data + ) internal returns (bool success, bytes memory returnData) { + success = systemCall(gasLimit, to, value, data); + + uint256 size; + assembly { + size := returndatasize() + } + + returnData = new bytes(size); + assembly { + returndatacopy(add(returnData, 0x20), 0, size) + } + } + + function getFarCallABI( + uint32 dataOffset, + uint32 memoryPage, + uint32 dataStart, + uint32 dataLength, + uint32 gasPassed, + uint8 shardId, + CalldataForwardingMode forwardingMode, + bool isConstructorCall, + bool isSystemCall + ) internal pure returns (uint256 farCallAbi) { + // Fill in the call parameter fields + farCallAbi = getFarCallABIWithEmptyFatPointer({ + gasPassed: gasPassed, + shardId: shardId, + forwardingMode: forwardingMode, + isConstructorCall: isConstructorCall, + isSystemCall: isSystemCall + }); + // Fill in the fat pointer fields + farCallAbi |= dataOffset; + farCallAbi |= (uint256(memoryPage) << 32); + farCallAbi |= (uint256(dataStart) << 64); + farCallAbi |= (uint256(dataLength) << 96); + } + + function getFarCallABIWithEmptyFatPointer( + uint32 gasPassed, + uint8 shardId, + CalldataForwardingMode forwardingMode, + bool isConstructorCall, + bool isSystemCall + ) internal pure returns (uint256 farCallAbiWithEmptyFatPtr) { + farCallAbiWithEmptyFatPtr |= (uint256(gasPassed) << 192); + farCallAbiWithEmptyFatPtr |= (uint256(forwardingMode) << 224); + farCallAbiWithEmptyFatPtr |= (uint256(shardId) << 232); + if (isConstructorCall) { + farCallAbiWithEmptyFatPtr |= (1 << 240); + } + if (isSystemCall) { + farCallAbiWithEmptyFatPtr |= (1 << 248); + } + } +} diff --git a/contracts/l2-contracts/TestnetPaymaster.sol b/contracts/l2-contracts/TestnetPaymaster.sol new file mode 100644 index 0000000..54558fe --- /dev/null +++ b/contracts/l2-contracts/TestnetPaymaster.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.20; + +import {IERC20} from "@openzeppelin/contracts-v4/token/ERC20/IERC20.sol"; + +import {IPaymaster, ExecutionResult, PAYMASTER_VALIDATION_SUCCESS_MAGIC} from "./interfaces/IPaymaster.sol"; +import {IPaymasterFlow} from "./interfaces/IPaymasterFlow.sol"; +import {Transaction, BOOTLOADER_ADDRESS} from "./L2ContractHelper.sol"; +import {Unauthorized, InvalidInput, InsufficientAllowance, FailedToTransferTokens, UnsupportedPaymasterFlow} from "./errors/L2ContractErrors.sol"; + +// This is a dummy paymaster. It expects the paymasterInput to contain its "signature" as well as the needed exchange rate. +// It supports only approval-based paymaster flow. +contract TestnetPaymaster is IPaymaster { + function validateAndPayForPaymasterTransaction( + bytes32, + bytes32, + Transaction calldata _transaction + ) external payable returns (bytes4 magic, bytes memory) { + // By default we consider the transaction as accepted. + magic = PAYMASTER_VALIDATION_SUCCESS_MAGIC; + + if (msg.sender != BOOTLOADER_ADDRESS) { + revert Unauthorized(msg.sender); + } + + if (_transaction.paymasterInput.length < 4) { + revert InvalidInput(); + } + + bytes4 paymasterInputSelector = bytes4(_transaction.paymasterInput[0:4]); + if (paymasterInputSelector == IPaymasterFlow.approvalBased.selector) { + // While the actual data consists of address, uint256 and bytes data, + // the data is not needed for the testnet paymaster + (address token, uint256 amount, ) = abi.decode(_transaction.paymasterInput[4:], (address, uint256, bytes)); + + // Firstly, we verify that the user has provided enough allowance + address userAddress = address(uint160(_transaction.from)); + address thisAddress = address(this); + + uint256 providedAllowance = IERC20(token).allowance(userAddress, thisAddress); + if (providedAllowance < amount) { + revert InsufficientAllowance(providedAllowance, amount); + } + + // The testnet paymaster exchanges X wei of the token to the X wei of ETH. + uint256 requiredETH = _transaction.gasLimit * _transaction.maxFeePerGas; + if (amount < requiredETH) { + // Important note: while this clause definitely means that the user + // has underpaid the paymaster and the transaction should not accepted, + // we do not want the transaction to revert, because for fee estimation + // we allow users to provide smaller amount of funds then necessary to preserve + // the property that if using X gas the transaction success, then it will succeed with X+1 gas. + magic = bytes4(0); + } + + // Pulling all the tokens from the user + try IERC20(token).transferFrom(userAddress, thisAddress, amount) {} catch (bytes memory revertReason) { + // If the revert reason is empty or represented by just a function selector, + // we replace the error with a more user-friendly message + if (revertReason.length <= 4) { + revert FailedToTransferTokens(token, thisAddress, amount); + } else { + assembly { + revert(add(0x20, revertReason), mload(revertReason)) + } + } + } + + // The bootloader never returns any data, so it can safely be ignored here. + (bool success, ) = payable(BOOTLOADER_ADDRESS).call{value: requiredETH}(""); + if (!success) { + revert FailedToTransferTokens(address(0), BOOTLOADER_ADDRESS, requiredETH); + } + } else { + revert UnsupportedPaymasterFlow(); + } + } + + function postTransaction( + bytes calldata _context, + Transaction calldata _transaction, + bytes32, + bytes32, + ExecutionResult _txResult, + uint256 _maxRefundedGas + ) external payable override { + // Refunds are not supported yet. + } + + receive() external payable {} +} diff --git a/contracts/l2-contracts/bridge/L2SharedBridge.sol b/contracts/l2-contracts/bridge/L2SharedBridge.sol new file mode 100644 index 0000000..d4e7b79 --- /dev/null +++ b/contracts/l2-contracts/bridge/L2SharedBridge.sol @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {Initializable} from "@openzeppelin/contracts-v4/proxy/utils/Initializable.sol"; +import {BeaconProxy} from "@openzeppelin/contracts-v4/proxy/beacon/BeaconProxy.sol"; +import {UpgradeableBeacon} from "@openzeppelin/contracts-v4/proxy/beacon/UpgradeableBeacon.sol"; + +import {IL1ERC20Bridge} from "./interfaces/IL1ERC20Bridge.sol"; +import {IL2SharedBridge} from "./interfaces/IL2SharedBridge.sol"; +import {IL2StandardToken} from "./interfaces/IL2StandardToken.sol"; + +import {L2StandardERC20} from "./L2StandardERC20.sol"; +import {AddressAliasHelper} from "../vendor/AddressAliasHelper.sol"; +import {L2ContractHelper, DEPLOYER_SYSTEM_CONTRACT, IContractDeployer} from "../L2ContractHelper.sol"; +import {SystemContractsCaller} from "../SystemContractsCaller.sol"; + +import {ZeroAddress, EmptyBytes32, Unauthorized, AddressMismatch, AmountMustBeGreaterThanZero, DeployFailed} from "../errors/L2ContractErrors.sol"; + +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +/// @notice The "default" bridge implementation for the ERC20 tokens. Note, that it does not +/// support any custom token logic, i.e. rebase tokens' functionality is not supported. +contract L2SharedBridge is IL2SharedBridge, Initializable { + /// @dev The address of the L1 shared bridge counterpart. + address public override l1SharedBridge; + + /// @dev Contract that stores the implementation address for token. + /// @dev For more details see https://docs.openzeppelin.com/contracts/3.x/api/proxy#UpgradeableBeacon. + UpgradeableBeacon public l2TokenBeacon; + + /// @dev Bytecode hash of the proxy for tokens deployed by the bridge. + bytes32 internal l2TokenProxyBytecodeHash; + + /// @dev A mapping l2 token address => l1 token address + mapping(address l2TokenAddress => address l1TokenAddress) public override l1TokenAddress; + + /// @dev The address of the legacy L1 erc20 bridge counterpart. + /// This is non-zero only on Era, and should not be renamed for backward compatibility with the SDKs. + address public override l1Bridge; + + /// @dev Contract is expected to be used as proxy implementation. + /// @dev Disable the initialization to prevent Parity hack. + uint256 public immutable ERA_CHAIN_ID; + + constructor(uint256 _eraChainId) { + ERA_CHAIN_ID = _eraChainId; + _disableInitializers(); + } + + /// @notice Initializes the bridge contract for later use. Expected to be used in the proxy. + /// @param _l1SharedBridge The address of the L1 Bridge contract. + /// @param _l1Bridge The address of the legacy L1 Bridge contract. + /// @param _l2TokenProxyBytecodeHash The bytecode hash of the proxy for tokens deployed by the bridge. + /// @param _aliasedOwner The address of the governor contract. + function initialize( + address _l1SharedBridge, + address _l1Bridge, + bytes32 _l2TokenProxyBytecodeHash, + address _aliasedOwner + ) external reinitializer(2) { + if (_l1SharedBridge == address(0)) { + revert ZeroAddress(); + } + + if (_l2TokenProxyBytecodeHash == bytes32(0)) { + revert EmptyBytes32(); + } + + if (_aliasedOwner == address(0)) { + revert ZeroAddress(); + } + + l1SharedBridge = _l1SharedBridge; + + if (block.chainid != ERA_CHAIN_ID) { + address l2StandardToken = address(new L2StandardERC20{salt: bytes32(0)}()); + l2TokenBeacon = new UpgradeableBeacon{salt: bytes32(0)}(l2StandardToken); + l2TokenProxyBytecodeHash = _l2TokenProxyBytecodeHash; + l2TokenBeacon.transferOwnership(_aliasedOwner); + } else { + if (_l1Bridge == address(0)) { + revert ZeroAddress(); + } + l1Bridge = _l1Bridge; + // l2StandardToken and l2TokenBeacon are already deployed on ERA, and stored in the proxy + } + } + + /// @notice Finalize the deposit and mint funds + /// @param _l1Sender The account address that initiated the deposit on L1 + /// @param _l2Receiver The account address that would receive minted ether + /// @param _l1Token The address of the token that was locked on the L1 + /// @param _amount Total amount of tokens deposited from L1 + /// @param _data The additional data that user can pass with the deposit + function finalizeDeposit( + address _l1Sender, + address _l2Receiver, + address _l1Token, + uint256 _amount, + bytes calldata _data + ) external override { + // Only the L1 bridge counterpart can initiate and finalize the deposit. + if ( + AddressAliasHelper.undoL1ToL2Alias(msg.sender) != l1Bridge && + AddressAliasHelper.undoL1ToL2Alias(msg.sender) != l1SharedBridge + ) { + revert Unauthorized(msg.sender); + } + + address expectedL2Token = l2TokenAddress(_l1Token); + address currentL1Token = l1TokenAddress[expectedL2Token]; + if (currentL1Token == address(0)) { + address deployedToken = _deployL2Token(_l1Token, _data); + if (deployedToken != expectedL2Token) { + revert AddressMismatch(expectedL2Token, deployedToken); + } + + l1TokenAddress[expectedL2Token] = _l1Token; + } else { + if (currentL1Token != _l1Token) { + revert AddressMismatch(_l1Token, currentL1Token); + } + } + + IL2StandardToken(expectedL2Token).bridgeMint(_l2Receiver, _amount); + emit FinalizeDeposit(_l1Sender, _l2Receiver, expectedL2Token, _amount); + } + + /// @dev Deploy and initialize the L2 token for the L1 counterpart + function _deployL2Token(address _l1Token, bytes calldata _data) internal returns (address) { + bytes32 salt = _getCreate2Salt(_l1Token); + + BeaconProxy l2Token = _deployBeaconProxy(salt); + L2StandardERC20(address(l2Token)).bridgeInitialize(_l1Token, _data); + + return address(l2Token); + } + + /// @notice Initiates a withdrawal by burning funds on the contract and sending the message to L1 + /// where tokens would be unlocked + /// @param _l1Receiver The account address that should receive funds on L1 + /// @param _l2Token The L2 token address which is withdrawn + /// @param _amount The total amount of tokens to be withdrawn + function withdraw(address _l1Receiver, address _l2Token, uint256 _amount) external override { + if (_amount == 0) { + revert AmountMustBeGreaterThanZero(); + } + + IL2StandardToken(_l2Token).bridgeBurn(msg.sender, _amount); + + address l1Token = l1TokenAddress[_l2Token]; + if (l1Token == address(0)) { + revert ZeroAddress(); + } + + bytes memory message = _getL1WithdrawMessage(_l1Receiver, l1Token, _amount); + L2ContractHelper.sendMessageToL1(message); + + emit WithdrawalInitiated(msg.sender, _l1Receiver, _l2Token, _amount); + } + + /// @dev Encode the message for l2ToL1log sent with withdraw initialization + function _getL1WithdrawMessage( + address _to, + address _l1Token, + uint256 _amount + ) internal pure returns (bytes memory) { + // note we use the IL1ERC20Bridge.finalizeWithdrawal function selector to specify the selector for L1<>L2 messages, + // and we use this interface so that when the switch happened the old messages could be processed + return abi.encodePacked(IL1ERC20Bridge.finalizeWithdrawal.selector, _to, _l1Token, _amount); + } + + /// @return Address of an L2 token counterpart + function l2TokenAddress(address _l1Token) public view override returns (address) { + bytes32 constructorInputHash = keccak256(abi.encode(address(l2TokenBeacon), "")); + bytes32 salt = _getCreate2Salt(_l1Token); + return + L2ContractHelper.computeCreate2Address(address(this), salt, l2TokenProxyBytecodeHash, constructorInputHash); + } + + /// @dev Convert the L1 token address to the create2 salt of deployed L2 token + function _getCreate2Salt(address _l1Token) internal pure returns (bytes32 salt) { + salt = bytes32(uint256(uint160(_l1Token))); + } + + /// @dev Deploy the beacon proxy for the L2 token, while using ContractDeployer system contract. + /// @dev This function uses raw call to ContractDeployer to make sure that exactly `l2TokenProxyBytecodeHash` is used + /// for the code of the proxy. + function _deployBeaconProxy(bytes32 salt) internal returns (BeaconProxy proxy) { + (bool success, bytes memory returndata) = SystemContractsCaller.systemCallWithReturndata( + uint32(gasleft()), + DEPLOYER_SYSTEM_CONTRACT, + 0, + abi.encodeCall( + IContractDeployer.create2, + (salt, l2TokenProxyBytecodeHash, abi.encode(address(l2TokenBeacon), "")) + ) + ); + + // The deployment should be successful and return the address of the proxy + if (!success) { + revert DeployFailed(); + } + proxy = BeaconProxy(abi.decode(returndata, (address))); + } +} diff --git a/contracts/l2-contracts/bridge/L2StandardERC20.sol b/contracts/l2-contracts/bridge/L2StandardERC20.sol new file mode 100644 index 0000000..90db458 --- /dev/null +++ b/contracts/l2-contracts/bridge/L2StandardERC20.sol @@ -0,0 +1,198 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {ERC20PermitUpgradeable} from "@openzeppelin/contracts-upgradeable-v4/token/ERC20/extensions/draft-ERC20PermitUpgradeable.sol"; +import {UpgradeableBeacon} from "@openzeppelin/contracts-v4/proxy/beacon/UpgradeableBeacon.sol"; +import {ERC1967Upgrade} from "@openzeppelin/contracts-v4/proxy/ERC1967/ERC1967Upgrade.sol"; + +import {IL2StandardToken} from "./interfaces/IL2StandardToken.sol"; +import {ZeroAddress, Unauthorized, NonSequentialVersion} from "../errors/L2ContractErrors.sol"; + +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +/// @notice The ERC20 token implementation, that is used in the "default" ERC20 bridge. Note, that it does not +/// support any custom token logic, i.e. rebase tokens' functionality is not supported. +contract L2StandardERC20 is ERC20PermitUpgradeable, IL2StandardToken, ERC1967Upgrade { + /// @dev Describes whether there is a specific getter in the token. + /// @notice Used to explicitly separate which getters the token has and which it does not. + /// @notice Different tokens in L1 can implement or not implement getter function as `name`/`symbol`/`decimals`, + /// @notice Our goal is to store all the getters that L1 token implements, and for others, we keep it as an unimplemented method. + struct ERC20Getters { + bool ignoreName; + bool ignoreSymbol; + bool ignoreDecimals; + } + + ERC20Getters private availableGetters; + + /// @dev The decimals of the token, that are used as a value for `decimals` getter function. + /// @notice A private variable is used only for decimals, but not for `name` and `symbol`, because standard + /// @notice OpenZeppelin token represents `name` and `symbol` as storage variables and `decimals` as constant. + uint8 private decimals_; + + /// @dev Address of the L2 bridge that is used as trustee who can mint/burn tokens + address public override l2Bridge; + + /// @dev Address of the L1 token that can be deposited to mint this L2 token + address public override l1Address; + + /// @dev Contract is expected to be used as proxy implementation. + constructor() { + // Disable initialization to prevent Parity hack. + _disableInitializers(); + } + + /// @notice Initializes a contract token for later use. Expected to be used in the proxy. + /// @dev Stores the L1 address of the bridge and set `name`/`symbol`/`decimals` getters that L1 token has. + /// @param _l1Address Address of the L1 token that can be deposited to mint this L2 token + /// @param _data The additional data that the L1 bridge provide for initialization. + /// In this case, it is packed `name`/`symbol`/`decimals` of the L1 token. + function bridgeInitialize(address _l1Address, bytes calldata _data) external initializer { + if (_l1Address == address(0)) { + revert ZeroAddress(); + } + l1Address = _l1Address; + + l2Bridge = msg.sender; + + // We parse the data exactly as they were created on the L1 bridge + (bytes memory nameBytes, bytes memory symbolBytes, bytes memory decimalsBytes) = abi.decode( + _data, + (bytes, bytes, bytes) + ); + + ERC20Getters memory getters; + string memory decodedName; + string memory decodedSymbol; + + // L1 bridge didn't check if the L1 token return values with proper types for `name`/`symbol`/`decimals` + // That's why we need to try to decode them, and if it works out, set the values as getters. + + // NOTE: Solidity doesn't have a convenient way to try to decode a value: + // - Decode them manually, i.e. write a function that will validate that data in the correct format + // and return decoded value and a boolean value - whether it was possible to decode. + // - Use the standard abi.decode method, but wrap it into an external call in which error can be handled. + // We use the second option here. + + try this.decodeString(nameBytes) returns (string memory nameString) { + decodedName = nameString; + } catch { + getters.ignoreName = true; + } + + try this.decodeString(symbolBytes) returns (string memory symbolString) { + decodedSymbol = symbolString; + } catch { + getters.ignoreSymbol = true; + } + + // Set decoded values for name and symbol. + __ERC20_init_unchained(decodedName, decodedSymbol); + + // Set the name for EIP-712 signature. + __ERC20Permit_init(decodedName); + + try this.decodeUint8(decimalsBytes) returns (uint8 decimalsUint8) { + // Set decoded value for decimals. + decimals_ = decimalsUint8; + } catch { + getters.ignoreDecimals = true; + } + + availableGetters = getters; + emit BridgeInitialize(_l1Address, decodedName, decodedSymbol, decimals_); + } + + /// @notice A method to be called by the governor to update the token's metadata. + /// @param _availableGetters The getters that the token has. + /// @param _newName The new name of the token. + /// @param _newSymbol The new symbol of the token. + /// @param _version The version of the token that will be initialized. + /// @dev The _version must be exactly the version higher by 1 than the current version. This is needed + /// to ensure that the governor can not accidentally disable future reinitialization of the token. + function reinitializeToken( + ERC20Getters calldata _availableGetters, + string calldata _newName, + string calldata _newSymbol, + uint8 _version + ) external onlyNextVersion(_version) reinitializer(_version) { + // It is expected that this token is deployed as a beacon proxy, so we'll + // allow the governor of the beacon to reinitialize the token. + address beaconAddress = _getBeacon(); + if (msg.sender != UpgradeableBeacon(beaconAddress).owner()) { + revert Unauthorized(msg.sender); + } + + __ERC20_init_unchained(_newName, _newSymbol); + __ERC20Permit_init(_newName); + availableGetters = _availableGetters; + + emit BridgeInitialize(l1Address, _newName, _newSymbol, decimals_); + } + + modifier onlyBridge() { + if (msg.sender != l2Bridge) { + revert Unauthorized(msg.sender); + } + _; + } + + modifier onlyNextVersion(uint8 _version) { + // The version should be incremented by 1. Otherwise, the governor risks disabling + // future reinitialization of the token by providing too large a version. + if (_version != _getInitializedVersion() + 1) { + revert NonSequentialVersion(); + } + _; + } + + /// @dev Mint tokens to a given account. + /// @param _to The account that will receive the created tokens. + /// @param _amount The amount that will be created. + /// @notice Should be called by bridge after depositing tokens from L1. + function bridgeMint(address _to, uint256 _amount) external override onlyBridge { + _mint(_to, _amount); + emit BridgeMint(_to, _amount); + } + + /// @dev Burn tokens from a given account. + /// @param _from The account from which tokens will be burned. + /// @param _amount The amount that will be burned. + /// @notice Should be called by bridge before withdrawing tokens to L1. + function bridgeBurn(address _from, uint256 _amount) external override onlyBridge { + _burn(_from, _amount); + emit BridgeBurn(_from, _amount); + } + + function name() public view override returns (string memory) { + // If method is not available, behave like a token that does not implement this method - revert on call. + // solhint-disable-next-line reason-string, gas-custom-errors + if (availableGetters.ignoreName) revert(); + return super.name(); + } + + function symbol() public view override returns (string memory) { + // If method is not available, behave like a token that does not implement this method - revert on call. + // solhint-disable-next-line reason-string, gas-custom-errors + if (availableGetters.ignoreSymbol) revert(); + return super.symbol(); + } + + function decimals() public view override returns (uint8) { + // If method is not available, behave like a token that does not implement this method - revert on call. + // solhint-disable-next-line reason-string, gas-custom-errors + if (availableGetters.ignoreDecimals) revert(); + return decimals_; + } + + /// @dev External function to decode a string from bytes. + function decodeString(bytes calldata _input) external pure returns (string memory result) { + (result) = abi.decode(_input, (string)); + } + + /// @dev External function to decode a uint8 from bytes. + function decodeUint8(bytes calldata _input) external pure returns (uint8 result) { + (result) = abi.decode(_input, (uint8)); + } +} diff --git a/contracts/l2-contracts/bridge/L2WrappedBaseToken.sol b/contracts/l2-contracts/bridge/L2WrappedBaseToken.sol new file mode 100644 index 0000000..8ff9f81 --- /dev/null +++ b/contracts/l2-contracts/bridge/L2WrappedBaseToken.sol @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {ERC20PermitUpgradeable} from "@openzeppelin/contracts-upgradeable-v4/token/ERC20/extensions/draft-ERC20PermitUpgradeable.sol"; + +import {IL2WrappedBaseToken} from "./interfaces/IL2WrappedBaseToken.sol"; +import {IL2StandardToken} from "./interfaces/IL2StandardToken.sol"; + +import {ZeroAddress, Unauthorized, UnimplementedMessage, BRIDGE_MINT_NOT_IMPLEMENTED, WithdrawFailed} from "../errors/L2ContractErrors.sol"; + +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +/// @notice The canonical implementation of the WETH token. +/// @dev The idea is to replace the legacy WETH9 (which has well-known issues) with something better. +/// This implementation has the following differences from the WETH9: +/// - It does not have a silent fallback method and will revert if it's called for a method it hasn't implemented. +/// - It implements `receive` method to allow users to deposit ether directly. +/// - It implements `permit` method to allow users to sign a message instead of calling `approve`. +/// - It implements `depositTo` method to allow users to deposit to another address. +/// - It implements `withdrawTo` method to allow users to withdraw to another address. +/// +/// Note: This is an upgradeable contract. In the future, we will remove upgradeability to make it trustless. +/// But for now, when the Rollup has instant upgradability, we leave the possibility of upgrading to improve the contract if needed. +contract L2WrappedBaseToken is ERC20PermitUpgradeable, IL2WrappedBaseToken, IL2StandardToken { + /// @dev Address of the L2 WETH Bridge. + address public override l2Bridge; + + /// @dev Address of the L1 base token. It can be deposited to mint this L2 token. + address public override l1Address; + + /// @dev Contract is expected to be used as proxy implementation. + constructor() { + // Disable initialization to prevent Parity hack. + _disableInitializers(); + } + + /// @notice Initializes a contract token for later use. Expected to be used in the proxy. + /// @notice This function is used to integrate the previously deployed WETH token with the bridge. + /// @dev Sets up `name`/`symbol`/`decimals` getters. + /// @param name_ The name of the token. + /// @param symbol_ The symbol of the token. + /// @param _l2Bridge Address of the L2 bridge + /// @param _l1Address Address of the L1 token that can be deposited to mint this L2 WETH. + /// Note: The decimals are hardcoded to 18, the same as on Ether. + function initializeV2( + string calldata name_, + string calldata symbol_, + address _l2Bridge, + address _l1Address + ) external reinitializer(2) { + if (_l2Bridge == address(0)) { + revert ZeroAddress(); + } + + if (_l1Address == address(0)) { + revert ZeroAddress(); + } + l2Bridge = _l2Bridge; + l1Address = _l1Address; + + // Set decoded values for name and symbol. + __ERC20_init_unchained(name_, symbol_); + + // Set the name for EIP-712 signature. + __ERC20Permit_init(name_); + + emit Initialize(name_, symbol_, 18); + } + + modifier onlyBridge() { + if (msg.sender != l2Bridge) { + revert Unauthorized(msg.sender); + } + _; + } + + /// @notice Function for minting tokens on L2, implemented only to be compatible with IL2StandardToken interface. + /// Always reverts instead of minting anything! + /// Note: Use `deposit`/`depositTo` methods instead. + // solhint-disable-next-line no-unused-vars + function bridgeMint(address _to, uint256 _amount) external override onlyBridge { + revert UnimplementedMessage(BRIDGE_MINT_NOT_IMPLEMENTED); + } + + /// @dev Burn tokens from a given account and send the same amount of Ether to the bridge. + /// @param _from The account from which tokens will be burned. + /// @param _amount The amount that will be burned. + /// @notice Should be called by the bridge before withdrawing tokens to L1. + function bridgeBurn(address _from, uint256 _amount) external override onlyBridge { + _burn(_from, _amount); + // sends Ether to the bridge + (bool success, ) = msg.sender.call{value: _amount}(""); + if (!success) { + revert WithdrawFailed(); + } + + emit BridgeBurn(_from, _amount); + } + + /// @notice Deposit Ether to mint WETH. + function deposit() external payable override { + depositTo(msg.sender); + } + + /// @notice Withdraw WETH to get Ether. + function withdraw(uint256 _amount) external override { + withdrawTo(msg.sender, _amount); + } + + /// @notice Deposit Ether to mint WETH to a given account. + function depositTo(address _to) public payable override { + _mint(_to, msg.value); + } + + /// @notice Withdraw WETH to get Ether to a given account. + /// burns sender's tokens and sends Ether to the given account + function withdrawTo(address _to, uint256 _amount) public override { + _burn(msg.sender, _amount); + (bool success, ) = _to.call{value: _amount}(""); + if (!success) { + revert WithdrawFailed(); + } + } + + /// @dev Fallback function to allow receiving Ether. + receive() external payable { + depositTo(msg.sender); + } +} diff --git a/contracts/l2-contracts/bridge/interfaces/IL1ERC20Bridge.sol b/contracts/l2-contracts/bridge/interfaces/IL1ERC20Bridge.sol new file mode 100644 index 0000000..84d4122 --- /dev/null +++ b/contracts/l2-contracts/bridge/interfaces/IL1ERC20Bridge.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.20; + +/// @author Matter Labs +// note we use the IL1ERC20Bridge only to send L1<>L2 messages, +// and we use this interface so that when the switch happened the old messages could be processed +interface IL1ERC20Bridge { + function finalizeWithdrawal( + uint256 _l2BatchNumber, + uint256 _l2MessageIndex, + uint16 _l2TxNumberInBatch, + bytes calldata _message, + bytes32[] calldata _merkleProof + ) external; +} diff --git a/contracts/l2-contracts/bridge/interfaces/IL1SharedBridge.sol b/contracts/l2-contracts/bridge/interfaces/IL1SharedBridge.sol new file mode 100644 index 0000000..cc468ab --- /dev/null +++ b/contracts/l2-contracts/bridge/interfaces/IL1SharedBridge.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.20; + +/// @title L1 Bridge contract interface +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +interface IL1SharedBridge { + function finalizeWithdrawal( + uint256 _chainId, + uint256 _l2BatchNumber, + uint256 _l2MessageIndex, + uint16 _l2TxNumberInBatch, + bytes calldata _message, + bytes32[] calldata _merkleProof + ) external; +} diff --git a/contracts/l2-contracts/bridge/interfaces/IL2SharedBridge.sol b/contracts/l2-contracts/bridge/interfaces/IL2SharedBridge.sol new file mode 100644 index 0000000..ee31f66 --- /dev/null +++ b/contracts/l2-contracts/bridge/interfaces/IL2SharedBridge.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.20; + +/// @author Matter Labs +interface IL2SharedBridge { + event FinalizeDeposit( + address indexed l1Sender, + address indexed l2Receiver, + address indexed l2Token, + uint256 amount + ); + + event WithdrawalInitiated( + address indexed l2Sender, + address indexed l1Receiver, + address indexed l2Token, + uint256 amount + ); + + function finalizeDeposit( + address _l1Sender, + address _l2Receiver, + address _l1Token, + uint256 _amount, + bytes calldata _data + ) external; + + function withdraw(address _l1Receiver, address _l2Token, uint256 _amount) external; + + function l1TokenAddress(address _l2Token) external view returns (address); + + function l2TokenAddress(address _l1Token) external view returns (address); + + function l1Bridge() external view returns (address); + + function l1SharedBridge() external view returns (address); +} diff --git a/contracts/l2-contracts/bridge/interfaces/IL2StandardToken.sol b/contracts/l2-contracts/bridge/interfaces/IL2StandardToken.sol new file mode 100644 index 0000000..6fefafa --- /dev/null +++ b/contracts/l2-contracts/bridge/interfaces/IL2StandardToken.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.20; + +interface IL2StandardToken { + event BridgeInitialize(address indexed l1Token, string name, string symbol, uint8 decimals); + + event BridgeMint(address indexed _account, uint256 _amount); + + event BridgeBurn(address indexed _account, uint256 _amount); + + function bridgeMint(address _account, uint256 _amount) external; + + function bridgeBurn(address _account, uint256 _amount) external; + + function l1Address() external view returns (address); + + function l2Bridge() external view returns (address); +} diff --git a/contracts/l2-contracts/bridge/interfaces/IL2WrappedBaseToken.sol b/contracts/l2-contracts/bridge/interfaces/IL2WrappedBaseToken.sol new file mode 100644 index 0000000..ae7e1a9 --- /dev/null +++ b/contracts/l2-contracts/bridge/interfaces/IL2WrappedBaseToken.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.20; + +interface IL2WrappedBaseToken { + event Initialize(string name, string symbol, uint8 decimals); + + function deposit() external payable; + + function withdraw(uint256 _amount) external; + + function depositTo(address _to) external payable; + + function withdrawTo(address _to, uint256 _amount) external; +} diff --git a/contracts/l2-contracts/dev-contracts/DevL2SharedBridge.sol b/contracts/l2-contracts/dev-contracts/DevL2SharedBridge.sol new file mode 100644 index 0000000..e93d5c9 --- /dev/null +++ b/contracts/l2-contracts/dev-contracts/DevL2SharedBridge.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {L2SharedBridge} from "../bridge/L2SharedBridge.sol"; +import {L2StandardERC20} from "../bridge/L2StandardERC20.sol"; +import {UpgradeableBeacon} from "@openzeppelin/contracts-v4/proxy/beacon/UpgradeableBeacon.sol"; + +/// @author Matter Labs +/// @notice The implementation of the shared bridge that allows setting legacy bridge. Must only be used in local testing environments. +contract DevL2SharedBridge is L2SharedBridge { + constructor(uint256 _eraChainId) L2SharedBridge(_eraChainId) {} + + function initializeDevBridge( + address _l1SharedBridge, + address _l1Bridge, + bytes32 _l2TokenProxyBytecodeHash, + address _aliasedOwner + ) external reinitializer(2) { + l1SharedBridge = _l1SharedBridge; + + address l2StandardToken = address(new L2StandardERC20{salt: bytes32(0)}()); + l2TokenBeacon = new UpgradeableBeacon{salt: bytes32(0)}(l2StandardToken); + l2TokenProxyBytecodeHash = _l2TokenProxyBytecodeHash; + l2TokenBeacon.transferOwnership(_aliasedOwner); + + // Unfortunately the `l1Bridge` is not an internal variable in the parent contract. + // To keep the changes to the production code minimal, we'll just manually set the variable here. + assembly { + sstore(4, _l1Bridge) + } + } +} diff --git a/contracts/l2-contracts/errors/L2ContractErrors.sol b/contracts/l2-contracts/errors/L2ContractErrors.sol new file mode 100644 index 0000000..c5177f3 --- /dev/null +++ b/contracts/l2-contracts/errors/L2ContractErrors.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.20; + +// 0x1f73225f +error AddressMismatch(address expected, address supplied); +// 0x5e85ae73 +error AmountMustBeGreaterThanZero(); +// 0xb4f54111 +error DeployFailed(); +// 0x7138356f +error EmptyAddress(); +// 0x1c25715b +error EmptyBytes32(); +// 0x1bdfd505 +error FailedToTransferTokens(address tokenContract, address to, uint256 amount); +// 0x2a1b2dd8 +error InsufficientAllowance(uint256 providedAllowance, uint256 requiredAmount); +// 0xcbd9d2e0 +error InvalidCaller(address); +// 0xb4fa3fb3 +error InvalidInput(); +// 0x0ac76f01 +error NonSequentialVersion(); +// 0x8e4a23d6 +error Unauthorized(address); +// 0x6e128399 +error Unimplemented(); +// 0xa4dde386 +error UnimplementedMessage(string message); +// 0xff15b069 +error UnsupportedPaymasterFlow(); +// 0x750b219c +error WithdrawFailed(); +// 0xd92e233d +error ZeroAddress(); + +string constant BRIDGE_MINT_NOT_IMPLEMENTED = "bridgeMint is not implemented! Use deposit/depositTo methods instead."; diff --git a/contracts/l2-contracts/interfaces/IPaymaster.sol b/contracts/l2-contracts/interfaces/IPaymaster.sol new file mode 100644 index 0000000..ed7e5c5 --- /dev/null +++ b/contracts/l2-contracts/interfaces/IPaymaster.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.20; + +import {Transaction} from "../L2ContractHelper.sol"; + +enum ExecutionResult { + Revert, + Success +} + +bytes4 constant PAYMASTER_VALIDATION_SUCCESS_MAGIC = IPaymaster.validateAndPayForPaymasterTransaction.selector; + +interface IPaymaster { + /// @dev Called by the bootloader to verify that the paymaster agrees to pay for the + /// fee for the transaction. This transaction should also send the necessary amount of funds onto the bootloader + /// address. + /// @param _txHash The hash of the transaction + /// @param _suggestedSignedHash The hash of the transaction that is signed by an EOA + /// @param _transaction The transaction itself. + /// @return magic The value that should be equal to the signature of the validateAndPayForPaymasterTransaction + /// if the paymaster agrees to pay for the transaction. + /// @return context The "context" of the transaction: an array of bytes of length at most 1024 bytes, which will be + /// passed to the `postTransaction` method of the account. + /// @dev The developer should strive to preserve as many steps as possible both for valid + /// and invalid transactions as this very method is also used during the gas fee estimation + /// (without some of the necessary data, e.g. signature). + function validateAndPayForPaymasterTransaction( + bytes32 _txHash, + bytes32 _suggestedSignedHash, + Transaction calldata _transaction + ) external payable returns (bytes4 magic, bytes memory context); + + /// @dev Called by the bootloader after the execution of the transaction. Please note that + /// there is no guarantee that this method will be called at all. Unlike the original EIP4337, + /// this method won't be called if the transaction execution results in out-of-gas. + /// @param _context, the context of the execution, returned by the "validateAndPayForPaymasterTransaction" method. + /// @param _transaction, the users' transaction. + /// @param _txResult, the result of the transaction execution (success or failure). + /// @param _maxRefundedGas, the upper bound on the amount of gas that could be refunded to the paymaster. + /// @dev The exact amount refunded depends on the gas spent by the "postOp" itself and so the developers should + /// take that into account. + function postTransaction( + bytes calldata _context, + Transaction calldata _transaction, + bytes32 _txHash, + bytes32 _suggestedSignedHash, + ExecutionResult _txResult, + uint256 _maxRefundedGas + ) external payable; +} diff --git a/contracts/l2-contracts/interfaces/IPaymasterFlow.sol b/contracts/l2-contracts/interfaces/IPaymasterFlow.sol new file mode 100644 index 0000000..207aee2 --- /dev/null +++ b/contracts/l2-contracts/interfaces/IPaymasterFlow.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.20; + +/** + * @author Matter Labs + * @custom:security-contact security@matterlabs.dev + * @dev The interface that is used for encoding/decoding of + * different types of paymaster flows. + * @notice This is NOT an interface to be implemented + * by contracts. It is just used for encoding. + */ +interface IPaymasterFlow { + function general(bytes calldata input) external; + + function approvalBased(address _token, uint256 _minAllowance, bytes calldata _innerInput) external; +} diff --git a/contracts/l2-contracts/vendor/AddressAliasHelper.sol b/contracts/l2-contracts/vendor/AddressAliasHelper.sol new file mode 100644 index 0000000..6adab1d --- /dev/null +++ b/contracts/l2-contracts/vendor/AddressAliasHelper.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright 2019-2021, Offchain Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.20; + +library AddressAliasHelper { + uint160 internal constant offset = uint160(0x1111000000000000000000000000000000001111); + + /// @notice Utility function converts the address that submitted a tx + /// to the inbox on L1 to the msg.sender viewed on L2 + /// @param l1Address the address in the L1 that triggered the tx to L2 + /// @return l2Address L2 address as viewed in msg.sender + function applyL1ToL2Alias(address l1Address) internal pure returns (address l2Address) { + unchecked { + l2Address = address(uint160(l1Address) + offset); + } + } + + /// @notice Utility function that converts the msg.sender viewed on L2 to the + /// address that submitted a tx to the inbox on L1 + /// @param l2Address L2 address as viewed in msg.sender + /// @return l1Address the address in the L1 that triggered the tx to L2 + function undoL1ToL2Alias(address l2Address) internal pure returns (address l1Address) { + unchecked { + l1Address = address(uint160(l2Address) - offset); + } + } +} diff --git a/contracts/system-contracts/AccountCodeStorage.sol b/contracts/system-contracts/AccountCodeStorage.sol new file mode 100644 index 0000000..4c55279 --- /dev/null +++ b/contracts/system-contracts/AccountCodeStorage.sol @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {IAccountCodeStorage} from "./interfaces/IAccountCodeStorage.sol"; +import {Utils} from "./libraries/Utils.sol"; +import {DEPLOYER_SYSTEM_CONTRACT, NONCE_HOLDER_SYSTEM_CONTRACT, CURRENT_MAX_PRECOMPILE_ADDRESS} from "./Constants.sol"; +import {Unauthorized, InvalidCodeHash, CodeHashReason} from "./SystemContractErrors.sol"; + +/** + * @author Matter Labs + * @custom:security-contact security@matterlabs.dev + * @notice The storage of this contract serves as a mapping for the code hashes of the 32-byte account addresses. + * @dev Code hash is not strictly a hash, it's a structure where the first byte denotes the version of the hash, + * the second byte denotes whether the contract is constructed, and the next two bytes denote the length in 32-byte words. + * And then the next 28 bytes are the truncated hash. + * @dev In this version of ZKsync, the first byte of the hash MUST be 1. + * @dev The length of each bytecode MUST be odd. It's internal code format requirements, due to padding of SHA256 function. + * @dev It is also assumed that all the bytecode hashes are *known*, i.e. the full bytecodes + * were published on L1 as calldata. This contract trusts the ContractDeployer and the KnownCodesStorage + * system contracts to enforce the invariants mentioned above. + */ +contract AccountCodeStorage is IAccountCodeStorage { + bytes32 private constant EMPTY_STRING_KECCAK = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470; + + modifier onlyDeployer() { + if (msg.sender != address(DEPLOYER_SYSTEM_CONTRACT)) { + revert Unauthorized(msg.sender); + } + _; + } + + /// @notice Stores the bytecodeHash of constructing contract. + /// @param _address The address of the account to set the codehash to. + /// @param _hash The new bytecode hash of the constructing account. + /// @dev This method trusts the ContractDeployer to make sure that the bytecode is known and well-formed, + /// but checks whether the bytecode hash corresponds to the constructing smart contract. + function storeAccountConstructingCodeHash(address _address, bytes32 _hash) external override onlyDeployer { + // Check that code hash corresponds to the deploying smart contract + if (!Utils.isContractConstructing(_hash)) { + revert InvalidCodeHash(CodeHashReason.NotContractOnConstructor); + } + _storeCodeHash(_address, _hash); + } + + /// @notice Stores the bytecodeHash of constructed contract. + /// @param _address The address of the account to set the codehash to. + /// @param _hash The new bytecode hash of the constructed account. + /// @dev This method trusts the ContractDeployer to make sure that the bytecode is known and well-formed, + /// but checks whether the bytecode hash corresponds to the constructed smart contract. + function storeAccountConstructedCodeHash(address _address, bytes32 _hash) external override onlyDeployer { + // Check that code hash corresponds to the deploying smart contract + if (!Utils.isContractConstructed(_hash)) { + revert InvalidCodeHash(CodeHashReason.NotConstructedContract); + } + _storeCodeHash(_address, _hash); + } + + /// @notice Marks the account bytecodeHash as constructed. + /// @param _address The address of the account to mark as constructed + function markAccountCodeHashAsConstructed(address _address) external override onlyDeployer { + bytes32 codeHash = getRawCodeHash(_address); + + if (!Utils.isContractConstructing(codeHash)) { + revert InvalidCodeHash(CodeHashReason.NotContractOnConstructor); + } + + // Get the bytecode hash with "isConstructor" flag equal to false + bytes32 constructedBytecodeHash = Utils.constructedBytecodeHash(codeHash); + + _storeCodeHash(_address, constructedBytecodeHash); + } + + /// @dev Store the codehash of the account without any checks. + /// @param _address The address of the account to set the codehash to. + /// @param _hash The new account bytecode hash. + function _storeCodeHash(address _address, bytes32 _hash) internal { + uint256 addressAsKey = uint256(uint160(_address)); + assembly { + sstore(addressAsKey, _hash) + } + } + + /// @notice Get the codehash stored for an address. + /// @param _address The address of the account of which the codehash to return + /// @return codeHash The codehash stored for this account. + function getRawCodeHash(address _address) public view override returns (bytes32 codeHash) { + uint256 addressAsKey = uint256(uint160(_address)); + + assembly { + codeHash := sload(addressAsKey) + } + } + + /// @notice Simulate the behavior of the `extcodehash` EVM opcode. + /// @param _input The 256-bit account address. + /// @return codeHash - hash of the bytecode according to the EIP-1052 specification. + function getCodeHash(uint256 _input) external view override returns (bytes32) { + // We consider the account bytecode hash of the last 20 bytes of the input, because + // according to the spec "If EXTCODEHASH of A is X, then EXTCODEHASH of A + 2**160 is X". + address account = address(uint160(_input)); + if (uint160(account) <= CURRENT_MAX_PRECOMPILE_ADDRESS) { + return EMPTY_STRING_KECCAK; + } + + bytes32 codeHash = getRawCodeHash(account); + + // The code hash is equal to the `keccak256("")` if the account is an EOA with at least one transaction. + // Otherwise, the account is either deployed smart contract or an empty account, + // for both cases the code hash is equal to the raw code hash. + if (codeHash == 0x00 && NONCE_HOLDER_SYSTEM_CONTRACT.getRawNonce(account) > 0) { + codeHash = EMPTY_STRING_KECCAK; + } + // The contract is still on the constructor, which means it is not deployed yet, + // so set `keccak256("")` as a code hash. The EVM has the same behavior. + else if (Utils.isContractConstructing(codeHash)) { + codeHash = EMPTY_STRING_KECCAK; + } + + return codeHash; + } + + /// @notice Simulate the behavior of the `extcodesize` EVM opcode. + /// @param _input The 256-bit account address. + /// @return codeSize - the size of the deployed smart contract in bytes. + function getCodeSize(uint256 _input) external view override returns (uint256 codeSize) { + // We consider the account bytecode size of the last 20 bytes of the input, because + // according to the spec "If EXTCODESIZE of A is X, then EXTCODESIZE of A + 2**160 is X". + address account = address(uint160(_input)); + bytes32 codeHash = getRawCodeHash(account); + + // If the contract is a default account or is on constructor the code size is zero, + // otherwise extract the proper value for it from the bytecode hash. + // NOTE: zero address and precompiles are a special case, they are contracts, but we + // want to preserve EVM invariants (see EIP-1052 specification). That's why we automatically + // return `0` length in the following cases: + // - `codehash(0) == 0` + // - `account` is a precompile. + // - `account` is currently being constructed + if ( + uint160(account) > CURRENT_MAX_PRECOMPILE_ADDRESS && + codeHash != 0x00 && + !Utils.isContractConstructing(codeHash) + ) { + codeSize = Utils.bytecodeLenInBytes(codeHash); + } + } +} diff --git a/contracts/system-contracts/BootloaderUtilities.sol b/contracts/system-contracts/BootloaderUtilities.sol new file mode 100644 index 0000000..4fd38da --- /dev/null +++ b/contracts/system-contracts/BootloaderUtilities.sol @@ -0,0 +1,332 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {IBootloaderUtilities} from "./interfaces/IBootloaderUtilities.sol"; +import {Transaction, TransactionHelper, EIP_712_TX_TYPE, LEGACY_TX_TYPE, EIP_2930_TX_TYPE, EIP_1559_TX_TYPE} from "./libraries/TransactionHelper.sol"; +import {RLPEncoder} from "./libraries/RLPEncoder.sol"; +import {EfficientCall} from "./libraries/EfficientCall.sol"; +import {UnsupportedTxType, InvalidSig, SigField} from "./SystemContractErrors.sol"; + +/** + * @author Matter Labs + * @custom:security-contact security@matterlabs.dev + * @notice A contract that provides some utility methods for the bootloader + * that is very hard to write in Yul. + */ +contract BootloaderUtilities is IBootloaderUtilities { + using TransactionHelper for *; + + /// @notice Calculates the canonical transaction hash and the recommended transaction hash. + /// @param _transaction The transaction. + /// @return txHash and signedTxHash of the transaction, i.e. the transaction hash to be used in the explorer and commits to all + /// the fields of the transaction and the recommended hash to be signed for this transaction. + /// @dev txHash must be unique for all transactions. + function getTransactionHashes( + Transaction calldata _transaction + ) external view override returns (bytes32 txHash, bytes32 signedTxHash) { + signedTxHash = _transaction.encodeHash(); + if (_transaction.txType == EIP_712_TX_TYPE) { + txHash = keccak256(bytes.concat(signedTxHash, EfficientCall.keccak(_transaction.signature))); + } else if (_transaction.txType == LEGACY_TX_TYPE) { + txHash = encodeLegacyTransactionHash(_transaction); + } else if (_transaction.txType == EIP_1559_TX_TYPE) { + txHash = encodeEIP1559TransactionHash(_transaction); + } else if (_transaction.txType == EIP_2930_TX_TYPE) { + txHash = encodeEIP2930TransactionHash(_transaction); + } else { + revert UnsupportedTxType(_transaction.txType); + } + } + + /// @notice Calculates the hash for a legacy transaction. + /// @param _transaction The legacy transaction. + /// @return txHash The hash of the transaction. + function encodeLegacyTransactionHash(Transaction calldata _transaction) internal view returns (bytes32 txHash) { + // Hash of legacy transactions are encoded as one of the: + // - RLP(nonce, gasPrice, gasLimit, to, value, data, chainId, 0, 0) + // - RLP(nonce, gasPrice, gasLimit, to, value, data) + // + // In this RLP encoding, only the first one above list appears, so we encode each element + // inside list and then concatenate the length of all elements with them. + + bytes memory encodedNonce = RLPEncoder.encodeUint256(_transaction.nonce); + // Encode `gasPrice` and `gasLimit` together to prevent "stack too deep error". + bytes memory encodedGasParam; + { + bytes memory encodedGasPrice = RLPEncoder.encodeUint256(_transaction.maxFeePerGas); + bytes memory encodedGasLimit = RLPEncoder.encodeUint256(_transaction.gasLimit); + encodedGasParam = bytes.concat(encodedGasPrice, encodedGasLimit); + } + + bytes memory encodedTo = RLPEncoder.encodeAddress(address(uint160(_transaction.to))); + bytes memory encodedValue = RLPEncoder.encodeUint256(_transaction.value); + // Encode only the length of the transaction data, and not the data itself, + // so as not to copy to memory a potentially huge transaction data twice. + bytes memory encodedDataLength; + { + // Safe cast, because the length of the transaction data can't be so large. + uint64 txDataLen = uint64(_transaction.data.length); + if (txDataLen != 1) { + // If the length is not equal to one, then only using the length can it be encoded definitely. + encodedDataLength = RLPEncoder.encodeNonSingleBytesLen(txDataLen); + } else if (_transaction.data[0] >= 0x80) { + // If input is a byte in [0x80, 0xff] range, RLP encoding will concatenates 0x81 with the byte. + encodedDataLength = hex"81"; + } + // Otherwise the length is not encoded at all. + } + + bytes memory rEncoded; + { + uint256 rInt = uint256(bytes32(_transaction.signature[0:32])); + rEncoded = RLPEncoder.encodeUint256(rInt); + } + bytes memory sEncoded; + { + uint256 sInt = uint256(bytes32(_transaction.signature[32:64])); + sEncoded = RLPEncoder.encodeUint256(sInt); + } + bytes memory vEncoded; + { + uint256 vInt = uint256(uint8(_transaction.signature[64])); + if (vInt != 27 && vInt != 28) { + revert InvalidSig(SigField.V, vInt); + } + + // If the `chainId` is specified in the transaction, then the `v` value is encoded as + // `35 + y + 2 * chainId == vInt + 8 + 2 * chainId`, where y - parity bit (see EIP-155). + if (_transaction.reserved[0] != 0) { + vInt += 8 + block.chainid * 2; + } + + vEncoded = RLPEncoder.encodeUint256(vInt); + } + + bytes memory encodedListLength; + unchecked { + uint256 listLength = encodedNonce.length + + encodedGasParam.length + + encodedTo.length + + encodedValue.length + + encodedDataLength.length + + _transaction.data.length + + rEncoded.length + + sEncoded.length + + vEncoded.length; + + // Safe cast, because the length of the list can't be so large. + encodedListLength = RLPEncoder.encodeListLen(uint64(listLength)); + } + + return + keccak256( + // solhint-disable-next-line func-named-parameters + bytes.concat( + encodedListLength, + encodedNonce, + encodedGasParam, + encodedTo, + encodedValue, + encodedDataLength, + _transaction.data, + vEncoded, + rEncoded, + sEncoded + ) + ); + } + + /// @notice Calculates the hash for an EIP2930 transaction. + /// @param _transaction The EIP2930 transaction. + /// @return txHash The hash of the transaction. + function encodeEIP2930TransactionHash(Transaction calldata _transaction) internal view returns (bytes32) { + // Encode all fixed-length params to avoid "stack too deep error" + bytes memory encodedFixedLengthParams; + { + bytes memory encodedChainId = RLPEncoder.encodeUint256(block.chainid); + bytes memory encodedNonce = RLPEncoder.encodeUint256(_transaction.nonce); + bytes memory encodedGasPrice = RLPEncoder.encodeUint256(_transaction.maxFeePerGas); + bytes memory encodedGasLimit = RLPEncoder.encodeUint256(_transaction.gasLimit); + bytes memory encodedTo = RLPEncoder.encodeAddress(address(uint160(_transaction.to))); + bytes memory encodedValue = RLPEncoder.encodeUint256(_transaction.value); + // solhint-disable-next-line func-named-parameters + encodedFixedLengthParams = bytes.concat( + encodedChainId, + encodedNonce, + encodedGasPrice, + encodedGasLimit, + encodedTo, + encodedValue + ); + } + + // Encode only the length of the transaction data, and not the data itself, + // so as not to copy to memory a potentially huge transaction data twice. + bytes memory encodedDataLength; + { + // Safe cast, because the length of the transaction data can't be so large. + uint64 txDataLen = uint64(_transaction.data.length); + if (txDataLen != 1) { + // If the length is not equal to one, then only using the length can it be encoded definitely. + encodedDataLength = RLPEncoder.encodeNonSingleBytesLen(txDataLen); + } else if (_transaction.data[0] >= 0x80) { + // If input is a byte in [0x80, 0xff] range, RLP encoding will concatenates 0x81 with the byte. + encodedDataLength = hex"81"; + } + // Otherwise the length is not encoded at all. + } + + // On ZKsync, access lists are always zero length (at least for now). + bytes memory encodedAccessListLength = RLPEncoder.encodeListLen(0); + + bytes memory rEncoded; + { + uint256 rInt = uint256(bytes32(_transaction.signature[0:32])); + rEncoded = RLPEncoder.encodeUint256(rInt); + } + bytes memory sEncoded; + { + uint256 sInt = uint256(bytes32(_transaction.signature[32:64])); + sEncoded = RLPEncoder.encodeUint256(sInt); + } + bytes memory vEncoded; + { + uint256 vInt = uint256(uint8(_transaction.signature[64])); + if (vInt != 27 && vInt != 28) { + revert InvalidSig(SigField.V, vInt); + } + + vEncoded = RLPEncoder.encodeUint256(vInt - 27); + } + + bytes memory encodedListLength; + unchecked { + uint256 listLength = encodedFixedLengthParams.length + + encodedDataLength.length + + _transaction.data.length + + encodedAccessListLength.length + + rEncoded.length + + sEncoded.length + + vEncoded.length; + + // Safe cast, because the length of the list can't be so large. + encodedListLength = RLPEncoder.encodeListLen(uint64(listLength)); + } + + return + keccak256( + // solhint-disable-next-line func-named-parameters + bytes.concat( + "\x01", + encodedListLength, + encodedFixedLengthParams, + encodedDataLength, + _transaction.data, + encodedAccessListLength, + vEncoded, + rEncoded, + sEncoded + ) + ); + } + + /// @notice Calculates the hash for an EIP1559 transaction. + /// @param _transaction The legacy transaction. + /// @return txHash The hash of the transaction. + function encodeEIP1559TransactionHash(Transaction calldata _transaction) internal view returns (bytes32) { + // The formula for hash of EIP1559 transaction in the original proposal: + // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1559.md + + // Encode all fixed-length params to avoid "stack too deep error" + bytes memory encodedFixedLengthParams; + { + bytes memory encodedChainId = RLPEncoder.encodeUint256(block.chainid); + bytes memory encodedNonce = RLPEncoder.encodeUint256(_transaction.nonce); + bytes memory encodedMaxPriorityFeePerGas = RLPEncoder.encodeUint256(_transaction.maxPriorityFeePerGas); + bytes memory encodedMaxFeePerGas = RLPEncoder.encodeUint256(_transaction.maxFeePerGas); + bytes memory encodedGasLimit = RLPEncoder.encodeUint256(_transaction.gasLimit); + bytes memory encodedTo = RLPEncoder.encodeAddress(address(uint160(_transaction.to))); + bytes memory encodedValue = RLPEncoder.encodeUint256(_transaction.value); + // solhint-disable-next-line func-named-parameters + encodedFixedLengthParams = bytes.concat( + encodedChainId, + encodedNonce, + encodedMaxPriorityFeePerGas, + encodedMaxFeePerGas, + encodedGasLimit, + encodedTo, + encodedValue + ); + } + + // Encode only the length of the transaction data, and not the data itself, + // so as not to copy to memory a potentially huge transaction data twice. + bytes memory encodedDataLength; + { + // Safe cast, because the length of the transaction data can't be so large. + uint64 txDataLen = uint64(_transaction.data.length); + if (txDataLen != 1) { + // If the length is not equal to one, then only using the length can it be encoded definitely. + encodedDataLength = RLPEncoder.encodeNonSingleBytesLen(txDataLen); + } else if (_transaction.data[0] >= 0x80) { + // If input is a byte in [0x80, 0xff] range, RLP encoding will concatenates 0x81 with the byte. + encodedDataLength = hex"81"; + } + // Otherwise the length is not encoded at all. + } + + // On ZKsync, access lists are always zero length (at least for now). + bytes memory encodedAccessListLength = RLPEncoder.encodeListLen(0); + + bytes memory rEncoded; + { + uint256 rInt = uint256(bytes32(_transaction.signature[0:32])); + rEncoded = RLPEncoder.encodeUint256(rInt); + } + bytes memory sEncoded; + { + uint256 sInt = uint256(bytes32(_transaction.signature[32:64])); + sEncoded = RLPEncoder.encodeUint256(sInt); + } + bytes memory vEncoded; + { + uint256 vInt = uint256(uint8(_transaction.signature[64])); + if (vInt != 27 && vInt != 28) { + revert InvalidSig(SigField.V, vInt); + } + + vEncoded = RLPEncoder.encodeUint256(vInt - 27); + } + + bytes memory encodedListLength; + unchecked { + uint256 listLength = encodedFixedLengthParams.length + + encodedDataLength.length + + _transaction.data.length + + encodedAccessListLength.length + + rEncoded.length + + sEncoded.length + + vEncoded.length; + + // Safe cast, because the length of the list can't be so large. + encodedListLength = RLPEncoder.encodeListLen(uint64(listLength)); + } + + return + keccak256( + // solhint-disable-next-line func-named-parameters + bytes.concat( + "\x02", + encodedListLength, + encodedFixedLengthParams, + encodedDataLength, + _transaction.data, + encodedAccessListLength, + vEncoded, + rEncoded, + sEncoded + ) + ); + } +} diff --git a/contracts/system-contracts/ComplexUpgrader.sol b/contracts/system-contracts/ComplexUpgrader.sol new file mode 100644 index 0000000..a695451 --- /dev/null +++ b/contracts/system-contracts/ComplexUpgrader.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {IComplexUpgrader} from "./interfaces/IComplexUpgrader.sol"; +import {FORCE_DEPLOYER} from "./Constants.sol"; +import {Unauthorized, AddressHasNoCode} from "./SystemContractErrors.sol"; + +/** + * @author Matter Labs + * @custom:security-contact security@matterlabs.dev + * @notice Upgrader which should be used to perform complex multistep upgrades on L2. In case some custom logic for an upgrade is needed + * this logic should be deployed into the user space and then this contract will delegatecall to the deployed contract. + */ +contract ComplexUpgrader is IComplexUpgrader { + /// @notice Executes an upgrade process by delegating calls to another contract. + /// @dev This function allows only the `FORCE_DEPLOYER` to initiate the upgrade. + /// If the delegate call fails, the function will revert the transaction, returning the error message + /// provided by the delegated contract. + /// @param _delegateTo the address of the contract to which the calls will be delegated + /// @param _calldata the calldata to be delegate called in the `_delegateTo` contract + function upgrade(address _delegateTo, bytes calldata _calldata) external payable { + if (msg.sender != FORCE_DEPLOYER) { + revert Unauthorized(msg.sender); + } + + if (_delegateTo.code.length == 0) { + revert AddressHasNoCode(_delegateTo); + } + (bool success, bytes memory returnData) = _delegateTo.delegatecall(_calldata); + assembly { + if iszero(success) { + revert(add(returnData, 0x20), mload(returnData)) + } + } + } +} diff --git a/contracts/system-contracts/Compressor.sol b/contracts/system-contracts/Compressor.sol new file mode 100644 index 0000000..d4c52f9 --- /dev/null +++ b/contracts/system-contracts/Compressor.sol @@ -0,0 +1,271 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {ICompressor, OPERATION_BITMASK, LENGTH_BITS_OFFSET, MAX_ENUMERATION_INDEX_SIZE} from "./interfaces/ICompressor.sol"; +import {SystemContractBase} from "./abstract/SystemContractBase.sol"; +import {Utils} from "./libraries/Utils.sol"; +import {UnsafeBytesCalldata} from "./libraries/UnsafeBytesCalldata.sol"; +import {EfficientCall} from "./libraries/EfficientCall.sol"; +import {L1_MESSENGER_CONTRACT, STATE_DIFF_ENTRY_SIZE, KNOWN_CODE_STORAGE_CONTRACT} from "./Constants.sol"; +import {DerivedKeyNotEqualToCompressedValue, EncodedAndRealBytecodeChunkNotEqual, DictionaryDividedByEightNotGreaterThanEncodedDividedByTwo, EncodedLengthNotFourTimesSmallerThanOriginal, IndexOutOfBounds, IndexSizeError, UnsupportedOperation, CompressorInitialWritesProcessedNotEqual, CompressorEnumIndexNotEqual, StateDiffLengthMismatch, CompressionValueTransformError, CompressionValueAddError, CompressionValueSubError} from "./SystemContractErrors.sol"; + +/** + * @author Matter Labs + * @custom:security-contact security@matterlabs.dev + * @notice Contract with code pertaining to compression for zkEVM; at the moment this is used for bytecode compression + * and state diff compression validation. + * @dev Every deployed bytecode/published state diffs in zkEVM should be publicly restorable from the L1 data availability. + * For this reason, the user may request the sequencer to publish the original bytecode and mark it as known. + * Or the user may compress the bytecode and publish it instead (fewer data onchain!). At the end of every L1 Batch + * we publish pubdata, part of which contains the state diffs that occurred within the batch. + */ +contract Compressor is ICompressor, SystemContractBase { + using UnsafeBytesCalldata for bytes; + + /// @notice Verify the compressed bytecode and publish it on the L1. + /// @param _bytecode The original bytecode to be verified against. + /// @param _rawCompressedData The compressed bytecode in a format of: + /// - 2 bytes: the length of the dictionary + /// - N bytes: the dictionary + /// - M bytes: the encoded data + /// @return bytecodeHash The hash of the original bytecode. + /// @dev The dictionary is a sequence of 8-byte chunks, each of them has the associated index. + /// @dev The encoded data is a sequence of 2-byte chunks, each of them is an index of the dictionary. + /// @dev The compression algorithm works as follows: + /// 1. The original bytecode is split into 8-byte chunks. + /// Since the bytecode size is always a multiple of 32, this is always possible. + /// 2. For each 8-byte chunk in the original bytecode: + /// * If the chunk is not already in the dictionary, it is added to the dictionary array. + /// * If the dictionary becomes overcrowded (2^16 + 1 elements), the compression process will fail. + /// * The 2-byte index of the chunk in the dictionary is added to the encoded data. + /// @dev Currently, the method may be called only from the bootloader because the server is not ready to publish bytecodes + /// in internal transactions. However, in the future, we will allow everyone to publish compressed bytecodes. + /// @dev Read more about the compression: https://github.com/matter-labs/zksync-era/blob/main/docs/guides/advanced/compression.md + function publishCompressedBytecode( + bytes calldata _bytecode, + bytes calldata _rawCompressedData + ) external onlyCallFromBootloader returns (bytes32 bytecodeHash) { + unchecked { + (bytes calldata dictionary, bytes calldata encodedData) = _decodeRawBytecode(_rawCompressedData); + + if (encodedData.length * 4 != _bytecode.length) { + revert EncodedLengthNotFourTimesSmallerThanOriginal(); + } + + if (dictionary.length / 8 > encodedData.length / 2) { + revert DictionaryDividedByEightNotGreaterThanEncodedDividedByTwo(); + } + + // We disable this check because calldata array length is cheap. + // solhint-disable-next-line gas-length-in-loops + for (uint256 encodedDataPointer = 0; encodedDataPointer < encodedData.length; encodedDataPointer += 2) { + uint256 indexOfEncodedChunk = uint256(encodedData.readUint16(encodedDataPointer)) * 8; + if (indexOfEncodedChunk > dictionary.length - 1) { + revert IndexOutOfBounds(); + } + + uint64 encodedChunk = dictionary.readUint64(indexOfEncodedChunk); + uint64 realChunk = _bytecode.readUint64(encodedDataPointer * 4); + + if (encodedChunk != realChunk) { + revert EncodedAndRealBytecodeChunkNotEqual(realChunk, encodedChunk); + } + } + } + + bytecodeHash = Utils.hashL2Bytecode(_bytecode); + L1_MESSENGER_CONTRACT.sendToL1(_rawCompressedData); + KNOWN_CODE_STORAGE_CONTRACT.markBytecodeAsPublished(bytecodeHash); + } + + /// @notice Verifies that the compression of state diffs has been done correctly for the {_stateDiffs} param. + /// @param _numberOfStateDiffs The number of state diffs being checked. + /// @param _enumerationIndexSize Number of bytes used to represent an enumeration index for repeated writes. + /// @param _stateDiffs Encoded full state diff structs. See the first dev comment below for encoding. + /// @param _compressedStateDiffs The compressed state diffs + /// @dev We don't verify that the size of {_stateDiffs} is equivalent to {_numberOfStateDiffs} * STATE_DIFF_ENTRY_SIZE since that check is + /// done within the L1Messenger calling contract. + /// @return stateDiffHash Hash of the encoded (uncompressed) state diffs to be committed to via system log. + /// @dev This check assumes that the ordering of state diffs are sorted by (address, key) for the encoded state diffs and + /// then the compressed are sorted the same but with all the initial writes coming before the repeated writes. + /// @dev state diff: [20bytes address][32bytes key][32bytes derived key][8bytes enum index][32bytes initial value][32bytes final value] + /// @dev The compression format: + /// - 2 bytes: number of initial writes + /// - N bytes initial writes + /// - 32 bytes derived key + /// - 1 byte metadata: + /// - first 5 bits: length in bytes of compressed value + /// - last 3 bits: operation + /// - 0 -> Nothing (32 bytes) + /// - 1 -> Add + /// - 2 -> Subtract + /// - 3 -> Transform (< 32 bytes) + /// - Len Bytes: Compressed Value + /// - M bytes repeated writes + /// - {_enumerationIndexSize} bytes for enumeration index + /// - 1 byte metadata: + /// - first 5 bits: length in bytes of compressed value + /// - last 3 bits: operation + /// - 0 -> Nothing (32 bytes) + /// - 1 -> Add + /// - 2 -> Subtract + /// - 3 -> Transform (< 32 bytes) + /// - Len Bytes: Compressed Value + function verifyCompressedStateDiffs( + uint256 _numberOfStateDiffs, + uint256 _enumerationIndexSize, + bytes calldata _stateDiffs, + bytes calldata _compressedStateDiffs + ) external onlyCallFrom(address(L1_MESSENGER_CONTRACT)) returns (bytes32 stateDiffHash) { + // We do not enforce the operator to use the optimal, i.e. the minimally possible _enumerationIndexSize. + // We do enforce however, that the _enumerationIndexSize is not larger than 8 bytes long, which is the + // maximal ever possible size for enumeration index. + if (_enumerationIndexSize > MAX_ENUMERATION_INDEX_SIZE) { + revert IndexSizeError(); + } + + uint256 numberOfInitialWrites = uint256(_compressedStateDiffs.readUint16(0)); + + uint256 stateDiffPtr = 2; + uint256 numInitialWritesProcessed = 0; + + // Process initial writes + for (uint256 i = 0; i < _numberOfStateDiffs * STATE_DIFF_ENTRY_SIZE; i += STATE_DIFF_ENTRY_SIZE) { + bytes calldata stateDiff = _stateDiffs[i:i + STATE_DIFF_ENTRY_SIZE]; + uint64 enumIndex = stateDiff.readUint64(84); + if (enumIndex != 0) { + // It is a repeated write, so we skip it. + continue; + } + + ++numInitialWritesProcessed; + + bytes32 derivedKey = stateDiff.readBytes32(52); + uint256 initValue = stateDiff.readUint256(92); + uint256 finalValue = stateDiff.readUint256(124); + bytes32 compressedDerivedKey = _compressedStateDiffs.readBytes32(stateDiffPtr); + if (derivedKey != compressedDerivedKey) { + revert DerivedKeyNotEqualToCompressedValue(derivedKey, compressedDerivedKey); + } + stateDiffPtr += 32; + + uint8 metadata = uint8(bytes1(_compressedStateDiffs[stateDiffPtr])); + ++stateDiffPtr; + uint8 operation = metadata & OPERATION_BITMASK; + uint8 len = operation == 0 ? 32 : metadata >> LENGTH_BITS_OFFSET; + _verifyValueCompression( + initValue, + finalValue, + operation, + _compressedStateDiffs[stateDiffPtr:stateDiffPtr + len] + ); + stateDiffPtr += len; + } + + if (numInitialWritesProcessed != numberOfInitialWrites) { + revert CompressorInitialWritesProcessedNotEqual(numberOfInitialWrites, numInitialWritesProcessed); + } + + // Process repeated writes + for (uint256 i = 0; i < _numberOfStateDiffs * STATE_DIFF_ENTRY_SIZE; i += STATE_DIFF_ENTRY_SIZE) { + bytes calldata stateDiff = _stateDiffs[i:i + STATE_DIFF_ENTRY_SIZE]; + uint64 enumIndex = stateDiff.readUint64(84); + if (enumIndex == 0) { + continue; + } + + uint256 initValue = stateDiff.readUint256(92); + uint256 finalValue = stateDiff.readUint256(124); + uint256 compressedEnumIndex = _sliceToUint256( + _compressedStateDiffs[stateDiffPtr:stateDiffPtr + _enumerationIndexSize] + ); + if (enumIndex != compressedEnumIndex) { + revert CompressorEnumIndexNotEqual(enumIndex, compressedEnumIndex); + } + stateDiffPtr += _enumerationIndexSize; + + uint8 metadata = uint8(bytes1(_compressedStateDiffs[stateDiffPtr])); + ++stateDiffPtr; + uint8 operation = metadata & OPERATION_BITMASK; + uint8 len = operation == 0 ? 32 : metadata >> LENGTH_BITS_OFFSET; + _verifyValueCompression( + initValue, + finalValue, + operation, + _compressedStateDiffs[stateDiffPtr:stateDiffPtr + len] + ); + stateDiffPtr += len; + } + + if (stateDiffPtr != _compressedStateDiffs.length) { + revert StateDiffLengthMismatch(); + } + + stateDiffHash = EfficientCall.keccak(_stateDiffs); + } + + /// @notice Decode the raw compressed data into the dictionary and the encoded data. + /// @param _rawCompressedData The compressed bytecode in a format of: + /// - 2 bytes: the bytes length of the dictionary + /// - N bytes: the dictionary + /// - M bytes: the encoded data + function _decodeRawBytecode( + bytes calldata _rawCompressedData + ) internal pure returns (bytes calldata dictionary, bytes calldata encodedData) { + unchecked { + // The dictionary length can't be more than 2^16, so it fits into 2 bytes. + uint256 dictionaryLen = uint256(_rawCompressedData.readUint16(0)); + dictionary = _rawCompressedData[2:2 + dictionaryLen * 8]; + encodedData = _rawCompressedData[2 + dictionaryLen * 8:]; + } + } + + /// @notice Verify value compression was done correct given initial value, final value, operation, and compressed value + /// @param _initialValue Previous value of key/enumeration index. + /// @param _finalValue Updated value of key/enumeration index. + /// @param _operation The operation that was performed on value. + /// @param _compressedValue The slice of calldata with compressed value either representing the final + /// value or difference between initial and final value. It should be of arbitrary length less than or equal to 32 bytes. + /// @dev It is the responsibility of the caller of this function to ensure that the `_compressedValue` has length no longer than 32 bytes. + /// @dev Operation id mapping: + /// 0 -> Nothing (32 bytes) + /// 1 -> Add + /// 2 -> Subtract + /// 3 -> Transform (< 32 bytes) + function _verifyValueCompression( + uint256 _initialValue, + uint256 _finalValue, + uint256 _operation, + bytes calldata _compressedValue + ) internal pure { + uint256 convertedValue = _sliceToUint256(_compressedValue); + + unchecked { + if (_operation == 0 || _operation == 3) { + if (convertedValue != _finalValue) { + revert CompressionValueTransformError(_finalValue, convertedValue); + } + } else if (_operation == 1) { + if (_initialValue + convertedValue != _finalValue) { + revert CompressionValueAddError(_finalValue, _initialValue + convertedValue); + } + } else if (_operation == 2) { + if (_initialValue - convertedValue != _finalValue) { + revert CompressionValueSubError(_finalValue, _initialValue - convertedValue); + } + } else { + revert UnsupportedOperation(); + } + } + } + + /// @notice Converts a calldata slice into uint256. It is the responsibility of the caller to ensure that + /// the _calldataSlice has length no longer than 32 bytes + /// @param _calldataSlice The calldata slice to convert to uint256 + /// @return number The uint256 representation of the calldata slice + function _sliceToUint256(bytes calldata _calldataSlice) internal pure returns (uint256 number) { + number = uint256(bytes32(_calldataSlice)); + number >>= (256 - (_calldataSlice.length * 8)); + } +} diff --git a/contracts/system-contracts/Constants.sol b/contracts/system-contracts/Constants.sol new file mode 100644 index 0000000..d8dd57e --- /dev/null +++ b/contracts/system-contracts/Constants.sol @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.20; + +import {IAccountCodeStorage} from "./interfaces/IAccountCodeStorage.sol"; +import {INonceHolder} from "./interfaces/INonceHolder.sol"; +import {IContractDeployer} from "./interfaces/IContractDeployer.sol"; +import {IKnownCodesStorage} from "./interfaces/IKnownCodesStorage.sol"; +import {IImmutableSimulator} from "./interfaces/IImmutableSimulator.sol"; +import {IBaseToken} from "./interfaces/IBaseToken.sol"; +import {IL1Messenger} from "./interfaces/IL1Messenger.sol"; +import {ISystemContext} from "./interfaces/ISystemContext.sol"; +import {ICompressor} from "./interfaces/ICompressor.sol"; +import {IComplexUpgrader} from "./interfaces/IComplexUpgrader.sol"; +import {IBootloaderUtilities} from "./interfaces/IBootloaderUtilities.sol"; +import {IPubdataChunkPublisher} from "./interfaces/IPubdataChunkPublisher.sol"; + +/// @dev All the system contracts introduced by ZKsync have their addresses +/// started from 2^15 in order to avoid collision with Ethereum precompiles. +uint160 constant SYSTEM_CONTRACTS_OFFSET = 0x8000; // 2^15 + +/// @dev Unlike the value above, it is not overridden for the purpose of testing and +/// is identical to the constant value actually used as the system contracts offset on +/// mainnet. +uint160 constant REAL_SYSTEM_CONTRACTS_OFFSET = 0x8000; + +/// @dev All the system contracts must be located in the kernel space, +/// i.e. their addresses must be below 2^16. +uint160 constant MAX_SYSTEM_CONTRACT_ADDRESS = 0xffff; // 2^16 - 1 + +address constant ECRECOVER_SYSTEM_CONTRACT = address(0x01); +address constant SHA256_SYSTEM_CONTRACT = address(0x02); +address constant ECADD_SYSTEM_CONTRACT = address(0x06); +address constant ECMUL_SYSTEM_CONTRACT = address(0x07); +address constant ECPAIRING_SYSTEM_CONTRACT = address(0x08); + + +/// @dev The number of gas that need to be spent for a single byte of pubdata regardless of the pubdata price. +/// This variable is used to ensure the following: +/// - That the long-term storage of the operator is compensated properly. +/// - That it is not possible that the pubdata counter grows too high without spending proportional amount of computation. +uint256 constant COMPUTATIONAL_PRICE_FOR_PUBDATA = 80; + +/// @dev The maximal possible address of an L1-like precompie. These precompiles maintain the following properties: +/// - Their extcodehash is EMPTY_STRING_KECCAK +/// - Their extcodesize is 0 despite having a bytecode formally deployed there. +uint256 constant CURRENT_MAX_PRECOMPILE_ADDRESS = 0xff; + +address payable constant BOOTLOADER_FORMAL_ADDRESS = payable(address(SYSTEM_CONTRACTS_OFFSET + 0x01)); +IAccountCodeStorage constant ACCOUNT_CODE_STORAGE_SYSTEM_CONTRACT = IAccountCodeStorage( + address(SYSTEM_CONTRACTS_OFFSET + 0x02) +); +INonceHolder constant NONCE_HOLDER_SYSTEM_CONTRACT = INonceHolder(address(SYSTEM_CONTRACTS_OFFSET + 0x03)); +IKnownCodesStorage constant KNOWN_CODE_STORAGE_CONTRACT = IKnownCodesStorage(address(SYSTEM_CONTRACTS_OFFSET + 0x04)); +IImmutableSimulator constant IMMUTABLE_SIMULATOR_SYSTEM_CONTRACT = IImmutableSimulator( + address(SYSTEM_CONTRACTS_OFFSET + 0x05) +); +IContractDeployer constant DEPLOYER_SYSTEM_CONTRACT = IContractDeployer(address(SYSTEM_CONTRACTS_OFFSET + 0x06)); +IContractDeployer constant REAL_DEPLOYER_SYSTEM_CONTRACT = IContractDeployer(address(REAL_SYSTEM_CONTRACTS_OFFSET + 0x06)); + +// A contract that is allowed to deploy any codehash +// on any address. To be used only during an upgrade. +address constant FORCE_DEPLOYER = address(SYSTEM_CONTRACTS_OFFSET + 0x07); +IL1Messenger constant L1_MESSENGER_CONTRACT = IL1Messenger(address(SYSTEM_CONTRACTS_OFFSET + 0x08)); +address constant MSG_VALUE_SYSTEM_CONTRACT = address(SYSTEM_CONTRACTS_OFFSET + 0x09); + +IBaseToken constant BASE_TOKEN_SYSTEM_CONTRACT = IBaseToken(address(SYSTEM_CONTRACTS_OFFSET + 0x0a)); +IBaseToken constant REAL_BASE_TOKEN_SYSTEM_CONTRACT = IBaseToken(address(REAL_SYSTEM_CONTRACTS_OFFSET + 0x0a)); + +// Hardcoded because even for tests we should keep the address. (Instead `SYSTEM_CONTRACTS_OFFSET + 0x10`) +// Precompile call depends on it. +// And we don't want to mock this contract. +address constant KECCAK256_SYSTEM_CONTRACT = address(0x8010); + +ISystemContext constant SYSTEM_CONTEXT_CONTRACT = ISystemContext(payable(address(SYSTEM_CONTRACTS_OFFSET + 0x0b))); +ISystemContext constant REAL_SYSTEM_CONTEXT_CONTRACT = ISystemContext(payable(address(REAL_SYSTEM_CONTRACTS_OFFSET + 0x0b))); + +IBootloaderUtilities constant BOOTLOADER_UTILITIES = IBootloaderUtilities(address(SYSTEM_CONTRACTS_OFFSET + 0x0c)); + +// It will be a different value for tests, while shouldn't. But for now, this constant is not used by other contracts, so that's fine. +address constant EVENT_WRITER_CONTRACT = address(SYSTEM_CONTRACTS_OFFSET + 0x0d); + +ICompressor constant COMPRESSOR_CONTRACT = ICompressor(address(SYSTEM_CONTRACTS_OFFSET + 0x0e)); + +IComplexUpgrader constant COMPLEX_UPGRADER_CONTRACT = IComplexUpgrader(address(SYSTEM_CONTRACTS_OFFSET + 0x0f)); + +IPubdataChunkPublisher constant PUBDATA_CHUNK_PUBLISHER = IPubdataChunkPublisher( + address(SYSTEM_CONTRACTS_OFFSET + 0x11) +); + +/// @dev If the bitwise AND of the extraAbi[2] param when calling the MSG_VALUE_SIMULATOR +/// is non-zero, the call will be assumed to be a system one. +uint256 constant MSG_VALUE_SIMULATOR_IS_SYSTEM_BIT = 1; + +/// @dev The maximal msg.value that context can have +uint256 constant MAX_MSG_VALUE = type(uint128).max; + +/// @dev Prefix used during derivation of account addresses using CREATE2 +/// @dev keccak256("zksyncCreate2") +bytes32 constant CREATE2_PREFIX = 0x2020dba91b30cc0006188af794c2fb30dd8520db7e2c088b7fc7c103c00ca494; +/// @dev Prefix used during derivation of account addresses using CREATE +/// @dev keccak256("zksyncCreate") +bytes32 constant CREATE_PREFIX = 0x63bae3a9951d38e8a3fbb7b70909afc1200610fc5bc55ade242f815974674f23; + +/// @dev Each state diff consists of 156 bytes of actual data and 116 bytes of unused padding, needed for circuit efficiency. +uint256 constant STATE_DIFF_ENTRY_SIZE = 272; + +enum SystemLogKey { + L2_TO_L1_LOGS_TREE_ROOT_KEY, + TOTAL_L2_TO_L1_PUBDATA_KEY, + STATE_DIFF_HASH_KEY, + PACKED_BATCH_AND_L2_BLOCK_TIMESTAMP_KEY, + PREV_BATCH_HASH_KEY, + CHAINED_PRIORITY_TXN_HASH_KEY, + NUMBER_OF_LAYER_1_TXS_KEY, + BLOB_ONE_HASH_KEY, + BLOB_TWO_HASH_KEY, + BLOB_THREE_HASH_KEY, + BLOB_FOUR_HASH_KEY, + BLOB_FIVE_HASH_KEY, + BLOB_SIX_HASH_KEY, + EXPECTED_SYSTEM_CONTRACT_UPGRADE_TX_HASH_KEY +} + +/// @dev The number of leaves in the L2->L1 log Merkle tree. +/// While formally a tree of any length is acceptable, the node supports only a constant length of 16384 leaves. +uint256 constant L2_TO_L1_LOGS_MERKLE_TREE_LEAVES = 16_384; + +/// @dev The length of the derived key in bytes inside compressed state diffs. +uint256 constant DERIVED_KEY_LENGTH = 32; +/// @dev The length of the enum index in bytes inside compressed state diffs. +uint256 constant ENUM_INDEX_LENGTH = 8; +/// @dev The length of value in bytes inside compressed state diffs. +uint256 constant VALUE_LENGTH = 32; + +/// @dev The length of the compressed initial storage write in bytes. +uint256 constant COMPRESSED_INITIAL_WRITE_SIZE = DERIVED_KEY_LENGTH + VALUE_LENGTH; +/// @dev The length of the compressed repeated storage write in bytes. +uint256 constant COMPRESSED_REPEATED_WRITE_SIZE = ENUM_INDEX_LENGTH + VALUE_LENGTH; + +/// @dev The position from which the initial writes start in the compressed state diffs. +uint256 constant INITIAL_WRITE_STARTING_POSITION = 4; + +/// @dev Each storage diffs consists of the following elements: +/// [20bytes address][32bytes key][32bytes derived key][8bytes enum index][32bytes initial value][32bytes final value] +/// @dev The offset of the derived key in a storage diff. +uint256 constant STATE_DIFF_DERIVED_KEY_OFFSET = 52; +/// @dev The offset of the enum index in a storage diff. +uint256 constant STATE_DIFF_ENUM_INDEX_OFFSET = 84; +/// @dev The offset of the final value in a storage diff. +uint256 constant STATE_DIFF_FINAL_VALUE_OFFSET = 124; + +/// @dev Total number of bytes in a blob. Blob = 4096 field elements * 31 bytes per field element +/// @dev EIP-4844 defines it as 131_072 but we use 4096 * 31 within our circuits to always fit within a field element +/// @dev Our circuits will prove that a EIP-4844 blob and our internal blob are the same. +uint256 constant BLOB_SIZE_BYTES = 126_976; + +/// @dev Max number of blobs currently supported +uint256 constant MAX_NUMBER_OF_BLOBS = 6; diff --git a/contracts/system-contracts/ContractDeployer.sol b/contracts/system-contracts/ContractDeployer.sol new file mode 100644 index 0000000..8e9bda1 --- /dev/null +++ b/contracts/system-contracts/ContractDeployer.sol @@ -0,0 +1,387 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {ImmutableData} from "./interfaces/IImmutableSimulator.sol"; +import {IContractDeployer} from "./interfaces/IContractDeployer.sol"; +import {CREATE2_PREFIX, CREATE_PREFIX, NONCE_HOLDER_SYSTEM_CONTRACT, ACCOUNT_CODE_STORAGE_SYSTEM_CONTRACT, FORCE_DEPLOYER, MAX_SYSTEM_CONTRACT_ADDRESS, KNOWN_CODE_STORAGE_CONTRACT, BASE_TOKEN_SYSTEM_CONTRACT, IMMUTABLE_SIMULATOR_SYSTEM_CONTRACT, COMPLEX_UPGRADER_CONTRACT} from "./Constants.sol"; + +import {Utils} from "./libraries/Utils.sol"; +import {EfficientCall} from "./libraries/EfficientCall.sol"; +import {SystemContractHelper} from "./libraries/SystemContractHelper.sol"; +import {SystemContractBase} from "./abstract/SystemContractBase.sol"; +import {Unauthorized, InvalidNonceOrderingChange, ValueMismatch, EmptyBytes32, NotAllowedToDeployInKernelSpace, HashIsNonZero, NonEmptyAccount, UnknownCodeHash, NonEmptyMsgValue} from "./SystemContractErrors.sol"; + +/** + * @author Matter Labs + * @custom:security-contact security@matterlabs.dev + * @notice System smart contract that is responsible for deploying other smart contracts on ZKsync. + * @dev The contract is responsible for generating the address of the deployed smart contract, + * incrementing the deployment nonce and making sure that the constructor is never called twice in a contract. + * Note, contracts with bytecode that have already been published to L1 once + * do not need to be published anymore. + */ +contract ContractDeployer is IContractDeployer, SystemContractBase { + /// @notice Information about an account contract. + /// @dev For EOA and simple contracts (i.e. not accounts) this value is 0. + mapping(address => AccountInfo) internal accountInfo; + + modifier onlySelf() { + if (msg.sender != address(this)) { + revert Unauthorized(msg.sender); + } + _; + } + + /// @notice Returns information about a certain account. + function getAccountInfo(address _address) external view returns (AccountInfo memory info) { + return accountInfo[_address]; + } + + /// @notice Returns the account abstraction version if `_address` is a deployed contract. + /// Returns the latest supported account abstraction version if `_address` is an EOA. + function extendedAccountVersion(address _address) public view returns (AccountAbstractionVersion) { + AccountInfo memory info = accountInfo[_address]; + if (info.supportedAAVersion != AccountAbstractionVersion.None) { + return info.supportedAAVersion; + } + + // It is an EOA, it is still an account. + if ( + _address > address(MAX_SYSTEM_CONTRACT_ADDRESS) && + ACCOUNT_CODE_STORAGE_SYSTEM_CONTRACT.getRawCodeHash(_address) == 0 + ) { + return AccountAbstractionVersion.Version1; + } + + return AccountAbstractionVersion.None; + } + + /// @notice Stores the new account information + function _storeAccountInfo(address _address, AccountInfo memory _newInfo) internal { + accountInfo[_address] = _newInfo; + } + + /// @notice Update the used version of the account. + /// @param _version The new version of the AA protocol to use. + /// @dev Note that it allows changes from account to non-account and vice versa. + function updateAccountVersion(AccountAbstractionVersion _version) external onlySystemCall { + accountInfo[msg.sender].supportedAAVersion = _version; + + emit AccountVersionUpdated(msg.sender, _version); + } + + /// @notice Updates the nonce ordering of the account. Currently, + /// it only allows changes from sequential to arbitrary ordering. + /// @param _nonceOrdering The new nonce ordering to use. + function updateNonceOrdering(AccountNonceOrdering _nonceOrdering) external onlySystemCall { + AccountInfo memory currentInfo = accountInfo[msg.sender]; + + if ( + _nonceOrdering != AccountNonceOrdering.Arbitrary || + currentInfo.nonceOrdering != AccountNonceOrdering.Sequential + ) { + revert InvalidNonceOrderingChange(); + } + + currentInfo.nonceOrdering = _nonceOrdering; + _storeAccountInfo(msg.sender, currentInfo); + + emit AccountNonceOrderingUpdated(msg.sender, _nonceOrdering); + } + + /// @notice Calculates the address of a deployed contract via create2 + /// @param _sender The account that deploys the contract. + /// @param _bytecodeHash The correctly formatted hash of the bytecode. + /// @param _salt The create2 salt. + /// @param _input The constructor data. + /// @return newAddress The derived address of the account. + function getNewAddressCreate2( + address _sender, + bytes32 _bytecodeHash, + bytes32 _salt, + bytes calldata _input + ) public view override returns (address newAddress) { + // No collision is possible with the Ethereum's CREATE2, since + // the prefix begins with 0x20.... + bytes32 constructorInputHash = EfficientCall.keccak(_input); + + bytes32 hash = keccak256( + // solhint-disable-next-line func-named-parameters + bytes.concat(CREATE2_PREFIX, bytes32(uint256(uint160(_sender))), _salt, _bytecodeHash, constructorInputHash) + ); + + newAddress = address(uint160(uint256(hash))); + } + + /// @notice Calculates the address of a deployed contract via create + /// @param _sender The account that deploys the contract. + /// @param _senderNonce The deploy nonce of the sender's account. + function getNewAddressCreate( + address _sender, + uint256 _senderNonce + ) public pure override returns (address newAddress) { + // No collision is possible with the Ethereum's CREATE, since + // the prefix begins with 0x63.... + bytes32 hash = keccak256( + bytes.concat(CREATE_PREFIX, bytes32(uint256(uint160(_sender))), bytes32(_senderNonce)) + ); + + newAddress = address(uint160(uint256(hash))); + } + + /// @notice Deploys a contract with similar address derivation rules to the EVM's `CREATE2` opcode. + /// @param _salt The CREATE2 salt + /// @param _bytecodeHash The correctly formatted hash of the bytecode. + /// @param _input The constructor calldata + function create2( + bytes32 _salt, + bytes32 _bytecodeHash, + bytes calldata _input + ) external payable override returns (address) { + return create2Account(_salt, _bytecodeHash, _input, AccountAbstractionVersion.None); + } + + /// @notice Deploys a contract with similar address derivation rules to the EVM's `CREATE` opcode. + /// @param _bytecodeHash The correctly formatted hash of the bytecode. + /// @param _input The constructor calldata + /// @dev This method also accepts nonce as one of its parameters. + /// It is not used anywhere and it needed simply for the consistency for the compiler + /// Note: this method may be callable only in system mode, + /// that is checked in the `createAccount` by `onlySystemCall` modifier. + function create( + bytes32 _salt, + bytes32 _bytecodeHash, + bytes calldata _input + ) external payable override returns (address) { + return createAccount(_salt, _bytecodeHash, _input, AccountAbstractionVersion.None); + } + + /// @notice Deploys a contract account with similar address derivation rules to the EVM's `CREATE2` opcode. + /// @param _salt The CREATE2 salt + /// @param _bytecodeHash The correctly formatted hash of the bytecode. + /// @param _input The constructor calldata. + /// @param _aaVersion The account abstraction version to use. + /// Note: this method may be callable only in system mode, + /// that is checked in the `createAccount` by `onlySystemCall` modifier. + function create2Account( + bytes32 _salt, + bytes32 _bytecodeHash, + bytes calldata _input, + AccountAbstractionVersion _aaVersion + ) public payable override onlySystemCall returns (address) { + NONCE_HOLDER_SYSTEM_CONTRACT.incrementDeploymentNonce(msg.sender); + address newAddress = getNewAddressCreate2(msg.sender, _bytecodeHash, _salt, _input); + + _nonSystemDeployOnAddress(_bytecodeHash, newAddress, _aaVersion, _input); + + return newAddress; + } + + /// @notice Deploys a contract account with similar address derivation rules to the EVM's `CREATE` opcode. + /// @param _bytecodeHash The correctly formatted hash of the bytecode. + /// @param _input The constructor calldata. + /// @param _aaVersion The account abstraction version to use. + /// @dev This method also accepts salt as one of its parameters. + /// It is not used anywhere and it needed simply for the consistency for the compiler + function createAccount( + bytes32, // salt + bytes32 _bytecodeHash, + bytes calldata _input, + AccountAbstractionVersion _aaVersion + ) public payable override onlySystemCall returns (address) { + uint256 senderNonce = NONCE_HOLDER_SYSTEM_CONTRACT.incrementDeploymentNonce(msg.sender); + address newAddress = getNewAddressCreate(msg.sender, senderNonce); + + _nonSystemDeployOnAddress(_bytecodeHash, newAddress, _aaVersion, _input); + + return newAddress; + } + + /// @notice A struct that describes a forced deployment on an address + struct ForceDeployment { + // The bytecode hash to put on an address + bytes32 bytecodeHash; + // The address on which to deploy the bytecodehash to + address newAddress; + // Whether to run the constructor on the force deployment + bool callConstructor; + // The value with which to initialize a contract + uint256 value; + // The constructor calldata + bytes input; + } + + /// @notice The method that can be used to forcefully deploy a contract. + /// @param _deployment Information about the forced deployment. + /// @param _sender The `msg.sender` inside the constructor call. + function forceDeployOnAddress(ForceDeployment calldata _deployment, address _sender) external payable onlySelf { + _ensureBytecodeIsKnown(_deployment.bytecodeHash); + + // Since the `forceDeployOnAddress` function is called only during upgrades, the Governance is trusted to correctly select + // the addresses to deploy the new bytecodes to and to assess whether overriding the AccountInfo for the "force-deployed" + // contract is acceptable. + AccountInfo memory newAccountInfo; + newAccountInfo.supportedAAVersion = AccountAbstractionVersion.None; + // Accounts have sequential nonces by default. + newAccountInfo.nonceOrdering = AccountNonceOrdering.Sequential; + _storeAccountInfo(_deployment.newAddress, newAccountInfo); + + _constructContract({ + _sender: _sender, + _newAddress: _deployment.newAddress, + _bytecodeHash: _deployment.bytecodeHash, + _input: _deployment.input, + _isSystem: false, + _callConstructor: _deployment.callConstructor + }); + } + + /// @notice This method is to be used only during an upgrade to set bytecodes on specific addresses. + /// @dev We do not require `onlySystemCall` here, since the method is accessible only + /// by `FORCE_DEPLOYER`. + function forceDeployOnAddresses(ForceDeployment[] calldata _deployments) external payable { + if (msg.sender != FORCE_DEPLOYER && msg.sender != address(COMPLEX_UPGRADER_CONTRACT)) { + revert Unauthorized(msg.sender); + } + + uint256 deploymentsLength = _deployments.length; + // We need to ensure that the `value` provided by the call is enough to provide `value` + // for all of the deployments + uint256 sumOfValues = 0; + for (uint256 i = 0; i < deploymentsLength; ++i) { + sumOfValues += _deployments[i].value; + } + if (msg.value != sumOfValues) { + revert ValueMismatch(sumOfValues, msg.value); + } + + for (uint256 i = 0; i < deploymentsLength; ++i) { + this.forceDeployOnAddress{value: _deployments[i].value}(_deployments[i], msg.sender); + } + } + + function _nonSystemDeployOnAddress( + bytes32 _bytecodeHash, + address _newAddress, + AccountAbstractionVersion _aaVersion, + bytes calldata _input + ) internal { + if (_bytecodeHash == bytes32(0x0)) { + revert EmptyBytes32(); + } + if (uint160(_newAddress) <= MAX_SYSTEM_CONTRACT_ADDRESS) { + revert NotAllowedToDeployInKernelSpace(); + } + + // We do not allow deploying twice on the same address. + bytes32 codeHash = ACCOUNT_CODE_STORAGE_SYSTEM_CONTRACT.getCodeHash(uint256(uint160(_newAddress))); + if (codeHash != 0x0) { + revert HashIsNonZero(codeHash); + } + // Do not allow deploying contracts to default accounts that have already executed transactions. + if (NONCE_HOLDER_SYSTEM_CONTRACT.getRawNonce(_newAddress) != 0x00) { + revert NonEmptyAccount(); + } + + _performDeployOnAddress(_bytecodeHash, _newAddress, _aaVersion, _input); + } + + /// @notice Deploy a certain bytecode on the address. + /// @param _bytecodeHash The correctly formatted hash of the bytecode. + /// @param _newAddress The address of the contract to be deployed. + /// @param _aaVersion The version of the account abstraction protocol to use. + /// @param _input The constructor calldata. + function _performDeployOnAddress( + bytes32 _bytecodeHash, + address _newAddress, + AccountAbstractionVersion _aaVersion, + bytes calldata _input + ) internal { + _ensureBytecodeIsKnown(_bytecodeHash); + + AccountInfo memory newAccountInfo; + newAccountInfo.supportedAAVersion = _aaVersion; + // Accounts have sequential nonces by default. + newAccountInfo.nonceOrdering = AccountNonceOrdering.Sequential; + _storeAccountInfo(_newAddress, newAccountInfo); + + _constructContract({ + _sender: msg.sender, + _newAddress: _newAddress, + _bytecodeHash: _bytecodeHash, + _input: _input, + _isSystem: false, + _callConstructor: true + }); + } + + /// @notice Check that bytecode hash is marked as known on the `KnownCodeStorage` system contracts + function _ensureBytecodeIsKnown(bytes32 _bytecodeHash) internal view { + uint256 knownCodeMarker = KNOWN_CODE_STORAGE_CONTRACT.getMarker(_bytecodeHash); + if (knownCodeMarker == 0) { + revert UnknownCodeHash(_bytecodeHash); + } + } + + /// @notice Ensures that the _newAddress and assigns a new contract hash to it + /// @param _newAddress The address of the deployed contract + /// @param _bytecodeHash The correctly formatted hash of the bytecode. + function _storeConstructingByteCodeHashOnAddress(address _newAddress, bytes32 _bytecodeHash) internal { + // Set the "isConstructor" flag to the bytecode hash + bytes32 constructingBytecodeHash = Utils.constructingBytecodeHash(_bytecodeHash); + ACCOUNT_CODE_STORAGE_SYSTEM_CONTRACT.storeAccountConstructingCodeHash(_newAddress, constructingBytecodeHash); + } + + /// @notice Transfers the `msg.value` ETH to the deployed account & invokes its constructor. + /// This function must revert in case the deployment fails. + /// @param _sender The msg.sender to be used in the constructor + /// @param _newAddress The address of the deployed contract + /// @param _input The constructor calldata + /// @param _isSystem Whether the call should be a system call (could be possibly required in the future). + function _constructContract( + address _sender, + address _newAddress, + bytes32 _bytecodeHash, + bytes calldata _input, + bool _isSystem, + bool _callConstructor + ) internal { + uint256 value = msg.value; + if (_callConstructor) { + // 1. Transfer the balance to the new address on the constructor call. + if (value > 0) { + BASE_TOKEN_SYSTEM_CONTRACT.transferFromTo(address(this), _newAddress, value); + } + // 2. Set the constructed code hash on the account + _storeConstructingByteCodeHashOnAddress(_newAddress, _bytecodeHash); + + // 3. Call the constructor on behalf of the account + if (value > 0) { + // Safe to cast value, because `msg.value` <= `uint128.max` due to `MessageValueSimulator` invariant + SystemContractHelper.setValueForNextFarCall(uint128(value)); + } + bytes memory returnData = EfficientCall.mimicCall({ + _gas: gasleft(), + _address: _newAddress, + _data: _input, + _whoToMimic: _sender, + _isConstructor: true, + _isSystem: _isSystem + }); + // 4. Mark bytecode hash as constructed + ACCOUNT_CODE_STORAGE_SYSTEM_CONTRACT.markAccountCodeHashAsConstructed(_newAddress); + // 5. Set the contract immutables + ImmutableData[] memory immutables = abi.decode(returnData, (ImmutableData[])); + IMMUTABLE_SIMULATOR_SYSTEM_CONTRACT.setImmutables(_newAddress, immutables); + } else { + if (value != 0) { + revert NonEmptyMsgValue(); + } + // If we do not call the constructor, we need to set the constructed code hash. + ACCOUNT_CODE_STORAGE_SYSTEM_CONTRACT.storeAccountConstructedCodeHash(_newAddress, _bytecodeHash); + } + + emit ContractDeployed(_sender, _bytecodeHash, _newAddress); + } +} diff --git a/contracts/system-contracts/Create2Factory.sol b/contracts/system-contracts/Create2Factory.sol new file mode 100644 index 0000000..868de66 --- /dev/null +++ b/contracts/system-contracts/Create2Factory.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {REAL_DEPLOYER_SYSTEM_CONTRACT} from "./Constants.sol"; +import {EfficientCall} from "./libraries/EfficientCall.sol"; +import {IContractDeployer} from "./interfaces/IContractDeployer.sol"; + +/// @custom:security-contact security@matterlabs.dev +/// @author Matter Labs +/// @notice The contract that can be used for deterministic contract deployment. +contract Create2Factory { + /// @notice Function that calls the `create2` method of the `ContractDeployer` contract. + /// @dev This function accepts the same parameters as the `create2` function of the ContractDeployer system contract, + /// so that we could efficiently relay the calldata. + function create2( + bytes32, // _salt + bytes32, // _bytecodeHash + bytes calldata // _input + ) external payable returns (address) { + _relayCall(); + } + + /// @notice Function that calls the `create2Account` method of the `ContractDeployer` contract. + /// @dev This function accepts the same parameters as the `create2Account` function of the ContractDeployer system contract, + /// so that we could efficiently relay the calldata. + function create2Account( + bytes32, // _salt + bytes32, // _bytecodeHash + bytes calldata, // _input + IContractDeployer.AccountAbstractionVersion // _aaVersion + ) external payable returns (address) { + _relayCall(); + } + + /// @notice Function that efficiently relays the calldata to the contract deployer system contract. After that, + /// it also relays full result. + function _relayCall() internal { + bool success = EfficientCall.rawCall({ + _gas: gasleft(), + _address: address(REAL_DEPLOYER_SYSTEM_CONTRACT), + _value: msg.value, + _data: msg.data, + _isSystem: true + }); + + assembly { + returndatacopy(0, 0, returndatasize()) + if success { + return(0, returndatasize()) + } + revert(0, returndatasize()) + } + } +} diff --git a/contracts/system-contracts/DefaultAccount.sol b/contracts/system-contracts/DefaultAccount.sol new file mode 100644 index 0000000..40a38e4 --- /dev/null +++ b/contracts/system-contracts/DefaultAccount.sol @@ -0,0 +1,249 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {IAccount, ACCOUNT_VALIDATION_SUCCESS_MAGIC} from "./interfaces/IAccount.sol"; +import {TransactionHelper, Transaction} from "./libraries/TransactionHelper.sol"; +import {SystemContractsCaller} from "./libraries/SystemContractsCaller.sol"; +import {SystemContractHelper} from "./libraries/SystemContractHelper.sol"; +import {EfficientCall} from "./libraries/EfficientCall.sol"; +import {BOOTLOADER_FORMAL_ADDRESS, NONCE_HOLDER_SYSTEM_CONTRACT, DEPLOYER_SYSTEM_CONTRACT, INonceHolder} from "./Constants.sol"; +import {Utils} from "./libraries/Utils.sol"; +import {InsufficientFunds, InvalidSig, SigField, FailedToPayOperator} from "./SystemContractErrors.sol"; + +/** + * @author Matter Labs + * @custom:security-contact security@matterlabs.dev + * @notice The default implementation of account. + * @dev The bytecode of the contract is set by default for all addresses for which no other bytecodes are deployed. + * @notice If the caller is not a bootloader always returns empty data on call, just like EOA does. + * @notice If it is delegate called always returns empty data, just like EOA does. + */ +contract DefaultAccount is IAccount { + using TransactionHelper for *; + + /** + * @dev Simulate the behavior of the EOA if the caller is not the bootloader. + * Essentially, for all non-bootloader callers halt the execution with empty return data. + * If all functions will use this modifier AND the contract will implement an empty payable fallback() + * then the contract will be indistinguishable from the EOA when called. + */ + modifier ignoreNonBootloader() { + if (msg.sender != BOOTLOADER_FORMAL_ADDRESS) { + // If function was called outside of the bootloader, behave like an EOA. + assembly { + return(0, 0) + } + } + // Continue execution if called from the bootloader. + _; + } + + /** + * @dev Simulate the behavior of the EOA if it is called via `delegatecall`. + * Thus, the default account on a delegate call behaves the same as EOA on Ethereum. + * If all functions will use this modifier AND the contract will implement an empty payable fallback() + * then the contract will be indistinguishable from the EOA when called. + */ + modifier ignoreInDelegateCall() { + address codeAddress = SystemContractHelper.getCodeAddress(); + if (codeAddress != address(this)) { + // If the function was delegate called, behave like an EOA. + assembly { + return(0, 0) + } + } + + // Continue execution if not delegate called. + _; + } + + /// @notice Validates the transaction & increments nonce. + /// @dev The transaction is considered accepted by the account if + /// the call to this function by the bootloader does not revert + /// and the nonce has been set as used. + /// @param _suggestedSignedHash The suggested hash of the transaction to be signed by the user. + /// This is the hash that is signed by the EOA by default. + /// @param _transaction The transaction structure itself. + /// @dev Besides the params above, it also accepts unused first parameter "_txHash", which + /// is the unique (canonical) hash of the transaction. + function validateTransaction( + bytes32, // _txHash + bytes32 _suggestedSignedHash, + Transaction calldata _transaction + ) external payable override ignoreNonBootloader ignoreInDelegateCall returns (bytes4 magic) { + magic = _validateTransaction(_suggestedSignedHash, _transaction); + } + + /// @notice Inner method for validating transaction and increasing the nonce + /// @param _suggestedSignedHash The hash of the transaction signed by the EOA + /// @param _transaction The transaction. + function _validateTransaction( + bytes32 _suggestedSignedHash, + Transaction calldata _transaction + ) internal returns (bytes4 magic) { + // Note, that nonce holder can only be called with "isSystem" flag. + SystemContractsCaller.systemCallWithPropagatedRevert( + uint32(gasleft()), + address(NONCE_HOLDER_SYSTEM_CONTRACT), + 0, + abi.encodeCall(INonceHolder.incrementMinNonceIfEquals, (_transaction.nonce)) + ); + + // Even though for the transaction types present in the system right now, + // we always provide the suggested signed hash, this should not be + // always expected. In case the bootloader has no clue what the default hash + // is, the bytes32(0) will be supplied. + bytes32 txHash = _suggestedSignedHash != bytes32(0) ? _suggestedSignedHash : _transaction.encodeHash(); + + // The fact there is enough balance for the account + // should be checked explicitly to prevent user paying for fee for a + // transaction that wouldn't be included on Ethereum. + uint256 totalRequiredBalance = _transaction.totalRequiredBalance(); + if (totalRequiredBalance > address(this).balance) { + revert InsufficientFunds(totalRequiredBalance, address(this).balance); + } + + if (_isValidSignature(txHash, _transaction.signature)) { + magic = ACCOUNT_VALIDATION_SUCCESS_MAGIC; + } + } + + /// @notice Method called by the bootloader to execute the transaction. + /// @param _transaction The transaction to execute. + /// @dev It also accepts unused _txHash and _suggestedSignedHash parameters: + /// the unique (canonical) hash of the transaction and the suggested signed + /// hash of the transaction. + function executeTransaction( + bytes32, // _txHash + bytes32, // _suggestedSignedHash + Transaction calldata _transaction + ) external payable override ignoreNonBootloader ignoreInDelegateCall { + _execute(_transaction); + } + + /// @notice Method that should be used to initiate a transaction from this account by an external call. + /// @dev The custom account is supposed to implement this method to initiate a transaction on behalf + /// of the account via L1 -> L2 communication. However, the default account can initiate a transaction + /// from L1, so we formally implement the interface method, but it doesn't execute any logic. + /// @param _transaction The transaction to execute. + function executeTransactionFromOutside(Transaction calldata _transaction) external payable override { + // Behave the same as for fallback/receive, just execute nothing, returns nothing + } + + /// @notice Inner method for executing a transaction. + /// @param _transaction The transaction to execute. + function _execute(Transaction calldata _transaction) internal { + address to = address(uint160(_transaction.to)); + uint128 value = Utils.safeCastToU128(_transaction.value); + bytes calldata data = _transaction.data; + uint32 gas = Utils.safeCastToU32(gasleft()); + + // Note, that the deployment method from the deployer contract can only be called with a "systemCall" flag. + bool isSystemCall; + if (to == address(DEPLOYER_SYSTEM_CONTRACT) && data.length >= 4) { + bytes4 selector = bytes4(data[:4]); + // Check that called function is the deployment method, + // the others deployer method is not supposed to be called from the default account. + isSystemCall = + selector == DEPLOYER_SYSTEM_CONTRACT.create.selector || + selector == DEPLOYER_SYSTEM_CONTRACT.create2.selector || + selector == DEPLOYER_SYSTEM_CONTRACT.createAccount.selector || + selector == DEPLOYER_SYSTEM_CONTRACT.create2Account.selector; + } + bool success = EfficientCall.rawCall({ + _gas: gas, + _address: to, + _value: value, + _data: data, + _isSystem: isSystemCall + }); + if (!success) { + EfficientCall.propagateRevert(); + } + } + + /// @notice Validation that the ECDSA signature of the transaction is correct. + /// @param _hash The hash of the transaction to be signed. + /// @param _signature The signature of the transaction. + /// @return EIP1271_SUCCESS_RETURN_VALUE if the signature is correct. It reverts otherwise. + function _isValidSignature(bytes32 _hash, bytes memory _signature) internal view returns (bool) { + if (_signature.length != 65) { + revert InvalidSig(SigField.Length, _signature.length); + } + uint8 v; + bytes32 r; + bytes32 s; + // Signature loading code + // we jump 32 (0x20) as the first slot of bytes contains the length + // we jump 65 (0x41) per signature + // for v we load 32 bytes ending with v (the first 31 come from s) then apply a mask + assembly { + r := mload(add(_signature, 0x20)) + s := mload(add(_signature, 0x40)) + v := and(mload(add(_signature, 0x41)), 0xff) + } + if (v != 27 && v != 28) { + revert InvalidSig(SigField.V, v); + } + + // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature + // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines + // the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most + // signatures from current libraries generate a unique signature with an s-value in the lower half order. + // + // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value + // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or + // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept + // these malleable signatures as well. + if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) { + revert InvalidSig(SigField.S, uint256(s)); + } + + address recoveredAddress = ecrecover(_hash, v, r, s); + + return recoveredAddress == address(this) && recoveredAddress != address(0); + } + + /// @notice Method for paying the bootloader for the transaction. + /// @param _transaction The transaction for which the fee is paid. + /// @dev It also accepts unused _txHash and _suggestedSignedHash parameters: + /// the unique (canonical) hash of the transaction and the suggested signed + /// hash of the transaction. + function payForTransaction( + bytes32, // _txHash + bytes32, // _suggestedSignedHash + Transaction calldata _transaction + ) external payable ignoreNonBootloader ignoreInDelegateCall { + bool success = _transaction.payToTheBootloader(); + if (!success) { + revert FailedToPayOperator(); + } + } + + /// @notice Method, where the user should prepare for the transaction to be + /// paid for by a paymaster. + /// @dev Here, the account should set the allowance for the smart contracts + /// @param _transaction The transaction. + /// @dev It also accepts unused _txHash and _suggestedSignedHash parameters: + /// the unique (canonical) hash of the transaction and the suggested signed + /// hash of the transaction. + function prepareForPaymaster( + bytes32, // _txHash + bytes32, // _suggestedSignedHash + Transaction calldata _transaction + ) external payable ignoreNonBootloader ignoreInDelegateCall { + _transaction.processPaymasterInput(); + } + + fallback() external payable ignoreInDelegateCall { + // fallback of default account shouldn't be called by bootloader under no circumstances + assert(msg.sender != BOOTLOADER_FORMAL_ADDRESS); + + // If the contract is called directly, behave like an EOA + } + + receive() external payable { + // If the contract is called directly, behave like an EOA + } +} diff --git a/contracts/system-contracts/EmptyContract.sol b/contracts/system-contracts/EmptyContract.sol new file mode 100644 index 0000000..15516a7 --- /dev/null +++ b/contracts/system-contracts/EmptyContract.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +/** + * @author Matter Labs + * @custom:security-contact security@matterlabs.dev + * @notice The "empty" contract that is put into some system contracts by default. + */ +contract EmptyContract { + fallback() external payable {} + + receive() external payable {} +} diff --git a/contracts/system-contracts/EventWriter.yul b/contracts/system-contracts/EventWriter.yul new file mode 100644 index 0000000..4cd4a38 --- /dev/null +++ b/contracts/system-contracts/EventWriter.yul @@ -0,0 +1,170 @@ +/** + * @author Matter Labs + * @custom:security-contact security@matterlabs.dev + * @notice The contract responsible for decoding and writing events using low-level instructions. + * @dev The metadata and topics are passed via registers, and the first accessible register contains their number. + * The rest of the data is passed via calldata without copying. + */ +object "EventWriter" { + code { + return(0, 0) + } + object "EventWriter_deployed" { + code { + //////////////////////////////////////////////////////////////// + // HELPER FUNCTIONS + //////////////////////////////////////////////////////////////// + + // For the documentation of the helper functions, please refer to + // the corresponding functions in the SystemContractHelper.sol. + + /// @notice Returns the 0-th extraAbiParam for the current call. + /// @dev It is equal to the value of the 2-th register at the start of the call. + function getExtraAbiData_0() -> extraAbiData { + extraAbiData := verbatim_0i_1o("get_global::extra_abi_data_0") + } + + /// @notice Returns the 1-th extraAbiParam for the current call. + /// @dev It is equal to the value of the 3-th register at the start of the call. + function getExtraAbiData_1() -> extraAbiData { + extraAbiData := verbatim_0i_1o("get_global::extra_abi_data_1") + } + + /// @notice Returns the 2-th extraAbiParam for the current call. + /// @dev It is equal to the value of the 4-th register at the start of the call. + function getExtraAbiData_2() -> extraAbiData { + extraAbiData := verbatim_0i_1o("get_global::extra_abi_data_2") + } + + /// @notice Returns the 3-th extraAbiParam for the current call. + /// @dev It is equal to the value of the 5-th register at the start of the call. + function getExtraAbiData_3() -> extraAbiData { + extraAbiData := verbatim_0i_1o("get_global::extra_abi_data_3") + } + + /// @notice Returns the 4-th extraAbiParam for the current call. + /// @dev It is equal to the value of the 6-th register at the start of the call. + function getExtraAbiData_4() -> extraAbiData { + extraAbiData := verbatim_0i_1o("get_global::extra_abi_data_4") + } + + /// @notice Returns the call flags for the current call. + /// @dev Call flags is the value of the first register at the start of the call. + /// @dev The zero bit of the callFlags indicates whether the call is + /// a constructor call. The first bit of the callFlags indicates whether + /// the call is a system one. + function getCallFlags() -> ret { + ret := verbatim_0i_1o("get_global::call_flags") + } + + /// @notice Initialize a new event + /// @param initializer The event initializing value + /// @param value1 The first topic or data chunk. + function eventInitialize(initializer, value1) { + verbatim_2i_0o("event_initialize", initializer, value1) + } + + /// @notice Continue writing the previously initialized event. + /// @param value1 The first topic or data chunk. + /// @param value2 The second topic or data chunk. + function eventWrite(value1, value2) { + verbatim_2i_0o("event_write", value1, value2) + } + + // @dev Write 1-th topic and first data chunk + function writeFirstTopicWithDataChunk() { + let topic1 := getExtraAbiData_1() + let dataChunk := calldataload(0) + eventWrite(topic1, dataChunk) + } + + // @dev Write 1-th and 2-th event topics + function writeFirstTwoTopics() { + let topic1 := getExtraAbiData_1() + let topic2 := getExtraAbiData_2() + eventWrite(topic1, topic2) + } + + // @dev Write 3-th topic and first data chunk + function writeThirdTopicWithDataChunk() { + let topic3 := getExtraAbiData_3() + let dataChunk := calldataload(0) + eventWrite(topic3, dataChunk) + } + + // @dev Write 3-th and 4-th event topics + function writeSecondTwoTopics() { + let topic3 := getExtraAbiData_3() + let topic4 := getExtraAbiData_4() + eventWrite(topic3, topic4) + } + + // @dev Reverts the call if a caller hasn't set the "isSystem" flag before calling + // Note: this method is different from the `onlySystemCall` modifier that is used in system contracts. + function onlySystemCall() { + let callFlags := getCallFlags() + let isSystemCall := and(callFlags, 2) + + if iszero(isSystemCall) { + revert(0, 0) + } + } + + //////////////////////////////////////////////////////////////// + // FALLBACK + //////////////////////////////////////////////////////////////// + + // Ensure that contract is called on purpose + onlySystemCall() + + let numberOfTopics := getExtraAbiData_0() + // Only 4 indexed fields are allowed, same as on EVM + if gt(numberOfTopics, 4) { + revert(0, 0) + } + + let dataLength := calldatasize() + // Increment number of topics to include the `msg.sender` as a topic + let initializer := add(shl(32, dataLength), add(numberOfTopics, 1)) + eventInitialize(initializer, caller()) + + // Save the pointer to written data + let dataCursor + + // Handle every case separately, to save gas on loops (alternative approach) + switch numberOfTopics + case 0 { + // Nothing to publish + } + case 1 { + writeFirstTopicWithDataChunk() + dataCursor := add(dataCursor, 0x20) + } + case 2 { + writeFirstTwoTopics() + } + case 3 { + writeFirstTwoTopics() + writeThirdTopicWithDataChunk() + dataCursor := add(dataCursor, 0x20) + } + case 4 { + writeFirstTwoTopics() + writeSecondTwoTopics() + } + default { + // Unreachable + revert(0, 0) + } + + // Write all the event data, two words at a time + for {} lt(dataCursor, dataLength) { + dataCursor := add(dataCursor, 0x40) + } { + let chunk1 := calldataload(dataCursor) + let chunk2 := calldataload(add(dataCursor, 0x20)) + eventWrite(chunk1, chunk2) + } + } + } +} diff --git a/contracts/system-contracts/ImmutableSimulator.sol b/contracts/system-contracts/ImmutableSimulator.sol new file mode 100644 index 0000000..93110d7 --- /dev/null +++ b/contracts/system-contracts/ImmutableSimulator.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {IImmutableSimulator, ImmutableData} from "./interfaces/IImmutableSimulator.sol"; +import {DEPLOYER_SYSTEM_CONTRACT} from "./Constants.sol"; +import {Unauthorized} from "./SystemContractErrors.sol"; + +/** + * @author Matter Labs + * @custom:security-contact security@matterlabs.dev + * @notice System smart contract that simulates the behavior of immutable variables in Solidity. + * @dev The contract stores the immutable variables created during deployment by other contracts on his storage. + * @dev This simulator is needed so that smart contracts with the same Solidity code but different + * constructor parameters have the same bytecode. + * @dev The users are not expected to call this contract directly, only indirectly via the compiler simulations + * for the immutable variables in Solidity. + */ +contract ImmutableSimulator is IImmutableSimulator { + /// @dev mapping (contract address) => (index of immutable variable) => value + /// @notice that address uses `uint256` type to leave the option to introduce 32-byte address space in future. + mapping(uint256 contractAddress => mapping(uint256 index => bytes32 value)) internal immutableDataStorage; + + /// @notice Method that returns the immutable with a certain index for a user. + /// @param _dest The address which the immutable belongs to. + /// @param _index The index of the immutable. + /// @return The value of the immutables. + function getImmutable(address _dest, uint256 _index) external view override returns (bytes32) { + return immutableDataStorage[uint256(uint160(_dest))][_index]; + } + + /// @notice Method used by the contract deployer to store the immutables for an account + /// @param _dest The address which to store the immutables for. + /// @param _immutables The list of the immutables. + function setImmutables(address _dest, ImmutableData[] calldata _immutables) external override { + if (msg.sender != address(DEPLOYER_SYSTEM_CONTRACT)) { + revert Unauthorized(msg.sender); + } + unchecked { + uint256 immutablesLength = _immutables.length; + for (uint256 i = 0; i < immutablesLength; ++i) { + uint256 index = _immutables[i].index; + bytes32 value = _immutables[i].value; + immutableDataStorage[uint256(uint160(_dest))][index] = value; + } + } + } +} diff --git a/contracts/system-contracts/KnownCodesStorage.sol b/contracts/system-contracts/KnownCodesStorage.sol new file mode 100644 index 0000000..31fa047 --- /dev/null +++ b/contracts/system-contracts/KnownCodesStorage.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {IKnownCodesStorage} from "./interfaces/IKnownCodesStorage.sol"; +import {SystemContractBase} from "./abstract/SystemContractBase.sol"; +import {Utils} from "./libraries/Utils.sol"; +import {COMPRESSOR_CONTRACT, L1_MESSENGER_CONTRACT} from "./Constants.sol"; +import {Unauthorized, MalformedBytecode, BytecodeError} from "./SystemContractErrors.sol"; + +/** + * @author Matter Labs + * @custom:security-contact security@matterlabs.dev + * @notice The storage of this contract will basically serve as a mapping for the known code hashes. + * @dev Code hash is not strictly a hash, it's a structure where the first byte denotes the version of the hash, + * the second byte denotes whether the contract is constructed, and the next two bytes denote the length in 32-byte words. + * And then the next 28 bytes is the truncated hash. + */ +contract KnownCodesStorage is IKnownCodesStorage, SystemContractBase { + modifier onlyCompressor() { + if (msg.sender != address(COMPRESSOR_CONTRACT)) { + revert Unauthorized(msg.sender); + } + _; + } + + /// @notice The method that is used by the bootloader to mark several bytecode hashes as known. + /// @param _shouldSendToL1 Whether the bytecode should be sent on L1. + /// @param _hashes Hashes of the bytecodes to be marked as known. + function markFactoryDeps(bool _shouldSendToL1, bytes32[] calldata _hashes) external onlyCallFromBootloader { + unchecked { + uint256 hashesLen = _hashes.length; + for (uint256 i = 0; i < hashesLen; ++i) { + _markBytecodeAsPublished(_hashes[i], _shouldSendToL1); + } + } + } + + /// @notice The method used to mark a single bytecode hash as known. + /// @dev Only trusted contacts can call this method, currently only the bytecode compressor. + /// @param _bytecodeHash The hash of the bytecode that is marked as known. + function markBytecodeAsPublished(bytes32 _bytecodeHash) external onlyCompressor { + _markBytecodeAsPublished(_bytecodeHash, false); + } + + /// @notice The method used to mark a single bytecode hash as known + /// @param _bytecodeHash The hash of the bytecode that is marked as known + /// @param _shouldSendToL1 Whether the bytecode should be sent on L1 + function _markBytecodeAsPublished(bytes32 _bytecodeHash, bool _shouldSendToL1) internal { + if (getMarker(_bytecodeHash) == 0) { + _validateBytecode(_bytecodeHash); + + if (_shouldSendToL1) { + L1_MESSENGER_CONTRACT.requestBytecodeL1Publication(_bytecodeHash); + } + + // Save as known, to not resend the log to L1 + assembly { + sstore(_bytecodeHash, 1) + } + + emit MarkedAsKnown(_bytecodeHash, _shouldSendToL1); + } + } + + /// @notice Returns the marker stored for a bytecode hash. 1 means that the bytecode hash is known + /// and can be used for deploying contracts. 0 otherwise. + function getMarker(bytes32 _hash) public view override returns (uint256 marker) { + assembly { + marker := sload(_hash) + } + } + + /// @notice Validates the format of bytecodehash + /// @dev zk-circuit accepts & handles only valid format of bytecode hash, other input has undefined behavior + /// That's why we need to validate it + function _validateBytecode(bytes32 _bytecodeHash) internal pure { + uint8 version = uint8(_bytecodeHash[0]); + if (version != 1 || _bytecodeHash[1] != bytes1(0)) { + revert MalformedBytecode(BytecodeError.Version); + } + + if (Utils.bytecodeLenInWords(_bytecodeHash) % 2 == 0) { + revert MalformedBytecode(BytecodeError.NumberOfWords); + } + } +} diff --git a/contracts/system-contracts/L1Messenger.sol b/contracts/system-contracts/L1Messenger.sol new file mode 100644 index 0000000..f9578b1 --- /dev/null +++ b/contracts/system-contracts/L1Messenger.sol @@ -0,0 +1,351 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {IL1Messenger, L2ToL1Log, L2_L1_LOGS_TREE_DEFAULT_LEAF_HASH, L2_TO_L1_LOG_SERIALIZE_SIZE, STATE_DIFF_COMPRESSION_VERSION_NUMBER} from "./interfaces/IL1Messenger.sol"; +import {SystemContractBase} from "./abstract/SystemContractBase.sol"; +import {SystemContractHelper} from "./libraries/SystemContractHelper.sol"; +import {EfficientCall} from "./libraries/EfficientCall.sol"; +import {Utils} from "./libraries/Utils.sol"; +import {SystemLogKey, SYSTEM_CONTEXT_CONTRACT, KNOWN_CODE_STORAGE_CONTRACT, COMPRESSOR_CONTRACT, STATE_DIFF_ENTRY_SIZE, L2_TO_L1_LOGS_MERKLE_TREE_LEAVES, PUBDATA_CHUNK_PUBLISHER, COMPUTATIONAL_PRICE_FOR_PUBDATA} from "./Constants.sol"; +import {ReconstructionMismatch, PubdataField} from "./SystemContractErrors.sol"; + +/** + * @author Matter Labs + * @custom:security-contact security@matterlabs.dev + * @notice Smart contract for sending arbitrary length messages to L1 + * @dev by default ZkSync can send fixed length messages on L1. + * A fixed length message has 4 parameters `senderAddress` `isService`, `key`, `value`, + * the first one is taken from the context, the other three are chosen by the sender. + * @dev To send a variable length message we use this trick: + * - This system contract accepts a 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. + */ +contract L1Messenger is IL1Messenger, SystemContractBase { + /// @notice Sequential hash of logs sent in the current block. + /// @dev Will be reset at the end of the block to zero value. + bytes32 internal chainedLogsHash; + + /// @notice Number of logs sent in the current block. + /// @dev Will be reset at the end of the block to zero value. + uint256 internal numberOfLogsToProcess; + + /// @notice Sequential hash of hashes of the messages sent in the current block. + /// @dev Will be reset at the end of the block to zero value. + bytes32 internal chainedMessagesHash; + + /// @notice Sequential hash of bytecode hashes that needs to published + /// according to the current block execution invariant. + /// @dev Will be reset at the end of the block to zero value. + bytes32 internal chainedL1BytecodesRevealDataHash; + + /// The gas cost of processing one keccak256 round. + uint256 internal constant KECCAK_ROUND_GAS_COST = 40; + + /// The number of bytes processed in one keccak256 round. + uint256 internal constant KECCAK_ROUND_NUMBER_OF_BYTES = 136; + + /// The gas cost of calculation of keccak256 of bytes array of such length. + function keccakGasCost(uint256 _length) internal pure returns (uint256) { + return KECCAK_ROUND_GAS_COST * (_length / KECCAK_ROUND_NUMBER_OF_BYTES + 1); + } + + /// The gas cost of processing one sha256 round. + uint256 internal constant SHA256_ROUND_GAS_COST = 7; + + /// The number of bytes processed in one sha256 round. + uint256 internal constant SHA256_ROUND_NUMBER_OF_BYTES = 64; + + /// The gas cost of calculation of sha256 of bytes array of such length. + function sha256GasCost(uint256 _length) internal pure returns (uint256) { + return SHA256_ROUND_GAS_COST * ((_length + 8) / SHA256_ROUND_NUMBER_OF_BYTES + 1); + } + + /// @notice Sends L2ToL1Log. + /// @param _isService The `isService` flag. + /// @param _key The `key` part of the L2Log. + /// @param _value The `value` part of the L2Log. + /// @dev Can be called only by a system contract. + function sendL2ToL1Log( + bool _isService, + bytes32 _key, + bytes32 _value + ) external onlyCallFromSystemContract returns (uint256 logIdInMerkleTree) { + L2ToL1Log memory l2ToL1Log = L2ToL1Log({ + l2ShardId: 0, + isService: _isService, + txNumberInBlock: SYSTEM_CONTEXT_CONTRACT.txNumberInBlock(), + sender: msg.sender, + key: _key, + value: _value + }); + logIdInMerkleTree = _processL2ToL1Log(l2ToL1Log); + + // We need to charge cost of hashing, as it will be used in `publishPubdataAndClearState`: + // - keccakGasCost(L2_TO_L1_LOG_SERIALIZE_SIZE) and keccakGasCost(64) when reconstructing L2ToL1Log + // - at most 1 time keccakGasCost(64) when building the Merkle tree (as merkle tree can contain + // ~2*N nodes, where the first N nodes are leaves the hash of which is calculated on the previous step). + uint256 gasToPay = keccakGasCost(L2_TO_L1_LOG_SERIALIZE_SIZE) + 2 * keccakGasCost(64); + SystemContractHelper.burnGas(Utils.safeCastToU32(gasToPay), uint32(L2_TO_L1_LOG_SERIALIZE_SIZE)); + } + + /// @notice Internal function to send L2ToL1Log. + function _processL2ToL1Log(L2ToL1Log memory _l2ToL1Log) internal returns (uint256 logIdInMerkleTree) { + bytes32 hashedLog = keccak256( + // solhint-disable-next-line func-named-parameters + abi.encodePacked( + _l2ToL1Log.l2ShardId, + _l2ToL1Log.isService, + _l2ToL1Log.txNumberInBlock, + _l2ToL1Log.sender, + _l2ToL1Log.key, + _l2ToL1Log.value + ) + ); + + chainedLogsHash = keccak256(abi.encode(chainedLogsHash, hashedLog)); + + logIdInMerkleTree = numberOfLogsToProcess; + ++numberOfLogsToProcess; + + emit L2ToL1LogSent(_l2ToL1Log); + } + + /// @notice Public functionality to send messages to L1. + /// @param _message The message intended to be sent to L1. + function sendToL1(bytes calldata _message) external override returns (bytes32 hash) { + uint256 gasBeforeMessageHashing = gasleft(); + hash = EfficientCall.keccak(_message); + uint256 gasSpentOnMessageHashing = gasBeforeMessageHashing - gasleft(); + + /// Store message record + chainedMessagesHash = keccak256(abi.encode(chainedMessagesHash, hash)); + + /// Store log record + L2ToL1Log memory l2ToL1Log = L2ToL1Log({ + l2ShardId: 0, + isService: true, + txNumberInBlock: SYSTEM_CONTEXT_CONTRACT.txNumberInBlock(), + sender: address(this), + key: bytes32(uint256(uint160(msg.sender))), + value: hash + }); + _processL2ToL1Log(l2ToL1Log); + + uint256 pubdataLen; + unchecked { + // 4 bytes used to encode the length of the message (see `publishPubdataAndClearState`) + // L2_TO_L1_LOG_SERIALIZE_SIZE bytes used to encode L2ToL1Log + pubdataLen = 4 + _message.length + L2_TO_L1_LOG_SERIALIZE_SIZE; + } + + // We need to charge cost of hashing, as it will be used in `publishPubdataAndClearState`: + // - keccakGasCost(L2_TO_L1_LOG_SERIALIZE_SIZE) and keccakGasCost(64) when reconstructing L2ToL1Log + // - keccakGasCost(64) and gasSpentOnMessageHashing when reconstructing Messages + // - at most 1 time keccakGasCost(64) when building the Merkle tree (as merkle tree can contain + // ~2*N nodes, where the first N nodes are leaves the hash of which is calculated on the previous step). + uint256 gasToPay = keccakGasCost(L2_TO_L1_LOG_SERIALIZE_SIZE) + + 3 * + keccakGasCost(64) + + gasSpentOnMessageHashing + + COMPUTATIONAL_PRICE_FOR_PUBDATA * + pubdataLen; + SystemContractHelper.burnGas(Utils.safeCastToU32(gasToPay), uint32(pubdataLen)); + + emit L1MessageSent(msg.sender, hash, _message); + } + + /// @dev Can be called only by KnownCodesStorage system contract. + /// @param _bytecodeHash Hash of bytecode being published to L1. + function requestBytecodeL1Publication( + bytes32 _bytecodeHash + ) external override onlyCallFrom(address(KNOWN_CODE_STORAGE_CONTRACT)) { + chainedL1BytecodesRevealDataHash = keccak256(abi.encode(chainedL1BytecodesRevealDataHash, _bytecodeHash)); + + uint256 bytecodeLen = Utils.bytecodeLenInBytes(_bytecodeHash); + + uint256 pubdataLen; + unchecked { + // 4 bytes used to encode the length of the bytecode (see `publishPubdataAndClearState`) + pubdataLen = 4 + bytecodeLen; + } + + // We need to charge cost of hashing, as it will be used in `publishPubdataAndClearState` + uint256 gasToPay = sha256GasCost(bytecodeLen) + + keccakGasCost(64) + + COMPUTATIONAL_PRICE_FOR_PUBDATA * + pubdataLen; + SystemContractHelper.burnGas(Utils.safeCastToU32(gasToPay), uint32(pubdataLen)); + + emit BytecodeL1PublicationRequested(_bytecodeHash); + } + + /// @notice Verifies that the {_totalL2ToL1PubdataAndStateDiffs} reflects what occurred within the L1Batch and that + /// the compressed statediffs are equivalent to the full state diffs. + /// @param _totalL2ToL1PubdataAndStateDiffs The total pubdata and uncompressed state diffs of transactions that were + /// processed in the current L1 Batch. Pubdata consists of L2 to L1 Logs, messages, deployed bytecode, and state diffs. + /// @dev Function that should be called exactly once per L1 Batch by the bootloader. + /// @dev Checks that totalL2ToL1Pubdata is strictly packed data that should to be published to L1. + /// @dev The data passed in also contains the encoded state diffs to be checked again, however this is aux data that is not + /// part of the committed pubdata. + /// @dev Performs calculation of L2ToL1Logs merkle tree root, "sends" such root and keccak256(totalL2ToL1Pubdata) + /// to L1 using low-level (VM) L2Log. + function publishPubdataAndClearState( + bytes calldata _totalL2ToL1PubdataAndStateDiffs + ) external onlyCallFromBootloader { + uint256 calldataPtr = 0; + + /// Check logs + uint32 numberOfL2ToL1Logs = uint32(bytes4(_totalL2ToL1PubdataAndStateDiffs[calldataPtr:calldataPtr + 4])); + if (numberOfL2ToL1Logs > L2_TO_L1_LOGS_MERKLE_TREE_LEAVES) { + revert ReconstructionMismatch( + PubdataField.NumberOfLogs, + bytes32(L2_TO_L1_LOGS_MERKLE_TREE_LEAVES), + bytes32(uint256(numberOfL2ToL1Logs)) + ); + } + calldataPtr += 4; + + bytes32[] memory l2ToL1LogsTreeArray = new bytes32[](L2_TO_L1_LOGS_MERKLE_TREE_LEAVES); + bytes32 reconstructedChainedLogsHash = bytes32(0); + for (uint256 i = 0; i < numberOfL2ToL1Logs; ++i) { + bytes32 hashedLog = EfficientCall.keccak( + _totalL2ToL1PubdataAndStateDiffs[calldataPtr:calldataPtr + L2_TO_L1_LOG_SERIALIZE_SIZE] + ); + calldataPtr += L2_TO_L1_LOG_SERIALIZE_SIZE; + l2ToL1LogsTreeArray[i] = hashedLog; + reconstructedChainedLogsHash = keccak256(abi.encode(reconstructedChainedLogsHash, hashedLog)); + } + if (reconstructedChainedLogsHash != chainedLogsHash) { + revert ReconstructionMismatch(PubdataField.LogsHash, chainedLogsHash, reconstructedChainedLogsHash); + } + for (uint256 i = numberOfL2ToL1Logs; i < L2_TO_L1_LOGS_MERKLE_TREE_LEAVES; ++i) { + l2ToL1LogsTreeArray[i] = L2_L1_LOGS_TREE_DEFAULT_LEAF_HASH; + } + uint256 nodesOnCurrentLevel = L2_TO_L1_LOGS_MERKLE_TREE_LEAVES; + while (nodesOnCurrentLevel > 1) { + nodesOnCurrentLevel /= 2; + for (uint256 i = 0; i < nodesOnCurrentLevel; ++i) { + l2ToL1LogsTreeArray[i] = keccak256( + abi.encode(l2ToL1LogsTreeArray[2 * i], l2ToL1LogsTreeArray[2 * i + 1]) + ); + } + } + bytes32 l2ToL1LogsTreeRoot = l2ToL1LogsTreeArray[0]; + + /// Check messages + uint32 numberOfMessages = uint32(bytes4(_totalL2ToL1PubdataAndStateDiffs[calldataPtr:calldataPtr + 4])); + calldataPtr += 4; + bytes32 reconstructedChainedMessagesHash = bytes32(0); + for (uint256 i = 0; i < numberOfMessages; ++i) { + uint32 currentMessageLength = uint32(bytes4(_totalL2ToL1PubdataAndStateDiffs[calldataPtr:calldataPtr + 4])); + calldataPtr += 4; + bytes32 hashedMessage = EfficientCall.keccak( + _totalL2ToL1PubdataAndStateDiffs[calldataPtr:calldataPtr + currentMessageLength] + ); + calldataPtr += currentMessageLength; + reconstructedChainedMessagesHash = keccak256(abi.encode(reconstructedChainedMessagesHash, hashedMessage)); + } + if (reconstructedChainedMessagesHash != chainedMessagesHash) { + revert ReconstructionMismatch(PubdataField.MsgHash, chainedMessagesHash, reconstructedChainedMessagesHash); + } + + /// Check bytecodes + uint32 numberOfBytecodes = uint32(bytes4(_totalL2ToL1PubdataAndStateDiffs[calldataPtr:calldataPtr + 4])); + calldataPtr += 4; + bytes32 reconstructedChainedL1BytecodesRevealDataHash = bytes32(0); + for (uint256 i = 0; i < numberOfBytecodes; ++i) { + uint32 currentBytecodeLength = uint32( + bytes4(_totalL2ToL1PubdataAndStateDiffs[calldataPtr:calldataPtr + 4]) + ); + calldataPtr += 4; + reconstructedChainedL1BytecodesRevealDataHash = keccak256( + abi.encode( + reconstructedChainedL1BytecodesRevealDataHash, + Utils.hashL2Bytecode( + _totalL2ToL1PubdataAndStateDiffs[calldataPtr:calldataPtr + currentBytecodeLength] + ) + ) + ); + calldataPtr += currentBytecodeLength; + } + if (reconstructedChainedL1BytecodesRevealDataHash != chainedL1BytecodesRevealDataHash) { + revert ReconstructionMismatch( + PubdataField.Bytecode, + chainedL1BytecodesRevealDataHash, + reconstructedChainedL1BytecodesRevealDataHash + ); + } + + /// Check State Diffs + /// encoding is as follows: + /// header (1 byte version, 3 bytes total len of compressed, 1 byte enumeration index size) + /// body (`compressedStateDiffSize` bytes, 4 bytes number of state diffs, `numberOfStateDiffs` * `STATE_DIFF_ENTRY_SIZE` bytes for the uncompressed state diffs) + /// encoded state diffs: [20bytes address][32bytes key][32bytes derived key][8bytes enum index][32bytes initial value][32bytes final value] + if ( + uint256(uint8(bytes1(_totalL2ToL1PubdataAndStateDiffs[calldataPtr]))) != + STATE_DIFF_COMPRESSION_VERSION_NUMBER + ) { + revert ReconstructionMismatch( + PubdataField.StateDiffCompressionVersion, + bytes32(STATE_DIFF_COMPRESSION_VERSION_NUMBER), + bytes32(uint256(uint8(bytes1(_totalL2ToL1PubdataAndStateDiffs[calldataPtr])))) + ); + } + ++calldataPtr; + + uint24 compressedStateDiffSize = uint24(bytes3(_totalL2ToL1PubdataAndStateDiffs[calldataPtr:calldataPtr + 3])); + calldataPtr += 3; + + uint8 enumerationIndexSize = uint8(bytes1(_totalL2ToL1PubdataAndStateDiffs[calldataPtr])); + ++calldataPtr; + + bytes calldata compressedStateDiffs = _totalL2ToL1PubdataAndStateDiffs[calldataPtr:calldataPtr + + compressedStateDiffSize]; + calldataPtr += compressedStateDiffSize; + + bytes calldata totalL2ToL1Pubdata = _totalL2ToL1PubdataAndStateDiffs[:calldataPtr]; + + uint32 numberOfStateDiffs = uint32(bytes4(_totalL2ToL1PubdataAndStateDiffs[calldataPtr:calldataPtr + 4])); + calldataPtr += 4; + + bytes calldata stateDiffs = _totalL2ToL1PubdataAndStateDiffs[calldataPtr:calldataPtr + + (numberOfStateDiffs * STATE_DIFF_ENTRY_SIZE)]; + calldataPtr += numberOfStateDiffs * STATE_DIFF_ENTRY_SIZE; + + bytes32 stateDiffHash = COMPRESSOR_CONTRACT.verifyCompressedStateDiffs( + numberOfStateDiffs, + enumerationIndexSize, + stateDiffs, + compressedStateDiffs + ); + + /// Check for calldata strict format + if (calldataPtr != _totalL2ToL1PubdataAndStateDiffs.length) { + revert ReconstructionMismatch( + PubdataField.ExtraData, + bytes32(calldataPtr), + bytes32(_totalL2ToL1PubdataAndStateDiffs.length) + ); + } + + PUBDATA_CHUNK_PUBLISHER.chunkAndPublishPubdata(totalL2ToL1Pubdata); + + /// Native (VM) L2 to L1 log + SystemContractHelper.toL1(true, bytes32(uint256(SystemLogKey.L2_TO_L1_LOGS_TREE_ROOT_KEY)), l2ToL1LogsTreeRoot); + SystemContractHelper.toL1( + true, + bytes32(uint256(SystemLogKey.TOTAL_L2_TO_L1_PUBDATA_KEY)), + EfficientCall.keccak(totalL2ToL1Pubdata) + ); + SystemContractHelper.toL1(true, bytes32(uint256(SystemLogKey.STATE_DIFF_HASH_KEY)), stateDiffHash); + + /// Clear logs state + chainedLogsHash = bytes32(0); + numberOfLogsToProcess = 0; + chainedMessagesHash = bytes32(0); + chainedL1BytecodesRevealDataHash = bytes32(0); + } +} diff --git a/contracts/system-contracts/L2BaseToken.sol b/contracts/system-contracts/L2BaseToken.sol new file mode 100644 index 0000000..9f826a8 --- /dev/null +++ b/contracts/system-contracts/L2BaseToken.sol @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {IBaseToken} from "./interfaces/IBaseToken.sol"; +import {SystemContractBase} from "./abstract/SystemContractBase.sol"; +import {MSG_VALUE_SYSTEM_CONTRACT, DEPLOYER_SYSTEM_CONTRACT, BOOTLOADER_FORMAL_ADDRESS, L1_MESSENGER_CONTRACT} from "./Constants.sol"; +import {IMailbox} from "./interfaces/IMailbox.sol"; +import {Unauthorized, InsufficientFunds} from "./SystemContractErrors.sol"; + +/** + * @author Matter Labs + * @custom:security-contact security@matterlabs.dev + * @notice Native ETH contract. + * @dev It does NOT provide interfaces for personal interaction with tokens like `transfer`, `approve`, and `transferFrom`. + * Instead, this contract is used by the bootloader and `MsgValueSimulator`/`ContractDeployer` system contracts + * to perform the balance changes while simulating the `msg.value` Ethereum behavior. + */ +contract L2BaseToken is IBaseToken, SystemContractBase { + /// @notice The balances of the users. + mapping(address account => uint256 balance) internal balance; + + /// @notice The total amount of tokens that have been minted. + uint256 public override totalSupply; + + /// @notice Transfer tokens from one address to another. + /// @param _from The address to transfer the ETH from. + /// @param _to The address to transfer the ETH to. + /// @param _amount The amount of ETH in wei being transferred. + /// @dev This function can be called only by trusted system contracts. + /// @dev This function also emits "Transfer" event, which might be removed + /// later on. + function transferFromTo(address _from, address _to, uint256 _amount) external override { + if ( + msg.sender != MSG_VALUE_SYSTEM_CONTRACT && + msg.sender != address(DEPLOYER_SYSTEM_CONTRACT) && + msg.sender != BOOTLOADER_FORMAL_ADDRESS + ) { + revert Unauthorized(msg.sender); + } + + uint256 fromBalance = balance[_from]; + if (fromBalance < _amount) { + revert InsufficientFunds(_amount, fromBalance); + } + unchecked { + balance[_from] = fromBalance - _amount; + // Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by + // decrementing then incrementing. + balance[_to] += _amount; + } + + emit Transfer(_from, _to, _amount); + } + + /// @notice Returns ETH balance of an account + /// @dev It takes `uint256` as an argument to be able to properly simulate the behaviour of the + /// Ethereum's `BALANCE` opcode that accepts uint256 as an argument and truncates any upper bits + /// @param _account The address of the account to return the balance of. + function balanceOf(uint256 _account) external view override returns (uint256) { + return balance[address(uint160(_account))]; + } + + /// @notice Increase the total supply of tokens and balance of the receiver. + /// @dev This method is only callable by the bootloader. + /// @param _account The address which to mint the funds to. + /// @param _amount The amount of ETH in wei to be minted. + function mint(address _account, uint256 _amount) external override onlyCallFromBootloader { + totalSupply += _amount; + balance[_account] += _amount; + emit Mint(_account, _amount); + } + + /// @notice Initiate the withdrawal of the base token, funds will be available to claim on L1 `finalizeEthWithdrawal` method. + /// @param _l1Receiver The address on L1 to receive the funds. + function withdraw(address _l1Receiver) external payable override { + uint256 amount = _burnMsgValue(); + + // Send the L2 log, a user could use it as proof of the withdrawal + bytes memory message = _getL1WithdrawMessage(_l1Receiver, amount); + L1_MESSENGER_CONTRACT.sendToL1(message); + + emit Withdrawal(msg.sender, _l1Receiver, amount); + } + + /// @notice Initiate the withdrawal of the base token, with the sent message. The funds will be available to claim on L1 `finalizeEthWithdrawal` method. + /// @param _l1Receiver The address on L1 to receive the funds. + /// @param _additionalData Additional data to be sent to L1 with the withdrawal. + function withdrawWithMessage(address _l1Receiver, bytes calldata _additionalData) external payable override { + uint256 amount = _burnMsgValue(); + + // Send the L2 log, a user could use it as proof of the withdrawal + bytes memory message = _getExtendedWithdrawMessage(_l1Receiver, amount, msg.sender, _additionalData); + L1_MESSENGER_CONTRACT.sendToL1(message); + + emit WithdrawalWithMessage(msg.sender, _l1Receiver, amount, _additionalData); + } + + /// @dev The function burn the sent `msg.value`. + /// NOTE: Since this contract holds the mapping of all ether balances of the system, + /// the sent `msg.value` is added to the `this` balance before the call. + /// So the balance of `address(this)` is always bigger or equal to the `msg.value`! + function _burnMsgValue() internal returns (uint256 amount) { + amount = msg.value; + + // Silent burning of the ether + unchecked { + // This is safe, since this contract holds the ether balances, and if user + // sends a `msg.value` it will be added to the contract (`this`) balance. + balance[address(this)] -= amount; + totalSupply -= amount; + } + } + + /// @dev Get the message to be sent to L1 to initiate a withdrawal. + function _getL1WithdrawMessage(address _to, uint256 _amount) internal pure returns (bytes memory) { + return abi.encodePacked(IMailbox.finalizeEthWithdrawal.selector, _to, _amount); + } + + /// @dev Get the message to be sent to L1 to initiate a withdrawal. + function _getExtendedWithdrawMessage( + address _to, + uint256 _amount, + address _sender, + bytes memory _additionalData + ) internal pure returns (bytes memory) { + // solhint-disable-next-line func-named-parameters + return abi.encodePacked(IMailbox.finalizeEthWithdrawal.selector, _to, _amount, _sender, _additionalData); + } + + /// @dev This method has not been stabilized and might be + /// removed later on. + function name() external pure override returns (string memory) { + return "Ether"; + } + + /// @dev This method has not been stabilized and might be + /// removed later on. + function symbol() external pure override returns (string memory) { + return "ETH"; + } + + /// @dev This method has not been stabilized and might be + /// removed later on. + function decimals() external pure override returns (uint8) { + return 18; + } +} diff --git a/contracts/system-contracts/MsgValueSimulator.sol b/contracts/system-contracts/MsgValueSimulator.sol new file mode 100644 index 0000000..5fcd0f2 --- /dev/null +++ b/contracts/system-contracts/MsgValueSimulator.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {Utils} from "./libraries/Utils.sol"; +import {EfficientCall} from "./libraries/EfficientCall.sol"; +import {SystemContractBase} from "./abstract/SystemContractBase.sol"; +import {SystemContractHelper} from "./libraries/SystemContractHelper.sol"; +import {MSG_VALUE_SIMULATOR_IS_SYSTEM_BIT, REAL_BASE_TOKEN_SYSTEM_CONTRACT} from "./Constants.sol"; +import {InvalidCall} from "./SystemContractErrors.sol"; + +/** + * @author Matter Labs + * @custom:security-contact security@matterlabs.dev + * @notice The contract responsible for simulating transactions with `msg.value` inside zkEVM. + * @dev It accepts value and whether the call should be system in the first extraAbi param and + * the address to call in the second extraAbi param, transfers the funds and uses `mimicCall` to continue the + * call with the same msg.sender. + */ +contract MsgValueSimulator is SystemContractBase { + /// @notice Extract value, isSystemCall and to from the extraAbi params. + /// @dev The contract accepts value, the callee and whether the call should be a system one via its ABI params. + /// @dev The first ABI param contains the value in the [0..127] bits. The 128th contains + /// the flag whether or not the call should be a system one. + /// The second ABI params contains the callee. + function _getAbiParams() internal view returns (uint256 value, bool isSystemCall, address to) { + value = SystemContractHelper.getExtraAbiData(0); + uint256 addressAsUint = SystemContractHelper.getExtraAbiData(1); + uint256 mask = SystemContractHelper.getExtraAbiData(2); + + isSystemCall = (mask & MSG_VALUE_SIMULATOR_IS_SYSTEM_BIT) != 0; + + to = address(uint160(addressAsUint)); + } + + /// @notice The maximal number of gas out of the stipend that should be passed to the callee. + uint256 private constant GAS_TO_PASS = 2300; + + /// @notice The amount of gas that is passed to the MsgValueSimulator as a stipend. + /// This number servers to pay for the ETH transfer as well as to provide gas for the `GAS_TO_PASS` gas. + /// It is equal to the following constant: https://github.com/matter-labs/era-zkevm_opcode_defs/blob/7bf8016f5bb13a73289f321ad6ea8f614540ece9/src/system_params.rs#L96. + uint256 private constant MSG_VALUE_SIMULATOR_STIPEND_GAS = 27000; + + /// @notice The fallback function that is the main entry point for the MsgValueSimulator. + /// @dev The contract accepts value, the callee and whether the call should be a system one via its ABI params. + /// @param _data The calldata to be passed to the callee. + /// @return The return data from the callee. + // solhint-disable-next-line payable-fallback + fallback(bytes calldata _data) external onlySystemCall returns (bytes memory) { + // Firstly we calculate how much gas has been actually provided by the user to the inner call. + // For that, we need to get the total gas available in this context and subtract the stipend from it. + uint256 gasInContext = gasleft(); + // Note, that the `gasInContext` might be slightly less than the MSG_VALUE_SIMULATOR_STIPEND_GAS, since + // by the time we retrieve it, some gas might have already been spent, e.g. on the `gasleft` opcode itself. + uint256 userGas = gasInContext > MSG_VALUE_SIMULATOR_STIPEND_GAS + ? gasInContext - MSG_VALUE_SIMULATOR_STIPEND_GAS + : 0; + + (uint256 value, bool isSystemCall, address to) = _getAbiParams(); + + // Prevent mimic call to the MsgValueSimulator to prevent an unexpected change of callee. + if (to == address(this)) { + revert InvalidCall(); + } + + if (value != 0) { + (bool success, ) = address(REAL_BASE_TOKEN_SYSTEM_CONTRACT).call( + abi.encodeCall(REAL_BASE_TOKEN_SYSTEM_CONTRACT.transferFromTo, (msg.sender, to, value)) + ); + + // If the transfer of ETH fails, we do the most Ethereum-like behaviour in such situation: revert(0,0) + if (!success) { + assembly { + revert(0, 0) + } + } + + // If value is non-zero, we also provide additional gas to the callee. + userGas += GAS_TO_PASS; + } + + // For the next call this `msg.value` will be used. + SystemContractHelper.setValueForNextFarCall(Utils.safeCastToU128(value)); + + return + EfficientCall.mimicCall({ + _gas: userGas, + _address: to, + _data: _data, + _whoToMimic: msg.sender, + _isConstructor: false, + _isSystem: isSystemCall + }); + } +} diff --git a/contracts/system-contracts/NonceHolder.sol b/contracts/system-contracts/NonceHolder.sol new file mode 100644 index 0000000..cca07b1 --- /dev/null +++ b/contracts/system-contracts/NonceHolder.sol @@ -0,0 +1,191 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {INonceHolder} from "./interfaces/INonceHolder.sol"; +import {IContractDeployer} from "./interfaces/IContractDeployer.sol"; +import {SystemContractBase} from "./abstract/SystemContractBase.sol"; +import {DEPLOYER_SYSTEM_CONTRACT} from "./Constants.sol"; +import {NonceIncreaseError, ZeroNonceError, NonceJumpError, ValueMismatch, NonceAlreadyUsed, NonceNotUsed, Unauthorized} from "./SystemContractErrors.sol"; + +/** + * @author Matter Labs + * @custom:security-contact security@matterlabs.dev + * @notice A contract used for managing nonces for accounts. Together with bootloader, + * this contract ensures that the pair (sender, nonce) is always unique, ensuring + * unique transaction hashes. + * @dev The account allows for both ascending growth in nonces and mapping nonces to specific + * stored values in them. + * The users can mark a range of nonces by increasing the `minNonce`. This way all the nonces + * less than `minNonce` will become used. The other way to mark a certain 256-bit key as nonce is to set + * some value under it in this contract. + * @dev Apart from transaction nonces, this contract also stores the deployment nonce for accounts, that + * will be used for address derivation using CREATE. For the economy of space, this nonce is stored tightly + * packed with the `minNonce`. + * @dev The behavior of some of the methods depends on the nonce ordering of the account. Nonce ordering is a mere suggestion and all the checks that are present + * here serve more as a help to users to prevent from doing mistakes, rather than any invariants. + */ +contract NonceHolder is INonceHolder, SystemContractBase { + uint256 private constant DEPLOY_NONCE_MULTIPLIER = 2 ** 128; + /// The minNonce can be increased by 2^32 at a time to prevent it from + /// overflowing beyond 2**128. + uint256 private constant MAXIMAL_MIN_NONCE_INCREMENT = 2 ** 32; + + /// RawNonces for accounts are stored in format + /// minNonce + 2^128 * deploymentNonce, where deploymentNonce + /// is the nonce used for deploying smart contracts. + mapping(uint256 account => uint256 packedMinAndDeploymentNonce) internal rawNonces; + + /// Mapping of values under nonces for accounts. + /// The main key of the mapping is the 256-bit address of the account, while the + /// inner mapping is a mapping from a nonce to the value stored there. + mapping(uint256 account => mapping(uint256 nonceKey => uint256 value)) internal nonceValues; + + /// @notice Returns the current minimal nonce for account. + /// @param _address The account to return the minimal nonce for + /// @return The current minimal nonce for this account. + function getMinNonce(address _address) public view returns (uint256) { + uint256 addressAsKey = uint256(uint160(_address)); + (, uint256 minNonce) = _splitRawNonce(rawNonces[addressAsKey]); + + return minNonce; + } + + /// @notice Returns the raw version of the current minimal nonce + /// @dev It is equal to minNonce + 2^128 * deployment nonce. + /// @param _address The account to return the raw nonce for + /// @return The raw nonce for this account. + function getRawNonce(address _address) public view returns (uint256) { + uint256 addressAsKey = uint256(uint160(_address)); + return rawNonces[addressAsKey]; + } + + /// @notice Increases the minimal nonce for the msg.sender and returns the previous one. + /// @param _value The number by which to increase the minimal nonce for msg.sender. + /// @return oldMinNonce The value of the minimal nonce for msg.sender before the increase. + function increaseMinNonce(uint256 _value) public onlySystemCall returns (uint256 oldMinNonce) { + if (_value > MAXIMAL_MIN_NONCE_INCREMENT) { + revert NonceIncreaseError(MAXIMAL_MIN_NONCE_INCREMENT, _value); + } + + uint256 addressAsKey = uint256(uint160(msg.sender)); + uint256 oldRawNonce = rawNonces[addressAsKey]; + + unchecked { + rawNonces[addressAsKey] = (oldRawNonce + _value); + } + + (, oldMinNonce) = _splitRawNonce(oldRawNonce); + } + + /// @notice Sets the nonce value `key` for the msg.sender as used. + /// @param _key The nonce key under which the value will be set. + /// @param _value The value to store under the _key. + /// @dev The value must be non-zero. + function setValueUnderNonce(uint256 _key, uint256 _value) public onlySystemCall { + IContractDeployer.AccountInfo memory accountInfo = DEPLOYER_SYSTEM_CONTRACT.getAccountInfo(msg.sender); + + if (_value == 0) { + revert ZeroNonceError(); + } + // If an account has sequential nonce ordering, we enforce that the previous + // nonce has already been used. + if (accountInfo.nonceOrdering == IContractDeployer.AccountNonceOrdering.Sequential && _key != 0) { + if (!isNonceUsed(msg.sender, _key - 1)) { + revert NonceJumpError(); + } + } + + uint256 addressAsKey = uint256(uint160(msg.sender)); + + nonceValues[addressAsKey][_key] = _value; + + emit ValueSetUnderNonce(msg.sender, _key, _value); + } + + /// @notice Gets the value stored under a custom nonce for msg.sender. + /// @param _key The key under which to get the stored value. + /// @return The value stored under the `_key` for the msg.sender. + function getValueUnderNonce(uint256 _key) public view returns (uint256) { + uint256 addressAsKey = uint256(uint160(msg.sender)); + return nonceValues[addressAsKey][_key]; + } + + /// @notice A convenience method to increment the minimal nonce if it is equal + /// to the `_expectedNonce`. + /// @param _expectedNonce The expected minimal nonce for the account. + function incrementMinNonceIfEquals(uint256 _expectedNonce) external onlySystemCall { + uint256 addressAsKey = uint256(uint160(msg.sender)); + uint256 oldRawNonce = rawNonces[addressAsKey]; + + (, uint256 oldMinNonce) = _splitRawNonce(oldRawNonce); + if (oldMinNonce != _expectedNonce) { + revert ValueMismatch(_expectedNonce, oldMinNonce); + } + + unchecked { + rawNonces[addressAsKey] = oldRawNonce + 1; + } + } + + /// @notice Returns the deployment nonce for the accounts used for CREATE opcode. + /// @param _address The address to return the deploy nonce of. + /// @return deploymentNonce The deployment nonce of the account. + function getDeploymentNonce(address _address) external view returns (uint256 deploymentNonce) { + uint256 addressAsKey = uint256(uint160(_address)); + (deploymentNonce, ) = _splitRawNonce(rawNonces[addressAsKey]); + + return deploymentNonce; + } + + /// @notice Increments the deployment nonce for the account and returns the previous one. + /// @param _address The address of the account which to return the deploy nonce for. + /// @return prevDeploymentNonce The deployment nonce at the time this function is called. + function incrementDeploymentNonce(address _address) external returns (uint256 prevDeploymentNonce) { + if (msg.sender != address(DEPLOYER_SYSTEM_CONTRACT)) { + revert Unauthorized(msg.sender); + } + uint256 addressAsKey = uint256(uint160(_address)); + uint256 oldRawNonce = rawNonces[addressAsKey]; + + unchecked { + rawNonces[addressAsKey] = (oldRawNonce + DEPLOY_NONCE_MULTIPLIER); + } + + (prevDeploymentNonce, ) = _splitRawNonce(oldRawNonce); + } + + /// @notice A method that checks whether the nonce has been used before. + /// @param _address The address the nonce of which is being checked. + /// @param _nonce The nonce value which is checked. + /// @return `true` if the nonce has been used, `false` otherwise. + function isNonceUsed(address _address, uint256 _nonce) public view returns (bool) { + uint256 addressAsKey = uint256(uint160(_address)); + return (_nonce < getMinNonce(_address) || nonceValues[addressAsKey][_nonce] > 0); + } + + /// @notice Checks and reverts based on whether the nonce is used (not used). + /// @param _address The address the nonce of which is being checked. + /// @param _key The nonce value which is tested. + /// @param _shouldBeUsed The flag for the method. If `true`, the method checks that whether this nonce + /// is marked as used and reverts if this is not the case. If `false`, this method will check that the nonce + /// has *not* been used yet, and revert otherwise. + /// @dev This method should be used by the bootloader. + function validateNonceUsage(address _address, uint256 _key, bool _shouldBeUsed) external view { + bool isUsed = isNonceUsed(_address, _key); + + if (isUsed && !_shouldBeUsed) { + revert NonceAlreadyUsed(_address, _key); + } else if (!isUsed && _shouldBeUsed) { + revert NonceNotUsed(_address, _key); + } + } + + /// @notice Splits the raw nonce value into the deployment nonce and the minimal nonce. + /// @param _rawMinNonce The value of the raw minimal nonce (equal to minNonce + deploymentNonce* 2**128). + /// @return deploymentNonce and minNonce. + function _splitRawNonce(uint256 _rawMinNonce) internal pure returns (uint256 deploymentNonce, uint256 minNonce) { + deploymentNonce = _rawMinNonce / DEPLOY_NONCE_MULTIPLIER; + minNonce = _rawMinNonce % DEPLOY_NONCE_MULTIPLIER; + } +} diff --git a/contracts/system-contracts/PubdataChunkPublisher.sol b/contracts/system-contracts/PubdataChunkPublisher.sol new file mode 100644 index 0000000..9402f05 --- /dev/null +++ b/contracts/system-contracts/PubdataChunkPublisher.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import {IPubdataChunkPublisher} from "./interfaces/IPubdataChunkPublisher.sol"; +import {SystemContractBase} from "./abstract/SystemContractBase.sol"; +import {L1_MESSENGER_CONTRACT, BLOB_SIZE_BYTES, MAX_NUMBER_OF_BLOBS, SystemLogKey} from "./Constants.sol"; +import {SystemContractHelper} from "./libraries/SystemContractHelper.sol"; +import {TooMuchPubdata} from "./SystemContractErrors.sol"; + +/** + * @author Matter Labs + * @custom:security-contact security@matterlabs.dev + * @notice Smart contract for chunking pubdata into the appropriate size for EIP-4844 blobs. + */ +contract PubdataChunkPublisher is IPubdataChunkPublisher, SystemContractBase { + /// @notice Chunks pubdata into pieces that can fit into blobs. + /// @param _pubdata The total l2 to l1 pubdata that will be sent via L1 blobs. + /// @dev Note: This is an early implementation, in the future we plan to support up to 16 blobs per l1 batch. + /// @dev We always publish 6 system logs even if our pubdata fits into a single blob. This makes processing logs on L1 easier. + function chunkAndPublishPubdata(bytes calldata _pubdata) external onlyCallFrom(address(L1_MESSENGER_CONTRACT)) { + if (_pubdata.length > BLOB_SIZE_BYTES * MAX_NUMBER_OF_BLOBS) { + revert TooMuchPubdata(BLOB_SIZE_BYTES * MAX_NUMBER_OF_BLOBS, _pubdata.length); + } + + bytes32[] memory blobHashes = new bytes32[](MAX_NUMBER_OF_BLOBS); + + // We allocate to the full size of MAX_NUMBER_OF_BLOBS * BLOB_SIZE_BYTES because we need to pad + // the data on the right with 0s if it doesn't take up the full blob + bytes memory totalBlobs = new bytes(BLOB_SIZE_BYTES * MAX_NUMBER_OF_BLOBS); + + assembly { + // The pointer to the allocated memory above. We skip 32 bytes to avoid overwriting the length. + let ptr := add(totalBlobs, 0x20) + calldatacopy(ptr, _pubdata.offset, _pubdata.length) + } + + for (uint256 i = 0; i < MAX_NUMBER_OF_BLOBS; ++i) { + uint256 start = BLOB_SIZE_BYTES * i; + + // We break if the pubdata isn't enough to cover all 6 blobs. On L1 it is expected that the hash + // will be bytes32(0) if a blob isn't going to be used. + if (start >= _pubdata.length) { + break; + } + + bytes32 blobHash; + assembly { + // The pointer to the allocated memory above skipping the length. + let ptr := add(totalBlobs, 0x20) + blobHash := keccak256(add(ptr, start), BLOB_SIZE_BYTES) + } + + blobHashes[i] = blobHash; + } + + for (uint8 i = 0; i < MAX_NUMBER_OF_BLOBS; ++i) { + SystemContractHelper.toL1( + true, + bytes32(uint256(SystemLogKey(i + uint256(SystemLogKey.BLOB_ONE_HASH_KEY)))), + blobHashes[i] + ); + } + } +} diff --git a/contracts/system-contracts/SystemContext.sol b/contracts/system-contracts/SystemContext.sol new file mode 100644 index 0000000..4763b41 --- /dev/null +++ b/contracts/system-contracts/SystemContext.sol @@ -0,0 +1,519 @@ +// SPDX-License-Identifier: MIT + +// solhint-disable reason-string, gas-custom-errors + +pragma solidity 0.8.24; + +import {ISystemContext} from "./interfaces/ISystemContext.sol"; +import {SystemContractBase} from "./abstract/SystemContractBase.sol"; +import {ISystemContextDeprecated} from "./interfaces/ISystemContextDeprecated.sol"; +import {SystemContractHelper} from "./libraries/SystemContractHelper.sol"; +import {BOOTLOADER_FORMAL_ADDRESS, SystemLogKey} from "./Constants.sol"; + +/** + * @author Matter Labs + * @custom:security-contact security@matterlabs.dev + * @notice Contract that stores some of the context variables, that may be either + * block-scoped, tx-scoped or system-wide. + */ +contract SystemContext is ISystemContext, ISystemContextDeprecated, SystemContractBase { + /// @notice The number of latest L2 blocks to store. + /// @dev EVM requires us to be able to query the hashes of previous 256 blocks. + /// We could either: + /// - Store the latest 256 hashes (and strictly rely that we do not accidentally override the hash of the block 256 blocks ago) + /// - Store the latest 257 blocks' hashes. + uint256 internal constant MINIBLOCK_HASHES_TO_STORE = 257; + + /// @notice The chainId of the network. It is set at the genesis. + uint256 public chainId; + + /// @notice The `tx.origin` in the current transaction. + /// @dev It is updated before each transaction by the bootloader + address public origin; + + /// @notice The `tx.gasPrice` in the current transaction. + /// @dev It is updated before each transaction by the bootloader + uint256 public gasPrice; + + /// @notice The current block's gasLimit. + /// @dev The same limit is used for both batches and L2 blocks. At this moment this limit is not explicitly + /// forced by the system, rather it is the responsibility of the operator to ensure that this value is never achieved. + uint256 public blockGasLimit = (1 << 50); + + /// @notice The `block.coinbase` in the current transaction. + /// @dev For the support of coinbase, we will use the bootloader formal address for now + address public coinbase = BOOTLOADER_FORMAL_ADDRESS; + + /// @notice Formal `block.difficulty` parameter. + uint256 public difficulty = 2.5e15; + + /// @notice The `block.basefee`. + /// @dev It is currently a constant. + uint256 public baseFee; + + /// @notice The number and the timestamp of the current L1 batch stored packed. + BlockInfo internal currentBatchInfo; + + /// @notice The hashes of batches. + /// @dev It stores batch hashes for all previous batches. + mapping(uint256 batchNumber => bytes32 batchHash) internal batchHashes; + + /// @notice The number and the timestamp of the current L2 block. + BlockInfo internal currentL2BlockInfo; + + /// @notice The rolling hash of the transactions in the current L2 block. + bytes32 internal currentL2BlockTxsRollingHash; + + /// @notice The hashes of L2 blocks. + /// @dev It stores block hashes for previous L2 blocks. Note, in order to make publishing the hashes + /// of the miniblocks cheaper, we only store the previous MINIBLOCK_HASHES_TO_STORE ones. Since whenever we need to publish a state + /// diff, a pair of is published and for cached keys only 8-byte id is used instead of 32 bytes. + /// By having this data in a cyclic array of MINIBLOCK_HASHES_TO_STORE blocks, we bring the costs down by 40% (i.e. 40 bytes per miniblock instead of 64 bytes). + /// @dev The hash of a miniblock with number N would be stored under slot N%MINIBLOCK_HASHES_TO_STORE. + /// @dev Hashes of the blocks older than the ones which are stored here can be calculated as _calculateLegacyL2BlockHash(blockNumber). + bytes32[MINIBLOCK_HASHES_TO_STORE] internal l2BlockHash; + + /// @notice To make migration to L2 blocks smoother, we introduce a temporary concept of virtual L2 blocks, the data + /// about which will be returned by the EVM-like methods: block.number/block.timestamp/blockhash. + /// - Their number will start from being equal to the number of the batch and it will increase until it reaches the L2 block number. + /// - Their timestamp is updated each time a new virtual block is created. + /// - Their hash is calculated as `keccak256(uint256(number))` + BlockInfo internal currentVirtualL2BlockInfo; + + /// @notice The information about the virtual blocks upgrade, which tracks when the migration to the L2 blocks has started and finished. + VirtualBlockUpgradeInfo internal virtualBlockUpgradeInfo; + + /// @notice Set the chainId origin. + /// @param _newChainId The chainId + function setChainId(uint256 _newChainId) external onlyCallFromForceDeployer { + chainId = _newChainId; + } + + /// @notice Number of current transaction in block. + uint16 public txNumberInBlock; + + /// @notice The current gas per pubdata byte + uint256 public gasPerPubdataByte; + + /// @notice The number of pubdata spent as of the start of the transaction + uint256 internal basePubdataSpent; + + /// @notice Set the current tx origin. + /// @param _newOrigin The new tx origin. + function setTxOrigin(address _newOrigin) external onlyCallFromBootloader { + origin = _newOrigin; + } + + /// @notice Set the the current gas price. + /// @param _gasPrice The new tx gasPrice. + function setGasPrice(uint256 _gasPrice) external onlyCallFromBootloader { + gasPrice = _gasPrice; + } + + /// @notice Sets the number of L2 gas that is needed to pay a single byte of pubdata. + /// @dev This value does not have any impact on the execution and purely serves as a way for users + /// to access the current gas price for the pubdata. + /// @param _gasPerPubdataByte The amount L2 gas that the operator charge the user for single byte of pubdata. + /// @param _basePubdataSpent The number of pubdata spent as of the start of the transaction. + function setPubdataInfo(uint256 _gasPerPubdataByte, uint256 _basePubdataSpent) external onlyCallFromBootloader { + basePubdataSpent = _basePubdataSpent; + gasPerPubdataByte = _gasPerPubdataByte; + } + + function getCurrentPubdataSpent() public view returns (uint256) { + uint256 pubdataPublished = SystemContractHelper.getZkSyncMeta().pubdataPublished; + return pubdataPublished > basePubdataSpent ? pubdataPublished - basePubdataSpent : 0; + } + + function getCurrentPubdataCost() external view returns (uint256) { + return gasPerPubdataByte * getCurrentPubdataSpent(); + } + + /// @notice The method that emulates `blockhash` opcode in EVM. + /// @dev Just like the blockhash in the EVM, it returns bytes32(0), + /// when queried about hashes that are older than 256 blocks ago. + /// @dev Since zksolc compiler calls this method to emulate `blockhash`, + /// its signature can not be changed to `getL2BlockHashEVM`. + /// @return hash The blockhash of the block with the given number. + function getBlockHashEVM(uint256 _block) external view returns (bytes32 hash) { + uint128 blockNumber = currentVirtualL2BlockInfo.number; + + VirtualBlockUpgradeInfo memory currentVirtualBlockUpgradeInfo = virtualBlockUpgradeInfo; + + // Due to virtual blocks upgrade, we'll have to use the following logic for retrieving the blockhash: + // 1. If the block number is out of the 256-block supported range, return 0. + // 2. If the block was created before the upgrade for the virtual blocks (i.e. there we used to use hashes of the batches), + // we return the hash of the batch. + // 3. If the block was created after the day when the virtual blocks have caught up with the L2 blocks, i.e. + // all the information which is returned for users should be for L2 blocks, we return the hash of the corresponding L2 block. + // 4. If the block queried is a virtual blocks, calculate it on the fly. + if (blockNumber <= _block || blockNumber - _block > 256) { + hash = bytes32(0); + } else if (_block < currentVirtualBlockUpgradeInfo.virtualBlockStartBatch) { + // Note, that we will get into this branch only for a brief moment of time, right after the upgrade + // for virtual blocks before 256 virtual blocks are produced. + hash = batchHashes[_block]; + } else if ( + _block >= currentVirtualBlockUpgradeInfo.virtualBlockFinishL2Block && + currentVirtualBlockUpgradeInfo.virtualBlockFinishL2Block > 0 + ) { + hash = _getLatest257L2blockHash(_block); + } else { + // Important: we do not want this number to ever collide with the L2 block hash (either new or old one) and so + // that's why the legacy L2 blocks' hashes are keccak256(abi.encodePacked(uint32(_block))), while these are equivalent to + // keccak256(abi.encodePacked(_block)) + hash = keccak256(abi.encode(_block)); + } + } + + /// @notice Returns the hash of the given batch. + /// @param _batchNumber The number of the batch. + /// @return hash The hash of the batch. + function getBatchHash(uint256 _batchNumber) external view returns (bytes32 hash) { + hash = batchHashes[_batchNumber]; + } + + /// @notice Returns the current batch's number and timestamp. + /// @return batchNumber and batchTimestamp tuple of the current batch's number and the current batch's timestamp + function getBatchNumberAndTimestamp() public view returns (uint128 batchNumber, uint128 batchTimestamp) { + BlockInfo memory batchInfo = currentBatchInfo; + batchNumber = batchInfo.number; + batchTimestamp = batchInfo.timestamp; + } + + /// @notice Returns the current block's number and timestamp. + /// @return blockNumber and blockTimestamp tuple of the current L2 block's number and the current block's timestamp + function getL2BlockNumberAndTimestamp() public view returns (uint128 blockNumber, uint128 blockTimestamp) { + BlockInfo memory blockInfo = currentL2BlockInfo; + blockNumber = blockInfo.number; + blockTimestamp = blockInfo.timestamp; + } + + /// @notice Returns the current L2 block's number. + /// @dev Since zksolc compiler calls this method to emulate `block.number`, + /// its signature can not be changed to `getL2BlockNumber`. + /// @return blockNumber The current L2 block's number. + function getBlockNumber() public view returns (uint128) { + return currentVirtualL2BlockInfo.number; + } + + /// @notice Returns the current L2 block's timestamp. + /// @dev Since zksolc compiler calls this method to emulate `block.timestamp`, + /// its signature can not be changed to `getL2BlockTimestamp`. + /// @return timestamp The current L2 block's timestamp. + function getBlockTimestamp() public view returns (uint128) { + return currentVirtualL2BlockInfo.timestamp; + } + + /// @notice Assuming that block is one of the last MINIBLOCK_HASHES_TO_STORE ones, returns its hash. + /// @param _block The number of the block. + /// @return hash The hash of the block. + function _getLatest257L2blockHash(uint256 _block) internal view returns (bytes32) { + return l2BlockHash[_block % MINIBLOCK_HASHES_TO_STORE]; + } + + /// @notice Assuming that the block is one of the last MINIBLOCK_HASHES_TO_STORE ones, sets its hash. + /// @param _block The number of the block. + /// @param _hash The hash of the block. + function _setL2BlockHash(uint256 _block, bytes32 _hash) internal { + l2BlockHash[_block % MINIBLOCK_HASHES_TO_STORE] = _hash; + } + + /// @notice Calculates the hash of an L2 block. + /// @param _blockNumber The number of the L2 block. + /// @param _blockTimestamp The timestamp of the L2 block. + /// @param _prevL2BlockHash The hash of the previous L2 block. + /// @param _blockTxsRollingHash The rolling hash of the transactions in the L2 block. + function _calculateL2BlockHash( + uint128 _blockNumber, + uint128 _blockTimestamp, + bytes32 _prevL2BlockHash, + bytes32 _blockTxsRollingHash + ) internal pure returns (bytes32) { + return keccak256(abi.encode(_blockNumber, _blockTimestamp, _prevL2BlockHash, _blockTxsRollingHash)); + } + + /// @notice Calculates the legacy block hash of L2 block, which were used before the upgrade where + /// the advanced block hashes were introduced. + /// @param _blockNumber The number of the L2 block. + function _calculateLegacyL2BlockHash(uint128 _blockNumber) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(uint32(_blockNumber))); + } + + /// @notice Performs the upgrade where we transition to the L2 blocks. + /// @param _l2BlockNumber The number of the new L2 block. + /// @param _expectedPrevL2BlockHash The expected hash of the previous L2 block. + /// @param _isFirstInBatch Whether this method is called for the first time in the batch. + function _upgradeL2Blocks(uint128 _l2BlockNumber, bytes32 _expectedPrevL2BlockHash, bool _isFirstInBatch) internal { + require(_isFirstInBatch, "Upgrade transaction must be first"); + + // This is how it will be commonly done in practice, but it will simplify some logic later + require(_l2BlockNumber > 0, "L2 block number is never expected to be zero"); + + unchecked { + bytes32 correctPrevBlockHash = _calculateLegacyL2BlockHash(_l2BlockNumber - 1); + require(correctPrevBlockHash == _expectedPrevL2BlockHash, "The previous L2 block hash is incorrect"); + + // Whenever we'll be queried about the hashes of the blocks before the upgrade, + // we'll use batches' hashes, so we don't need to store 256 previous hashes. + // However, we do need to store the last previous hash in order to be able to correctly calculate the + // hash of the new L2 block. + _setL2BlockHash(_l2BlockNumber - 1, correctPrevBlockHash); + } + } + + /// @notice Creates new virtual blocks, while ensuring they don't exceed the L2 block number. + /// @param _l2BlockNumber The number of the new L2 block. + /// @param _maxVirtualBlocksToCreate The maximum number of virtual blocks to create with this L2 block. + /// @param _newTimestamp The timestamp of the new L2 block, which is also the timestamp of the new virtual block. + function _setVirtualBlock( + uint128 _l2BlockNumber, + uint128 _maxVirtualBlocksToCreate, + uint128 _newTimestamp + ) internal { + if (virtualBlockUpgradeInfo.virtualBlockFinishL2Block != 0) { + // No need to to do anything about virtual blocks anymore + // All the info is the same as for L2 blocks. + currentVirtualL2BlockInfo = currentL2BlockInfo; + return; + } + + BlockInfo memory virtualBlockInfo = currentVirtualL2BlockInfo; + + if (currentVirtualL2BlockInfo.number == 0 && virtualBlockInfo.timestamp == 0) { + uint128 currentBatchNumber = currentBatchInfo.number; + + // The virtual block is set for the first time. We can count it as 1 creation of a virtual block. + // Note, that when setting the virtual block number we use the batch number to make a smoother upgrade from batch number to + // the L2 block number. + virtualBlockInfo.number = currentBatchNumber; + // Remembering the batch number on which the upgrade to the virtual blocks has been done. + virtualBlockUpgradeInfo.virtualBlockStartBatch = currentBatchNumber; + + require(_maxVirtualBlocksToCreate > 0, "Can't initialize the first virtual block"); + // solhint-disable-next-line gas-increment-by-one + _maxVirtualBlocksToCreate -= 1; + } else if (_maxVirtualBlocksToCreate == 0) { + // The virtual blocks have been already initialized, but the operator didn't ask to create + // any new virtual blocks. So we can just return. + return; + } + + virtualBlockInfo.number += _maxVirtualBlocksToCreate; + virtualBlockInfo.timestamp = _newTimestamp; + + // The virtual block number must never exceed the L2 block number. + // We do not use a `require` here, since the virtual blocks are a temporary solution to let the Solidity's `block.number` + // catch up with the L2 block number and so the situation where virtualBlockInfo.number starts getting larger + // than _l2BlockNumber is expected once virtual blocks have caught up the L2 blocks. + if (virtualBlockInfo.number >= _l2BlockNumber) { + virtualBlockUpgradeInfo.virtualBlockFinishL2Block = _l2BlockNumber; + virtualBlockInfo.number = _l2BlockNumber; + } + + currentVirtualL2BlockInfo = virtualBlockInfo; + } + + /// @notice Sets the current block number and timestamp of the L2 block. + /// @param _l2BlockNumber The number of the new L2 block. + /// @param _l2BlockTimestamp The timestamp of the new L2 block. + /// @param _prevL2BlockHash The hash of the previous L2 block. + function _setNewL2BlockData(uint128 _l2BlockNumber, uint128 _l2BlockTimestamp, bytes32 _prevL2BlockHash) internal { + // In the unsafe version we do not check that the block data is correct + currentL2BlockInfo = BlockInfo({number: _l2BlockNumber, timestamp: _l2BlockTimestamp}); + + // It is always assumed in production that _l2BlockNumber > 0 + _setL2BlockHash(_l2BlockNumber - 1, _prevL2BlockHash); + + // Resetting the rolling hash + currentL2BlockTxsRollingHash = bytes32(0); + } + + /// @notice Sets the current block number and timestamp of the L2 block. + /// @dev Called by the bootloader before each transaction. This is needed to ensure + /// that the data about the block is consistent with the sequencer. + /// @dev If the new block number is the same as the current one, we ensure that the block's data is + /// consistent with the one in the current block. + /// @dev If the new block number is greater than the current one by 1, + /// then we ensure that timestamp has increased. + /// @dev If the currently stored number is 0, we assume that it is the first upgrade transaction + /// and so we will fill up the old data. + /// @param _l2BlockNumber The number of the new L2 block. + /// @param _l2BlockTimestamp The timestamp of the new L2 block. + /// @param _expectedPrevL2BlockHash The expected hash of the previous L2 block. + /// @param _isFirstInBatch Whether this method is called for the first time in the batch. + /// @param _maxVirtualBlocksToCreate The maximum number of virtual block to create with this L2 block. + /// @dev It is a strict requirement that a new virtual block is created at the start of the batch. + /// @dev It is also enforced that the number of the current virtual L2 block can not exceed the number of the L2 block. + function setL2Block( + uint128 _l2BlockNumber, + uint128 _l2BlockTimestamp, + bytes32 _expectedPrevL2BlockHash, + bool _isFirstInBatch, + uint128 _maxVirtualBlocksToCreate + ) external onlyCallFromBootloader { + // We check that the timestamp of the L2 block is consistent with the timestamp of the batch. + if (_isFirstInBatch) { + uint128 currentBatchTimestamp = currentBatchInfo.timestamp; + require( + _l2BlockTimestamp >= currentBatchTimestamp, + "The timestamp of the L2 block must be greater than or equal to the timestamp of the current batch" + ); + require(_maxVirtualBlocksToCreate > 0, "There must be a virtual block created at the start of the batch"); + } + + (uint128 currentL2BlockNumber, uint128 currentL2BlockTimestamp) = getL2BlockNumberAndTimestamp(); + + if (currentL2BlockNumber == 0 && currentL2BlockTimestamp == 0) { + // Since currentL2BlockNumber and currentL2BlockTimestamp are zero it means that it is + // the first ever batch with L2 blocks, so we need to initialize those. + _upgradeL2Blocks(_l2BlockNumber, _expectedPrevL2BlockHash, _isFirstInBatch); + + _setNewL2BlockData(_l2BlockNumber, _l2BlockTimestamp, _expectedPrevL2BlockHash); + } else if (currentL2BlockNumber == _l2BlockNumber) { + require(!_isFirstInBatch, "Can not reuse L2 block number from the previous batch"); + require(currentL2BlockTimestamp == _l2BlockTimestamp, "The timestamp of the same L2 block must be same"); + require( + _expectedPrevL2BlockHash == _getLatest257L2blockHash(_l2BlockNumber - 1), + "The previous hash of the same L2 block must be same" + ); + require(_maxVirtualBlocksToCreate == 0, "Can not create virtual blocks in the middle of the miniblock"); + } else if (currentL2BlockNumber + 1 == _l2BlockNumber) { + // From the checks in _upgradeL2Blocks it is known that currentL2BlockNumber can not be 0 + bytes32 prevL2BlockHash = _getLatest257L2blockHash(currentL2BlockNumber - 1); + + bytes32 pendingL2BlockHash = _calculateL2BlockHash( + currentL2BlockNumber, + currentL2BlockTimestamp, + prevL2BlockHash, + currentL2BlockTxsRollingHash + ); + + require(_expectedPrevL2BlockHash == pendingL2BlockHash, "The current L2 block hash is incorrect"); + require( + _l2BlockTimestamp > currentL2BlockTimestamp, + "The timestamp of the new L2 block must be greater than the timestamp of the previous L2 block" + ); + + // Since the new block is created, we'll clear out the rolling hash + _setNewL2BlockData(_l2BlockNumber, _l2BlockTimestamp, _expectedPrevL2BlockHash); + } else { + revert("Invalid new L2 block number"); + } + + _setVirtualBlock(_l2BlockNumber, _maxVirtualBlocksToCreate, _l2BlockTimestamp); + } + + /// @notice Appends the transaction hash to the rolling hash of the current L2 block. + /// @param _txHash The hash of the transaction. + function appendTransactionToCurrentL2Block(bytes32 _txHash) external onlyCallFromBootloader { + currentL2BlockTxsRollingHash = keccak256(abi.encode(currentL2BlockTxsRollingHash, _txHash)); + } + + /// @notice Publishes L2->L1 logs needed to verify the validity of this batch on L1. + /// @dev Should be called at the end of the current batch. + function publishTimestampDataToL1() external onlyCallFromBootloader { + (uint128 currentBatchNumber, uint128 currentBatchTimestamp) = getBatchNumberAndTimestamp(); + (, uint128 currentL2BlockTimestamp) = getL2BlockNumberAndTimestamp(); + + // The structure of the "setNewBatch" implies that currentBatchNumber > 0, but we still double check it + require(currentBatchNumber > 0, "The current batch number must be greater than 0"); + + // In order to spend less pubdata, the packed version is published + uint256 packedTimestamps = (uint256(currentBatchTimestamp) << 128) | currentL2BlockTimestamp; + + SystemContractHelper.toL1( + false, + bytes32(uint256(SystemLogKey.PACKED_BATCH_AND_L2_BLOCK_TIMESTAMP_KEY)), + bytes32(packedTimestamps) + ); + } + + /// @notice Ensures that the timestamp of the batch is greater than the timestamp of the last L2 block. + /// @param _newTimestamp The timestamp of the new batch. + function _ensureBatchConsistentWithL2Block(uint128 _newTimestamp) internal view { + uint128 currentBlockTimestamp = currentL2BlockInfo.timestamp; + require( + _newTimestamp > currentBlockTimestamp, + "The timestamp of the batch must be greater than the timestamp of the previous block" + ); + } + + /// @notice Increments the current batch number and sets the new timestamp + /// @dev Called by the bootloader at the start of the batch. + /// @param _prevBatchHash The hash of the previous batch. + /// @param _newTimestamp The timestamp of the new batch. + /// @param _expectedNewNumber The new batch's number. + /// @param _baseFee The new batch's base fee + /// @dev While _expectedNewNumber can be derived as prevBatchNumber + 1, we still + /// manually supply it here for consistency checks. + /// @dev The correctness of the _prevBatchHash and _newTimestamp should be enforced on L1. + function setNewBatch( + bytes32 _prevBatchHash, + uint128 _newTimestamp, + uint128 _expectedNewNumber, + uint256 _baseFee + ) external onlyCallFromBootloader { + (uint128 previousBatchNumber, uint128 previousBatchTimestamp) = getBatchNumberAndTimestamp(); + require(_newTimestamp > previousBatchTimestamp, "Timestamps should be incremental"); + require(previousBatchNumber + 1 == _expectedNewNumber, "The provided batch number is not correct"); + + _ensureBatchConsistentWithL2Block(_newTimestamp); + + batchHashes[previousBatchNumber] = _prevBatchHash; + + // Setting new block number and timestamp + BlockInfo memory newBlockInfo = BlockInfo({number: previousBatchNumber + 1, timestamp: _newTimestamp}); + + currentBatchInfo = newBlockInfo; + + baseFee = _baseFee; + + // The correctness of this block hash: + SystemContractHelper.toL1(false, bytes32(uint256(SystemLogKey.PREV_BATCH_HASH_KEY)), _prevBatchHash); + } + + /// @notice A testing method that manually sets the current blocks' number and timestamp. + /// @dev Should be used only for testing / ethCalls and should never be used in production. + function unsafeOverrideBatch( + uint256 _newTimestamp, + uint256 _number, + uint256 _baseFee + ) external onlyCallFromBootloader { + BlockInfo memory newBlockInfo = BlockInfo({number: uint128(_number), timestamp: uint128(_newTimestamp)}); + currentBatchInfo = newBlockInfo; + + baseFee = _baseFee; + } + + function incrementTxNumberInBatch() external onlyCallFromBootloader { + ++txNumberInBlock; + } + + function resetTxNumberInBatch() external onlyCallFromBootloader { + txNumberInBlock = 0; + } + + /*////////////////////////////////////////////////////////////// + DEPRECATED METHODS + //////////////////////////////////////////////////////////////*/ + + /// @notice Returns the current batch's number and timestamp. + /// @dev Deprecated in favor of getBatchNumberAndTimestamp. + function currentBlockInfo() external view returns (uint256 blockInfo) { + (uint128 blockNumber, uint128 blockTimestamp) = getBatchNumberAndTimestamp(); + blockInfo = (uint256(blockNumber) << 128) | uint256(blockTimestamp); + } + + /// @notice Returns the current batch's number and timestamp. + /// @dev Deprecated in favor of getBatchNumberAndTimestamp. + function getBlockNumberAndTimestamp() external view returns (uint256 blockNumber, uint256 blockTimestamp) { + (blockNumber, blockTimestamp) = getBatchNumberAndTimestamp(); + } + + /// @notice Returns the hash of the given batch. + /// @dev Deprecated in favor of getBatchHash. + function blockHash(uint256 _blockNumber) external view returns (bytes32 hash) { + hash = batchHashes[_blockNumber]; + } +} diff --git a/contracts/system-contracts/SystemContractErrors.sol b/contracts/system-contracts/SystemContractErrors.sol new file mode 100644 index 0000000..b5dfc82 --- /dev/null +++ b/contracts/system-contracts/SystemContractErrors.sol @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.20; + +// 0x86bb51b8 +error AddressHasNoCode(address); +// 0xefce78c7 +error CallerMustBeBootloader(); +// 0xb7549616 +error CallerMustBeForceDeployer(); +// 0x9eedbd2b +error CallerMustBeSystemContract(); +// 0x4f951510 +error CompressionValueAddError(uint256 expected, uint256 actual); +// 0x1e6aff87 +error CompressionValueTransformError(uint256 expected, uint256 actual); +// 0xc2ea251e +error CompressionValueSubError(uint256 expected, uint256 actual); +// 0x849acb7f +error CompressorInitialWritesProcessedNotEqual(uint256 expected, uint256 actual); +// 0x61a6a4b3 +error CompressorEnumIndexNotEqual(uint256 expected, uint256 actual); +// 0x9be48d8d +error DerivedKeyNotEqualToCompressedValue(bytes32 expected, bytes32 provided); +// 0xe223db5e +error DictionaryDividedByEightNotGreaterThanEncodedDividedByTwo(); +// 0x1c25715b +error EmptyBytes32(); +// 0x92bf3cf8 +error EmptyVirtualBlocks(); +// 0xc06d5cb2 +error EncodedAndRealBytecodeChunkNotEqual(uint64 expected, uint64 provided); +// 0x2bfbfc11 +error EncodedLengthNotFourTimesSmallerThanOriginal(); +// 0xe95a1fbe +error FailedToChargeGas(); +// 0x1f70c58f +error FailedToPayOperator(); +// 0x9d5da395 +error FirstL2BlockInitializationError(); +// 0x9e4a3c8a +error HashIsNonZero(bytes32); +// 0x86302004 +error HashMismatch(bytes32 expected, uint256 actual); +// 0x4e23d035 +error IndexOutOfBounds(); +// 0x122e73e9 +error IndexSizeError(); +// 0x03eb8b54 +error InsufficientFunds(uint256 required, uint256 actual); +// 0x1c26714c +error InsufficientGas(); +// 0xae962d4e +error InvalidCall(); +// 0x6a84bc39 +error InvalidCodeHash(CodeHashReason); +// 0xb4fa3fb3 +error InvalidInput(); +// 0x60b85677 +error InvalidNonceOrderingChange(); +// 0x90f049c9 +error InvalidSig(SigField, uint256); +// 0xf4a271b5 +error Keccak256InvalidReturnData(); +// 0xd2906dd9 +error L2BlockMustBeGreaterThanZero(); +// 0x43e266b0 +error MalformedBytecode(BytecodeError); +// 0xe90aded4 +error NonceAlreadyUsed(address account, uint256 nonce); +// 0x45ac24a6 +error NonceIncreaseError(uint256 max, uint256 proposed); +// 0x13595475 +error NonceJumpError(); +// 0x1f2f8478 +error NonceNotUsed(address account, uint256 nonce); +// 0x760a1568 +error NonEmptyAccount(); +// 0x536ec84b +error NonEmptyMsgValue(); +// 0xd018e08e +error NonIncreasingTimestamp(); +// 0x50df6bc3 +error NotAllowedToDeployInKernelSpace(); +// 0x35278d12 +error Overflow(); +// 0x7f7b0cf7 +error ReconstructionMismatch(PubdataField, bytes32 expected, bytes32 actual); +// 0x3adb5f1d +error ShaInvalidReturnData(); +// 0xbd8665e2 +error StateDiffLengthMismatch(); +// 0x71c3da01 +error SystemCallFlagRequired(); +// 0xe0456dfe +error TooMuchPubdata(uint256 limit, uint256 supplied); +// 0x8e4a23d6 +error Unauthorized(address); +// 0x3e5efef9 +error UnknownCodeHash(bytes32); +// 0x9ba6061b +error UnsupportedOperation(); +// 0xff15b069 +error UnsupportedPaymasterFlow(); +// 0x17a84415 +error UnsupportedTxType(uint256); +// 0x5708aead +error UpgradeMustBeFirstTxn(); +// 0x626ade30 +error ValueMismatch(uint256 expected, uint256 actual); +// 0x460b9939 +error ValuesNotEqual(uint256 expected, uint256 actual); +// 0x6818f3f9 +error ZeroNonceError(); + +enum CodeHashReason { + NotContractOnConstructor, + NotConstructedContract +} + +enum SigField { + Length, + V, + S +} + +enum PubdataField { + NumberOfLogs, + LogsHash, + MsgHash, + Bytecode, + StateDiffCompressionVersion, + ExtraData +} + +enum BytecodeError { + Version, + NumberOfWords, + Length, + WordsMustBeOdd, + DictionaryLength +} diff --git a/contracts/system-contracts/abstract/SystemContractBase.sol b/contracts/system-contracts/abstract/SystemContractBase.sol new file mode 100644 index 0000000..89966a5 --- /dev/null +++ b/contracts/system-contracts/abstract/SystemContractBase.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.20; + +import {SystemContractHelper} from "../libraries/SystemContractHelper.sol"; +import {BOOTLOADER_FORMAL_ADDRESS, FORCE_DEPLOYER} from "../Constants.sol"; +import {SystemCallFlagRequired, Unauthorized, CallerMustBeSystemContract, CallerMustBeBootloader, CallerMustBeForceDeployer} from "../SystemContractErrors.sol"; + +/** + * @author Matter Labs + * @custom:security-contact security@matterlabs.dev + * @notice An abstract contract that is used to reuse modifiers across the system contracts. + * @dev Solidity does not allow exporting modifiers via libraries, so + * the only way to do reuse modifiers is to have a base contract + * @dev Never add storage variables into this contract as some + * system contracts rely on this abstract contract as on interface! + */ +abstract contract SystemContractBase { + /// @notice Modifier that makes sure that the method + /// can only be called via a system call. + modifier onlySystemCall() { + if (!SystemContractHelper.isSystemCall() && !SystemContractHelper.isSystemContract(msg.sender)) { + revert SystemCallFlagRequired(); + } + _; + } + + /// @notice Modifier that makes sure that the method + /// can only be called from a system contract. + modifier onlyCallFromSystemContract() { + if (!SystemContractHelper.isSystemContract(msg.sender)) { + revert CallerMustBeSystemContract(); + } + _; + } + + /// @notice Modifier that makes sure that the method + /// can only be called from a special given address. + modifier onlyCallFrom(address caller) { + if (msg.sender != caller) { + revert Unauthorized(msg.sender); + } + _; + } + + /// @notice Modifier that makes sure that the method + /// can only be called from the bootloader. + modifier onlyCallFromBootloader() { + if (msg.sender != BOOTLOADER_FORMAL_ADDRESS) { + revert CallerMustBeBootloader(); + } + _; + } + + /// @notice Modifier that makes sure that the method + /// can only be called from the L1 force deployer. + modifier onlyCallFromForceDeployer() { + if (msg.sender != FORCE_DEPLOYER) { + revert CallerMustBeForceDeployer(); + } + _; + } +} diff --git a/contracts/system-contracts/interfaces/IAccount.sol b/contracts/system-contracts/interfaces/IAccount.sol new file mode 100644 index 0000000..cebe91d --- /dev/null +++ b/contracts/system-contracts/interfaces/IAccount.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.20; + +import {Transaction} from "../libraries/TransactionHelper.sol"; + +bytes4 constant ACCOUNT_VALIDATION_SUCCESS_MAGIC = IAccount.validateTransaction.selector; + +interface IAccount { + /// @notice Called by the bootloader to validate that an account agrees to process the transaction + /// (and potentially pay for it). + /// @param _txHash The hash of the transaction to be used in the explorer + /// @param _suggestedSignedHash The hash of the transaction is signed by EOAs + /// @param _transaction The transaction itself + /// @return magic The magic value that should be equal to the signature of this function + /// if the user agrees to proceed with the transaction. + /// @dev The developer should strive to preserve as many steps as possible both for valid + /// and invalid transactions as this very method is also used during the gas fee estimation + /// (without some of the necessary data, e.g. signature). + function validateTransaction( + bytes32 _txHash, + bytes32 _suggestedSignedHash, + Transaction calldata _transaction + ) external payable returns (bytes4 magic); + + function executeTransaction( + bytes32 _txHash, + bytes32 _suggestedSignedHash, + Transaction calldata _transaction + ) external payable; + + // There is no point in providing possible signed hash in the `executeTransactionFromOutside` method, + // since it typically should not be trusted. + function executeTransactionFromOutside(Transaction calldata _transaction) external payable; + + function payForTransaction( + bytes32 _txHash, + bytes32 _suggestedSignedHash, + Transaction calldata _transaction + ) external payable; + + function prepareForPaymaster( + bytes32 _txHash, + bytes32 _possibleSignedHash, + Transaction calldata _transaction + ) external payable; +} diff --git a/contracts/system-contracts/interfaces/IAccountCodeStorage.sol b/contracts/system-contracts/interfaces/IAccountCodeStorage.sol new file mode 100644 index 0000000..5183e77 --- /dev/null +++ b/contracts/system-contracts/interfaces/IAccountCodeStorage.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.20; + +interface IAccountCodeStorage { + function storeAccountConstructingCodeHash(address _address, bytes32 _hash) external; + + function storeAccountConstructedCodeHash(address _address, bytes32 _hash) external; + + function markAccountCodeHashAsConstructed(address _address) external; + + function getRawCodeHash(address _address) external view returns (bytes32 codeHash); + + function getCodeHash(uint256 _input) external view returns (bytes32 codeHash); + + function getCodeSize(uint256 _input) external view returns (uint256 codeSize); +} diff --git a/contracts/system-contracts/interfaces/IBaseToken.sol b/contracts/system-contracts/interfaces/IBaseToken.sol new file mode 100644 index 0000000..fc32c7b --- /dev/null +++ b/contracts/system-contracts/interfaces/IBaseToken.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.20; + +interface IBaseToken { + function balanceOf(uint256) external view returns (uint256); + + function transferFromTo(address _from, address _to, uint256 _amount) external; + + function totalSupply() external view returns (uint256); + + function name() external pure returns (string memory); + + function symbol() external pure returns (string memory); + + function decimals() external pure returns (uint8); + + function mint(address _account, uint256 _amount) external; + + function withdraw(address _l1Receiver) external payable; + + function withdrawWithMessage(address _l1Receiver, bytes calldata _additionalData) external payable; + + event Mint(address indexed account, uint256 amount); + + event Transfer(address indexed from, address indexed to, uint256 value); + + event Withdrawal(address indexed _l2Sender, address indexed _l1Receiver, uint256 _amount); + + event WithdrawalWithMessage( + address indexed _l2Sender, + address indexed _l1Receiver, + uint256 _amount, + bytes _additionalData + ); +} diff --git a/contracts/system-contracts/interfaces/IBootloaderUtilities.sol b/contracts/system-contracts/interfaces/IBootloaderUtilities.sol new file mode 100644 index 0000000..e900bfb --- /dev/null +++ b/contracts/system-contracts/interfaces/IBootloaderUtilities.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.20; + +import {Transaction} from "../libraries/TransactionHelper.sol"; + +interface IBootloaderUtilities { + function getTransactionHashes( + Transaction calldata _transaction + ) external view returns (bytes32 txHash, bytes32 signedTxHash); +} diff --git a/contracts/system-contracts/interfaces/IComplexUpgrader.sol b/contracts/system-contracts/interfaces/IComplexUpgrader.sol new file mode 100644 index 0000000..3b14684 --- /dev/null +++ b/contracts/system-contracts/interfaces/IComplexUpgrader.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.20; + +/** + * @author Matter Labs + * @custom:security-contact security@matterlabs.dev + * @notice The interface for the ComplexUpgrader contract. + */ +interface IComplexUpgrader { + function upgrade(address _delegateTo, bytes calldata _calldata) external payable; +} diff --git a/contracts/system-contracts/interfaces/ICompressor.sol b/contracts/system-contracts/interfaces/ICompressor.sol new file mode 100644 index 0000000..854aa79 --- /dev/null +++ b/contracts/system-contracts/interfaces/ICompressor.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.20; + +// The bitmask by applying which to the compressed state diff metadata we retrieve its operation. +uint8 constant OPERATION_BITMASK = 7; +// The number of bits shifting the compressed state diff metadata by which we retrieve its length. +uint8 constant LENGTH_BITS_OFFSET = 3; +// The maximal length in bytes that an enumeration index can have. +uint8 constant MAX_ENUMERATION_INDEX_SIZE = 8; + +/** + * @author Matter Labs + * @custom:security-contact security@matterlabs.dev + * @notice The interface for the Compressor contract, responsible for verifying the correctness of + * the compression of the state diffs and bytecodes. + */ +interface ICompressor { + function publishCompressedBytecode( + bytes calldata _bytecode, + bytes calldata _rawCompressedData + ) external returns (bytes32 bytecodeHash); + + function verifyCompressedStateDiffs( + uint256 _numberOfStateDiffs, + uint256 _enumerationIndexSize, + bytes calldata _stateDiffs, + bytes calldata _compressedStateDiffs + ) external returns (bytes32 stateDiffHash); +} diff --git a/contracts/system-contracts/interfaces/IContractDeployer.sol b/contracts/system-contracts/interfaces/IContractDeployer.sol new file mode 100644 index 0000000..6e0bac3 --- /dev/null +++ b/contracts/system-contracts/interfaces/IContractDeployer.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.20; + +interface IContractDeployer { + /// @notice Defines the version of the account abstraction protocol + /// that a contract claims to follow. + /// - `None` means that the account is just a contract and it should never be interacted + /// with as a custom account + /// - `Version1` means that the account follows the first version of the account abstraction protocol + enum AccountAbstractionVersion { + None, + Version1 + } + + /// @notice Defines the nonce ordering used by the account + /// - `Sequential` means that it is expected that the nonces are monotonic and increment by 1 + /// at a time (the same as EOAs). + /// - `Arbitrary` means that the nonces for the accounts can be arbitrary. The operator + /// should serve the transactions from such an account on a first-come-first-serve basis. + /// @dev This ordering is more of a suggestion to the operator on how the AA expects its transactions + /// to be processed and is not considered as a system invariant. + enum AccountNonceOrdering { + Sequential, + Arbitrary + } + + struct AccountInfo { + AccountAbstractionVersion supportedAAVersion; + AccountNonceOrdering nonceOrdering; + } + + event ContractDeployed( + address indexed deployerAddress, + bytes32 indexed bytecodeHash, + address indexed contractAddress + ); + + event AccountNonceOrderingUpdated(address indexed accountAddress, AccountNonceOrdering nonceOrdering); + + event AccountVersionUpdated(address indexed accountAddress, AccountAbstractionVersion aaVersion); + + function getNewAddressCreate2( + address _sender, + bytes32 _bytecodeHash, + bytes32 _salt, + bytes calldata _input + ) external view returns (address newAddress); + + function getNewAddressCreate(address _sender, uint256 _senderNonce) external pure returns (address newAddress); + + function create2( + bytes32 _salt, + bytes32 _bytecodeHash, + bytes calldata _input + ) external payable returns (address newAddress); + + function create2Account( + bytes32 _salt, + bytes32 _bytecodeHash, + bytes calldata _input, + AccountAbstractionVersion _aaVersion + ) external payable returns (address newAddress); + + /// @dev While the `_salt` parameter is not used anywhere here, + /// it is still needed for consistency between `create` and + /// `create2` functions (required by the compiler). + function create( + bytes32 _salt, + bytes32 _bytecodeHash, + bytes calldata _input + ) external payable returns (address newAddress); + + /// @dev While `_salt` is never used here, we leave it here as a parameter + /// for the consistency with the `create` function. + function createAccount( + bytes32 _salt, + bytes32 _bytecodeHash, + bytes calldata _input, + AccountAbstractionVersion _aaVersion + ) external payable returns (address newAddress); + + /// @notice Returns the information about a certain AA. + function getAccountInfo(address _address) external view returns (AccountInfo memory info); + + /// @notice Can be called by an account to update its account version + function updateAccountVersion(AccountAbstractionVersion _version) external; + + /// @notice Can be called by an account to update its nonce ordering + function updateNonceOrdering(AccountNonceOrdering _nonceOrdering) external; +} diff --git a/contracts/system-contracts/interfaces/IImmutableSimulator.sol b/contracts/system-contracts/interfaces/IImmutableSimulator.sol new file mode 100644 index 0000000..8400538 --- /dev/null +++ b/contracts/system-contracts/interfaces/IImmutableSimulator.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.20; + +struct ImmutableData { + uint256 index; + bytes32 value; +} + +interface IImmutableSimulator { + function getImmutable(address _dest, uint256 _index) external view returns (bytes32); + + function setImmutables(address _dest, ImmutableData[] calldata _immutables) external; +} diff --git a/contracts/system-contracts/interfaces/IKnownCodesStorage.sol b/contracts/system-contracts/interfaces/IKnownCodesStorage.sol new file mode 100644 index 0000000..551cfb0 --- /dev/null +++ b/contracts/system-contracts/interfaces/IKnownCodesStorage.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.20; + +/** + * @author Matter Labs + * @custom:security-contact security@matterlabs.dev + * @notice The interface for the KnownCodesStorage contract, which is responsible + * for storing the hashes of the bytecodes that have been published to the network. + */ +interface IKnownCodesStorage { + event MarkedAsKnown(bytes32 indexed bytecodeHash, bool indexed sendBytecodeToL1); + + function markFactoryDeps(bool _shouldSendToL1, bytes32[] calldata _hashes) external; + + function markBytecodeAsPublished(bytes32 _bytecodeHash) external; + + function getMarker(bytes32 _hash) external view returns (uint256); +} diff --git a/contracts/system-contracts/interfaces/IL1Messenger.sol b/contracts/system-contracts/interfaces/IL1Messenger.sol new file mode 100644 index 0000000..88e2c81 --- /dev/null +++ b/contracts/system-contracts/interfaces/IL1Messenger.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.20; + +/// @dev The log passed from L2 +/// @param l2ShardId The shard identifier, 0 - rollup, 1 - porter. All other values are not used but are reserved for the future +/// @param isService A boolean flag that is part of the log along with `key`, `value`, and `sender` address. +/// This field is required formally but does not have any special meaning. +/// @param txNumberInBlock The L2 transaction number in a block, in which the log was sent +/// @param sender The L2 address which sent the log +/// @param key The 32 bytes of information that was sent in the log +/// @param value The 32 bytes of information that was sent in the log +// Both `key` and `value` are arbitrary 32-bytes selected by the log sender +struct L2ToL1Log { + uint8 l2ShardId; + bool isService; + uint16 txNumberInBlock; + address sender; + bytes32 key; + bytes32 value; +} + +/// @dev Bytes in raw L2 to L1 log +/// @dev Equal to the bytes size of the tuple - (uint8 ShardId, bool isService, uint16 txNumberInBlock, address sender, bytes32 key, bytes32 value) +uint256 constant L2_TO_L1_LOG_SERIALIZE_SIZE = 88; + +/// @dev The value of default leaf hash for L2 to L1 logs Merkle tree +/// @dev An incomplete fixed-size tree is filled with this value to be a full binary tree +/// @dev Actually equal to the `keccak256(new bytes(L2_TO_L1_LOG_SERIALIZE_SIZE))` +bytes32 constant L2_L1_LOGS_TREE_DEFAULT_LEAF_HASH = 0x72abee45b59e344af8a6e520241c4744aff26ed411f4c4b00f8af09adada43ba; + +/// @dev The current version of state diff compression being used. +uint256 constant STATE_DIFF_COMPRESSION_VERSION_NUMBER = 1; + +/** + * @author Matter Labs + * @custom:security-contact security@matterlabs.dev + * @notice The interface of the L1 Messenger contract, responsible for sending messages to L1. + */ +interface IL1Messenger { + // Possibly in the future we will be able to track the messages sent to L1 with + // some hooks in the VM. For now, it is much easier to track them with L2 events. + event L1MessageSent(address indexed _sender, bytes32 indexed _hash, bytes _message); + + event L2ToL1LogSent(L2ToL1Log _l2log); + + event BytecodeL1PublicationRequested(bytes32 _bytecodeHash); + + function sendToL1(bytes memory _message) external returns (bytes32); + + function sendL2ToL1Log(bool _isService, bytes32 _key, bytes32 _value) external returns (uint256 logIdInMerkleTree); + + // This function is expected to be called only by the KnownCodesStorage system contract + function requestBytecodeL1Publication(bytes32 _bytecodeHash) external; +} diff --git a/contracts/system-contracts/interfaces/IL2StandardToken.sol b/contracts/system-contracts/interfaces/IL2StandardToken.sol new file mode 100644 index 0000000..d67a3ea --- /dev/null +++ b/contracts/system-contracts/interfaces/IL2StandardToken.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.20; + +interface IL2StandardToken { + event BridgeMint(address indexed _account, uint256 _amount); + + event BridgeBurn(address indexed _account, uint256 _amount); + + function bridgeMint(address _account, uint256 _amount) external; + + function bridgeBurn(address _account, uint256 _amount) external; + + function l1Address() external view returns (address); + + function l2Bridge() external view returns (address); +} diff --git a/contracts/system-contracts/interfaces/IMailbox.sol b/contracts/system-contracts/interfaces/IMailbox.sol new file mode 100644 index 0000000..a9dcdad --- /dev/null +++ b/contracts/system-contracts/interfaces/IMailbox.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.20; + +interface IMailbox { + function finalizeEthWithdrawal( + uint256 _l2BatchNumber, + uint256 _l2MessageIndex, + uint16 _l2TxNumberInBlock, + bytes calldata _message, + bytes32[] calldata _merkleProof + ) external; +} diff --git a/contracts/system-contracts/interfaces/INonceHolder.sol b/contracts/system-contracts/interfaces/INonceHolder.sol new file mode 100644 index 0000000..ce3b027 --- /dev/null +++ b/contracts/system-contracts/interfaces/INonceHolder.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.20; + +/** + * @author Matter Labs + * @dev Interface of the nonce holder contract -- a contract used by the system to ensure + * that there is always a unique identifier for a transaction with a particular account (we call it nonce). + * In other words, the pair of (address, nonce) should always be unique. + * @dev Custom accounts should use methods of this contract to store nonces or other possible unique identifiers + * for the transaction. + */ +interface INonceHolder { + event ValueSetUnderNonce(address indexed accountAddress, uint256 indexed key, uint256 value); + + /// @dev Returns the current minimal nonce for account. + function getMinNonce(address _address) external view returns (uint256); + + /// @dev Returns the raw version of the current minimal nonce + /// (equal to minNonce + 2^128 * deployment nonce). + function getRawNonce(address _address) external view returns (uint256); + + /// @dev Increases the minimal nonce for the msg.sender. + function increaseMinNonce(uint256 _value) external returns (uint256); + + /// @dev Sets the nonce value `key` as used. + function setValueUnderNonce(uint256 _key, uint256 _value) external; + + /// @dev Gets the value stored inside a custom nonce. + function getValueUnderNonce(uint256 _key) external view returns (uint256); + + /// @dev A convenience method to increment the minimal nonce if it is equal + /// to the `_expectedNonce`. + function incrementMinNonceIfEquals(uint256 _expectedNonce) external; + + /// @dev Returns the deployment nonce for the accounts used for CREATE opcode. + function getDeploymentNonce(address _address) external view returns (uint256); + + /// @dev Increments the deployment nonce for the account and returns the previous one. + function incrementDeploymentNonce(address _address) external returns (uint256); + + /// @dev Determines whether a certain nonce has been already used for an account. + function validateNonceUsage(address _address, uint256 _key, bool _shouldBeUsed) external view; + + /// @dev Returns whether a nonce has been used for an account. + function isNonceUsed(address _address, uint256 _nonce) external view returns (bool); +} diff --git a/contracts/system-contracts/interfaces/IPaymaster.sol b/contracts/system-contracts/interfaces/IPaymaster.sol new file mode 100644 index 0000000..1c8af5b --- /dev/null +++ b/contracts/system-contracts/interfaces/IPaymaster.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.20; + +import {Transaction} from "../libraries/TransactionHelper.sol"; + +enum ExecutionResult { + Revert, + Success +} + +bytes4 constant PAYMASTER_VALIDATION_SUCCESS_MAGIC = IPaymaster.validateAndPayForPaymasterTransaction.selector; + +interface IPaymaster { + /// @dev Called by the bootloader to verify that the paymaster agrees to pay for the + /// fee for the transaction. This transaction should also send the necessary amount of funds onto the bootloader + /// address. + /// @param _txHash The hash of the transaction + /// @param _suggestedSignedHash The hash of the transaction that is signed by an EOA + /// @param _transaction The transaction itself. + /// @return magic The value that should be equal to the signature of the validateAndPayForPaymasterTransaction + /// if the paymaster agrees to pay for the transaction. + /// @return context The "context" of the transaction: an array of bytes of length at most 1024 bytes, which will be + /// passed to the `postTransaction` method of the account. + /// @dev The developer should strive to preserve as many steps as possible both for valid + /// and invalid transactions as this very method is also used during the gas fee estimation + /// (without some of the necessary data, e.g. signature). + function validateAndPayForPaymasterTransaction( + bytes32 _txHash, + bytes32 _suggestedSignedHash, + Transaction calldata _transaction + ) external payable returns (bytes4 magic, bytes memory context); + + /// @dev Called by the bootloader after the execution of the transaction. Please note that + /// there is no guarantee that this method will be called at all. Unlike the original EIP4337, + /// this method won't be called if the transaction execution results in out-of-gas. + /// @param _context, the context of the execution, returned by the "validateAndPayForPaymasterTransaction" method. + /// @param _transaction, the users' transaction. + /// @param _txResult, the result of the transaction execution (success or failure). + /// @param _maxRefundedGas, the upper bound on the amount of gas that could be refunded to the paymaster. + /// @dev The exact amount refunded depends on the gas spent by the "postOp" itself and so the developers should + /// take that into account. + function postTransaction( + bytes calldata _context, + Transaction calldata _transaction, + bytes32 _txHash, + bytes32 _suggestedSignedHash, + ExecutionResult _txResult, + uint256 _maxRefundedGas + ) external payable; +} diff --git a/contracts/system-contracts/interfaces/IPaymasterFlow.sol b/contracts/system-contracts/interfaces/IPaymasterFlow.sol new file mode 100644 index 0000000..4c9683f --- /dev/null +++ b/contracts/system-contracts/interfaces/IPaymasterFlow.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.20; + +/** + * @author Matter Labs + * @dev The interface that is used for encoding/decoding of + * different types of paymaster flows. + * @notice This is NOT an interface to be implemented + * by contracts. It is just used for encoding. + */ +interface IPaymasterFlow { + function general(bytes calldata input) external; + + function approvalBased(address _token, uint256 _minAllowance, bytes calldata _innerInput) external; +} diff --git a/contracts/system-contracts/interfaces/IPubdataChunkPublisher.sol b/contracts/system-contracts/interfaces/IPubdataChunkPublisher.sol new file mode 100644 index 0000000..47893ab --- /dev/null +++ b/contracts/system-contracts/interfaces/IPubdataChunkPublisher.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.20; + +/** + * @author Matter Labs + * @custom:security-contact security@matterlabs.dev + * @notice Interface for contract responsible chunking pubdata into the appropriate size for EIP-4844 blobs. + */ +interface IPubdataChunkPublisher { + /// @notice Chunks pubdata into pieces that can fit into blobs. + /// @param _pubdata The total l2 to l1 pubdata that will be sent via L1 blobs. + function chunkAndPublishPubdata(bytes calldata _pubdata) external; +} diff --git a/contracts/system-contracts/interfaces/ISystemContext.sol b/contracts/system-contracts/interfaces/ISystemContext.sol new file mode 100644 index 0000000..6b9a37f --- /dev/null +++ b/contracts/system-contracts/interfaces/ISystemContext.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.20; + +/** + * @author Matter Labs + * @custom:security-contact security@matterlabs.dev + * @notice Contract that stores some of the context variables, that may be either + * block-scoped, tx-scoped or system-wide. + */ +interface ISystemContext { + struct BlockInfo { + uint128 timestamp; + uint128 number; + } + + /// @notice A structure representing the timeline for the upgrade from the batch numbers to the L2 block numbers. + /// @dev It will be used for the L1 batch -> L2 block migration in Q3 2023 only. + struct VirtualBlockUpgradeInfo { + /// @notice In order to maintain consistent results for `blockhash` requests, we'll + /// have to remember the number of the batch when the upgrade to the virtual blocks has been done. + /// The hashes for virtual blocks before the upgrade are identical to the hashes of the corresponding batches. + uint128 virtualBlockStartBatch; + /// @notice L2 block when the virtual blocks have caught up with the L2 blocks. Starting from this block, + /// all the information returned to users for block.timestamp/number, etc should be the information about the L2 blocks and + /// not virtual blocks. + uint128 virtualBlockFinishL2Block; + } + + function chainId() external view returns (uint256); + + function origin() external view returns (address); + + function gasPrice() external view returns (uint256); + + function blockGasLimit() external view returns (uint256); + + function coinbase() external view returns (address); + + function difficulty() external view returns (uint256); + + function baseFee() external view returns (uint256); + + function txNumberInBlock() external view returns (uint16); + + function getBlockHashEVM(uint256 _block) external view returns (bytes32); + + function getBatchHash(uint256 _batchNumber) external view returns (bytes32 hash); + + function getBlockNumber() external view returns (uint128); + + function getBlockTimestamp() external view returns (uint128); + + function getBatchNumberAndTimestamp() external view returns (uint128 blockNumber, uint128 blockTimestamp); + + function getL2BlockNumberAndTimestamp() external view returns (uint128 blockNumber, uint128 blockTimestamp); + + function gasPerPubdataByte() external view returns (uint256 gasPerPubdataByte); + + function getCurrentPubdataSpent() external view returns (uint256 currentPubdataSpent); +} diff --git a/contracts/system-contracts/interfaces/ISystemContextDeprecated.sol b/contracts/system-contracts/interfaces/ISystemContextDeprecated.sol new file mode 100644 index 0000000..ac51532 --- /dev/null +++ b/contracts/system-contracts/interfaces/ISystemContextDeprecated.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.20; + +/** + * @author Matter Labs + * @custom:security-contact security@matterlabs.dev + * @notice The interface with deprecated functions of the SystemContext contract. It is aimed for backward compatibility. + */ +interface ISystemContextDeprecated { + function currentBlockInfo() external view returns (uint256); + + function getBlockNumberAndTimestamp() external view returns (uint256 blockNumber, uint256 blockTimestamp); + + function blockHash(uint256 _blockNumber) external view returns (bytes32 hash); +} diff --git a/contracts/system-contracts/libraries/EfficientCall.sol b/contracts/system-contracts/libraries/EfficientCall.sol new file mode 100644 index 0000000..27fea63 --- /dev/null +++ b/contracts/system-contracts/libraries/EfficientCall.sol @@ -0,0 +1,289 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.20; + +import {SystemContractHelper, ADDRESS_MASK} from "./SystemContractHelper.sol"; +import {SystemContractsCaller, CalldataForwardingMode, RAW_FAR_CALL_BY_REF_CALL_ADDRESS, SYSTEM_CALL_BY_REF_CALL_ADDRESS, MSG_VALUE_SIMULATOR_IS_SYSTEM_BIT, MIMIC_CALL_BY_REF_CALL_ADDRESS} from "./SystemContractsCaller.sol"; +import {Utils} from "./Utils.sol"; +import {SHA256_SYSTEM_CONTRACT, KECCAK256_SYSTEM_CONTRACT, MSG_VALUE_SYSTEM_CONTRACT} from "../Constants.sol"; +import {Keccak256InvalidReturnData, ShaInvalidReturnData} from "../SystemContractErrors.sol"; + +/** + * @author Matter Labs + * @custom:security-contact security@matterlabs.dev + * @notice This library is used to perform ultra-efficient calls using zkEVM-specific features. + * @dev EVM calls always accept a memory slice as input and return a memory slice as output. + * Therefore, even if the user has a ready-made calldata slice, they still need to copy it to memory + * before calling. This is especially inefficient for large inputs (proxies, multi-calls, etc.). + * In turn, zkEVM operates over a fat pointer, which is a set of (memory page, offset, start, length) in the memory/calldata/returndata. + * This allows forwarding the calldata slice as is, without copying it to memory. + * @dev Fat pointer is not just an integer, it is an extended data type supported on the VM level. + * zkEVM creates the wellformed fat pointers for all the calldata/returndata regions, later + * the contract may manipulate the already created fat pointers to forward a slice of the data, but not + * to create new fat pointers! + * @dev The allowed operation on fat pointers are: + * 1. `ptr.add` - Transforms `ptr.offset` into `ptr.offset + u32(_value)`. If overflow happens then it panics. + * 2. `ptr.sub` - Transforms `ptr.offset` into `ptr.offset - u32(_value)`. If underflow happens then it panics. + * 3. `ptr.pack` - Do the concatenation between the lowest 128 bits of the pointer itself and the highest 128 bits of `_value`. It is typically used to prepare the ABI for external calls. + * 4. `ptr.shrink` - Transforms `ptr.length` into `ptr.length - u32(_shrink)`. If underflow happens then it panics. + * @dev The call opcodes accept the fat pointer and change it to its canonical form before passing it to the child call + * 1. `ptr.start` is transformed into `ptr.offset + ptr.start` + * 2. `ptr.length` is transformed into `ptr.length - ptr.offset` + * 3. `ptr.offset` is transformed into `0` + */ +library EfficientCall { + /// @notice Call the `keccak256` without copying calldata to memory. + /// @param _data The preimage data. + /// @return The `keccak256` hash. + function keccak(bytes calldata _data) internal view returns (bytes32) { + bytes memory returnData = staticCall(gasleft(), KECCAK256_SYSTEM_CONTRACT, _data); + if (returnData.length != 32) { + revert Keccak256InvalidReturnData(); + } + return bytes32(returnData); + } + + /// @notice Call the `sha256` precompile without copying calldata to memory. + /// @param _data The preimage data. + /// @return The `sha256` hash. + function sha(bytes calldata _data) internal view returns (bytes32) { + bytes memory returnData = staticCall(gasleft(), SHA256_SYSTEM_CONTRACT, _data); + if (returnData.length != 32) { + revert ShaInvalidReturnData(); + } + return bytes32(returnData); + } + + /// @notice Perform a `call` without copying calldata to memory. + /// @param _gas The gas to use for the call. + /// @param _address The address to call. + /// @param _value The `msg.value` to send. + /// @param _data The calldata to use for the call. + /// @param _isSystem Whether the call should contain the `isSystem` flag. + /// @return returnData The copied to memory return data. + function call( + uint256 _gas, + address _address, + uint256 _value, + bytes calldata _data, + bool _isSystem + ) internal returns (bytes memory returnData) { + bool success = rawCall({_gas: _gas, _address: _address, _value: _value, _data: _data, _isSystem: _isSystem}); + returnData = _verifyCallResult(success); + } + + /// @notice Perform a `staticCall` without copying calldata to memory. + /// @param _gas The gas to use for the call. + /// @param _address The address to call. + /// @param _data The calldata to use for the call. + /// @return returnData The copied to memory return data. + function staticCall( + uint256 _gas, + address _address, + bytes calldata _data + ) internal view returns (bytes memory returnData) { + bool success = rawStaticCall(_gas, _address, _data); + returnData = _verifyCallResult(success); + } + + /// @notice Perform a `delegateCall` without copying calldata to memory. + /// @param _gas The gas to use for the call. + /// @param _address The address to call. + /// @param _data The calldata to use for the call. + /// @return returnData The copied to memory return data. + function delegateCall( + uint256 _gas, + address _address, + bytes calldata _data + ) internal returns (bytes memory returnData) { + bool success = rawDelegateCall(_gas, _address, _data); + returnData = _verifyCallResult(success); + } + + /// @notice Perform a `mimicCall` (a call with custom msg.sender) without copying calldata to memory. + /// @param _gas The gas to use for the call. + /// @param _address The address to call. + /// @param _data The calldata to use for the call. + /// @param _whoToMimic The `msg.sender` for the next call. + /// @param _isConstructor Whether the call should contain the `isConstructor` flag. + /// @param _isSystem Whether the call should contain the `isSystem` flag. + /// @return returnData The copied to memory return data. + function mimicCall( + uint256 _gas, + address _address, + bytes calldata _data, + address _whoToMimic, + bool _isConstructor, + bool _isSystem + ) internal returns (bytes memory returnData) { + bool success = rawMimicCall({ + _gas: _gas, + _address: _address, + _data: _data, + _whoToMimic: _whoToMimic, + _isConstructor: _isConstructor, + _isSystem: _isSystem + }); + + returnData = _verifyCallResult(success); + } + + /// @notice Perform a `call` without copying calldata to memory. + /// @param _gas The gas to use for the call. + /// @param _address The address to call. + /// @param _value The `msg.value` to send. + /// @param _data The calldata to use for the call. + /// @param _isSystem Whether the call should contain the `isSystem` flag. + /// @return success whether the call was successful. + function rawCall( + uint256 _gas, + address _address, + uint256 _value, + bytes calldata _data, + bool _isSystem + ) internal returns (bool success) { + if (_value == 0) { + _loadFarCallABIIntoActivePtr(_gas, _data, false, _isSystem); + + address callAddr = RAW_FAR_CALL_BY_REF_CALL_ADDRESS; + assembly { + success := call(_address, callAddr, 0, 0, 0xFFFF, 0, 0) + } + } else { + _loadFarCallABIIntoActivePtr(_gas, _data, false, true); + + // If there is provided `msg.value` call the `MsgValueSimulator` to forward ether. + address msgValueSimulator = MSG_VALUE_SYSTEM_CONTRACT; + address callAddr = SYSTEM_CALL_BY_REF_CALL_ADDRESS; + // We need to supply the mask to the MsgValueSimulator to denote + // that the call should be a system one. + uint256 forwardMask = _isSystem ? MSG_VALUE_SIMULATOR_IS_SYSTEM_BIT : 0; + + assembly { + success := call(msgValueSimulator, callAddr, _value, _address, 0xFFFF, forwardMask, 0) + } + } + } + + /// @notice Perform a `staticCall` without copying calldata to memory. + /// @param _gas The gas to use for the call. + /// @param _address The address to call. + /// @param _data The calldata to use for the call. + /// @return success whether the call was successful. + function rawStaticCall(uint256 _gas, address _address, bytes calldata _data) internal view returns (bool success) { + _loadFarCallABIIntoActivePtr(_gas, _data, false, false); + + address callAddr = RAW_FAR_CALL_BY_REF_CALL_ADDRESS; + assembly { + success := staticcall(_address, callAddr, 0, 0xFFFF, 0, 0) + } + } + + /// @notice Perform a `delegatecall` without copying calldata to memory. + /// @param _gas The gas to use for the call. + /// @param _address The address to call. + /// @param _data The calldata to use for the call. + /// @return success whether the call was successful. + function rawDelegateCall(uint256 _gas, address _address, bytes calldata _data) internal returns (bool success) { + _loadFarCallABIIntoActivePtr(_gas, _data, false, false); + + address callAddr = RAW_FAR_CALL_BY_REF_CALL_ADDRESS; + assembly { + success := delegatecall(_address, callAddr, 0, 0xFFFF, 0, 0) + } + } + + /// @notice Perform a `mimicCall` (call with custom msg.sender) without copying calldata to memory. + /// @param _gas The gas to use for the call. + /// @param _address The address to call. + /// @param _data The calldata to use for the call. + /// @param _whoToMimic The `msg.sender` for the next call. + /// @param _isConstructor Whether the call should contain the `isConstructor` flag. + /// @param _isSystem Whether the call should contain the `isSystem` flag. + /// @return success whether the call was successful. + /// @dev If called not in kernel mode, it will result in a revert (enforced by the VM) + function rawMimicCall( + uint256 _gas, + address _address, + bytes calldata _data, + address _whoToMimic, + bool _isConstructor, + bool _isSystem + ) internal returns (bool success) { + _loadFarCallABIIntoActivePtr(_gas, _data, _isConstructor, _isSystem); + + address callAddr = MIMIC_CALL_BY_REF_CALL_ADDRESS; + uint256 cleanupMask = ADDRESS_MASK; + assembly { + // Clearing values before usage in assembly, since Solidity + // doesn't do it by default + _whoToMimic := and(_whoToMimic, cleanupMask) + + success := call(_address, callAddr, 0, 0, _whoToMimic, 0, 0) + } + } + + /// @dev Verify that a low-level call was successful, and revert if it wasn't, by bubbling the revert reason. + /// @param _success Whether the call was successful. + /// @return returnData The copied to memory return data. + function _verifyCallResult(bool _success) private pure returns (bytes memory returnData) { + if (_success) { + uint256 size; + assembly { + size := returndatasize() + } + + returnData = new bytes(size); + assembly { + returndatacopy(add(returnData, 0x20), 0, size) + } + } else { + propagateRevert(); + } + } + + /// @dev Propagate the revert reason from the current call to the caller. + function propagateRevert() internal pure { + assembly { + let size := returndatasize() + returndatacopy(0, 0, size) + revert(0, size) + } + } + + /// @dev Load the far call ABI into active ptr, that will be used for the next call by reference. + /// @param _gas The gas to be passed to the call. + /// @param _data The calldata to be passed to the call. + /// @param _isConstructor Whether the call is a constructor call. + /// @param _isSystem Whether the call is a system call. + function _loadFarCallABIIntoActivePtr( + uint256 _gas, + bytes calldata _data, + bool _isConstructor, + bool _isSystem + ) private view { + SystemContractHelper.loadCalldataIntoActivePtr(); + + uint256 dataOffset; + assembly { + dataOffset := _data.offset + } + + // Safe to cast, offset is never bigger than `type(uint32).max` + SystemContractHelper.ptrAddIntoActive(uint32(dataOffset)); + // Safe to cast, `data.length` is never bigger than `type(uint32).max` + uint32 shrinkTo = uint32(msg.data.length - (_data.length + dataOffset)); + SystemContractHelper.ptrShrinkIntoActive(shrinkTo); + + uint32 gas = Utils.safeCastToU32(_gas); + uint256 farCallAbi = SystemContractsCaller.getFarCallABIWithEmptyFatPointer({ + gasPassed: gas, + // Only rollup is supported for now + shardId: 0, + forwardingMode: CalldataForwardingMode.ForwardFatPointer, + isConstructorCall: _isConstructor, + isSystemCall: _isSystem + }); + SystemContractHelper.ptrPackIntoActivePtr(farCallAbi); + } +} diff --git a/contracts/system-contracts/libraries/RLPEncoder.sol b/contracts/system-contracts/libraries/RLPEncoder.sol new file mode 100644 index 0000000..16eaa40 --- /dev/null +++ b/contracts/system-contracts/libraries/RLPEncoder.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.20; + +/** + * @author Matter Labs + * @custom:security-contact security@matterlabs.dev + * @notice This library provides RLP encoding functionality. + */ +library RLPEncoder { + function encodeAddress(address _val) internal pure returns (bytes memory encoded) { + // The size is equal to 20 bytes of the address itself + 1 for encoding bytes length in RLP. + encoded = new bytes(0x15); + + bytes20 shiftedVal = bytes20(_val); + assembly { + // In the first byte we write the encoded length as 0x80 + 0x14 == 0x94. + mstore(add(encoded, 0x20), 0x9400000000000000000000000000000000000000000000000000000000000000) + // Write address data without stripping zeros. + mstore(add(encoded, 0x21), shiftedVal) + } + } + + function encodeUint256(uint256 _val) internal pure returns (bytes memory encoded) { + unchecked { + if (_val < 128) { + encoded = new bytes(1); + // Handle zero as a non-value, since stripping zeroes results in an empty byte array + encoded[0] = (_val == 0) ? bytes1(uint8(128)) : bytes1(uint8(_val)); + } else { + uint256 hbs = _highestByteSet(_val); + + encoded = new bytes(hbs + 2); + encoded[0] = bytes1(uint8(hbs + 0x81)); + + uint256 lbs = 31 - hbs; + uint256 shiftedVal = _val << (lbs * 8); + + assembly { + mstore(add(encoded, 0x21), shiftedVal) + } + } + } + } + + /// @notice Encodes the size of bytes in RLP format. + /// @param _len The length of the bytes to encode. It has a `uint64` type since as larger values are not supported. + /// NOTE: panics if the length is 1 since the length encoding is ambiguous in this case. + function encodeNonSingleBytesLen(uint64 _len) internal pure returns (bytes memory) { + assert(_len != 1); + return _encodeLength(_len, 0x80); + } + + /// @notice Encodes the size of list items in RLP format. + /// @param _len The length of the bytes to encode. It has a `uint64` type since as larger values are not supported. + function encodeListLen(uint64 _len) internal pure returns (bytes memory) { + return _encodeLength(_len, 0xc0); + } + + function _encodeLength(uint64 _len, uint256 _offset) private pure returns (bytes memory encoded) { + unchecked { + if (_len < 56) { + encoded = new bytes(1); + encoded[0] = bytes1(uint8(_len + _offset)); + } else { + uint256 hbs = _highestByteSet(uint256(_len)); + + encoded = new bytes(hbs + 2); + encoded[0] = bytes1(uint8(_offset + hbs + 56)); + + uint256 lbs = 31 - hbs; + uint256 shiftedVal = uint256(_len) << (lbs * 8); + + assembly { + mstore(add(encoded, 0x21), shiftedVal) + } + } + } + } + + /// @notice Computes the index of the highest byte set in number. + /// @notice Uses little endian ordering (The least significant byte has index `0`). + /// NOTE: returns `0` for `0` + function _highestByteSet(uint256 _number) private pure returns (uint256 hbs) { + unchecked { + if (_number > type(uint128).max) { + _number >>= 128; + hbs += 16; + } + if (_number > type(uint64).max) { + _number >>= 64; + hbs += 8; + } + if (_number > type(uint32).max) { + _number >>= 32; + hbs += 4; + } + if (_number > type(uint16).max) { + _number >>= 16; + hbs += 2; + } + if (_number > type(uint8).max) { + ++hbs; + } + } + } +} diff --git a/contracts/system-contracts/libraries/SystemContractHelper.sol b/contracts/system-contracts/libraries/SystemContractHelper.sol new file mode 100644 index 0000000..e8469e3 --- /dev/null +++ b/contracts/system-contracts/libraries/SystemContractHelper.sol @@ -0,0 +1,361 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.20; + +import {MAX_SYSTEM_CONTRACT_ADDRESS} from "../Constants.sol"; + +import {CALLFLAGS_CALL_ADDRESS, CODE_ADDRESS_CALL_ADDRESS, EVENT_WRITE_ADDRESS, EVENT_INITIALIZE_ADDRESS, GET_EXTRA_ABI_DATA_ADDRESS, LOAD_CALLDATA_INTO_ACTIVE_PTR_CALL_ADDRESS, META_CODE_SHARD_ID_OFFSET, META_CALLER_SHARD_ID_OFFSET, META_SHARD_ID_OFFSET, META_AUX_HEAP_SIZE_OFFSET, META_HEAP_SIZE_OFFSET, META_PUBDATA_PUBLISHED_OFFSET, META_CALL_ADDRESS, PTR_CALLDATA_CALL_ADDRESS, PTR_ADD_INTO_ACTIVE_CALL_ADDRESS, PTR_SHRINK_INTO_ACTIVE_CALL_ADDRESS, PTR_PACK_INTO_ACTIVE_CALL_ADDRESS, PRECOMPILE_CALL_ADDRESS, SET_CONTEXT_VALUE_CALL_ADDRESS, TO_L1_CALL_ADDRESS} from "./SystemContractsCaller.sol"; +import {IndexOutOfBounds, FailedToChargeGas} from "../SystemContractErrors.sol"; + +uint256 constant UINT32_MASK = type(uint32).max; +uint256 constant UINT64_MASK = type(uint64).max; +uint256 constant UINT128_MASK = type(uint128).max; +uint256 constant ADDRESS_MASK = type(uint160).max; + +/// @notice NOTE: The `getZkSyncMeta` that is used to obtain this struct will experience a breaking change in 2024. +struct ZkSyncMeta { + uint32 pubdataPublished; + uint32 heapSize; + uint32 auxHeapSize; + uint8 shardId; + uint8 callerShardId; + uint8 codeShardId; +} + +enum Global { + CalldataPtr, + CallFlags, + ExtraABIData1, + ExtraABIData2, + ReturndataPtr +} + +/** + * @author Matter Labs + * @custom:security-contact security@matterlabs.dev + * @notice Library used for accessing zkEVM-specific opcodes, needed for the development + * of system contracts. + * @dev While this library will be eventually available to public, some of the provided + * methods won't work for non-system contracts and also breaking changes at short notice are possible. + * We do not recommend this library for external use. + */ +library SystemContractHelper { + /// @notice Send an L2Log to L1. + /// @param _isService The `isService` flag. + /// @param _key The `key` part of the L2Log. + /// @param _value The `value` part of the L2Log. + /// @dev The meaning of all these parameters is context-dependent, but they + /// have no intrinsic meaning per se. + function toL1(bool _isService, bytes32 _key, bytes32 _value) internal { + address callAddr = TO_L1_CALL_ADDRESS; + assembly { + // Ensuring that the type is bool + _isService := and(_isService, 1) + // This `success` is always 0, but the method always succeeds + // (except for the cases when there is not enough gas) + // solhint-disable-next-line no-unused-vars + let success := call(_isService, callAddr, _key, _value, 0xFFFF, 0, 0) + } + } + + /// @notice Get address of the currently executed code. + /// @dev This allows differentiating between `call` and `delegatecall`. + /// During the former `this` and `codeAddress` are the same, while + /// during the latter they are not. + function getCodeAddress() internal view returns (address addr) { + address callAddr = CODE_ADDRESS_CALL_ADDRESS; + assembly { + addr := staticcall(0, callAddr, 0, 0xFFFF, 0, 0) + } + } + + /// @notice Provide a compiler hint, by placing calldata fat pointer into virtual `ACTIVE_PTR`, + /// that can be manipulated by `ptr.add`/`ptr.sub`/`ptr.pack`/`ptr.shrink` later. + /// @dev This allows making a call by forwarding calldata pointer to the child call. + /// It is a much more efficient way to forward calldata, than standard EVM bytes copying. + function loadCalldataIntoActivePtr() internal view { + address callAddr = LOAD_CALLDATA_INTO_ACTIVE_PTR_CALL_ADDRESS; + assembly { + pop(staticcall(0, callAddr, 0, 0xFFFF, 0, 0)) + } + } + + /// @notice Compiler simulation of the `ptr.pack` opcode for the virtual `ACTIVE_PTR` pointer. + /// @dev Do the concatenation between lowest part of `ACTIVE_PTR` and highest part of `_farCallAbi` + /// forming packed fat pointer for a far call or ret ABI when necessary. + /// Note: Panics if the lowest 128 bits of `_farCallAbi` are not zeroes. + function ptrPackIntoActivePtr(uint256 _farCallAbi) internal view { + address callAddr = PTR_PACK_INTO_ACTIVE_CALL_ADDRESS; + assembly { + pop(staticcall(_farCallAbi, callAddr, 0, 0xFFFF, 0, 0)) + } + } + + /// @notice Compiler simulation of the `ptr.add` opcode for the virtual `ACTIVE_PTR` pointer. + /// @dev Transforms `ACTIVE_PTR.offset` into `ACTIVE_PTR.offset + u32(_value)`. If overflow happens then it panics. + function ptrAddIntoActive(uint32 _value) internal view { + address callAddr = PTR_ADD_INTO_ACTIVE_CALL_ADDRESS; + uint256 cleanupMask = UINT32_MASK; + assembly { + // Clearing input params as they are not cleaned by Solidity by default + _value := and(_value, cleanupMask) + pop(staticcall(_value, callAddr, 0, 0xFFFF, 0, 0)) + } + } + + /// @notice Compiler simulation of the `ptr.shrink` opcode for the virtual `ACTIVE_PTR` pointer. + /// @dev Transforms `ACTIVE_PTR.length` into `ACTIVE_PTR.length - u32(_shrink)`. If underflow happens then it panics. + function ptrShrinkIntoActive(uint32 _shrink) internal view { + address callAddr = PTR_SHRINK_INTO_ACTIVE_CALL_ADDRESS; + uint256 cleanupMask = UINT32_MASK; + assembly { + // Clearing input params as they are not cleaned by Solidity by default + _shrink := and(_shrink, cleanupMask) + pop(staticcall(_shrink, callAddr, 0, 0xFFFF, 0, 0)) + } + } + + /// @notice packs precompile parameters into one word + /// @param _inputMemoryOffset The memory offset in 32-byte words for the input data for calling the precompile. + /// @param _inputMemoryLength The length of the input data in words. + /// @param _outputMemoryOffset The memory offset in 32-byte words for the output data. + /// @param _outputMemoryLength The length of the output data in words. + /// @param _perPrecompileInterpreted The constant, the meaning of which is defined separately for + /// each precompile. For information, please read the documentation of the precompilecall log in + /// the VM. + function packPrecompileParams( + uint32 _inputMemoryOffset, + uint32 _inputMemoryLength, + uint32 _outputMemoryOffset, + uint32 _outputMemoryLength, + uint64 _perPrecompileInterpreted + ) internal pure returns (uint256 rawParams) { + rawParams = _inputMemoryOffset; + rawParams |= uint256(_inputMemoryLength) << 32; + rawParams |= uint256(_outputMemoryOffset) << 64; + rawParams |= uint256(_outputMemoryLength) << 96; + rawParams |= uint256(_perPrecompileInterpreted) << 192; + } + + /// @notice Call precompile with given parameters. + /// @param _rawParams The packed precompile params. They can be retrieved by + /// the `packPrecompileParams` method. + /// @param _gasToBurn The number of gas to burn during this call. + /// @param _pubdataToSpend The number of pubdata bytes to burn during the call. + /// @return success Whether the call was successful. + /// @dev The list of currently available precompiles sha256, keccak256, ecrecover. + /// NOTE: The precompile type depends on `this` which calls precompile, which means that only + /// system contracts corresponding to the list of precompiles above can do `precompileCall`. + /// @dev If used not in the `sha256`, `keccak256` or `ecrecover` contracts, it will just burn the gas provided. + /// @dev This method is `unsafe` because it does not check whether there is enough gas to burn. + function unsafePrecompileCall( + uint256 _rawParams, + uint32 _gasToBurn, + uint32 _pubdataToSpend + ) internal view returns (bool success) { + address callAddr = PRECOMPILE_CALL_ADDRESS; + + uint256 params = uint256(_gasToBurn) + (uint256(_pubdataToSpend) << 32); + + uint256 cleanupMask = UINT64_MASK; + assembly { + // Clearing input params as they are not cleaned by Solidity by default + params := and(params, cleanupMask) + success := staticcall(_rawParams, callAddr, params, 0xFFFF, 0, 0) + } + } + + /// @notice Set `msg.value` to next far call. + /// @param _value The msg.value that will be used for the *next* call. + /// @dev If called not in kernel mode, it will result in a revert (enforced by the VM) + function setValueForNextFarCall(uint128 _value) internal returns (bool success) { + uint256 cleanupMask = UINT128_MASK; + address callAddr = SET_CONTEXT_VALUE_CALL_ADDRESS; + assembly { + // Clearing input params as they are not cleaned by Solidity by default + _value := and(_value, cleanupMask) + success := call(0, callAddr, _value, 0, 0xFFFF, 0, 0) + } + } + + /// @notice Initialize a new event. + /// @param initializer The event initializing value. + /// @param value1 The first topic or data chunk. + function eventInitialize(uint256 initializer, uint256 value1) internal { + address callAddr = EVENT_INITIALIZE_ADDRESS; + assembly { + pop(call(initializer, callAddr, value1, 0, 0xFFFF, 0, 0)) + } + } + + /// @notice Continue writing the previously initialized event. + /// @param value1 The first topic or data chunk. + /// @param value2 The second topic or data chunk. + function eventWrite(uint256 value1, uint256 value2) internal { + address callAddr = EVENT_WRITE_ADDRESS; + assembly { + pop(call(value1, callAddr, value2, 0, 0xFFFF, 0, 0)) + } + } + + /// @notice Get the packed representation of the `ZkSyncMeta` from the current context. + /// @notice NOTE: The behavior of this function will experience a breaking change in 2024. + /// @return meta The packed representation of the ZkSyncMeta. + /// @dev The fields in ZkSyncMeta are NOT tightly packed, i.e. there is a special rule on how + /// they are packed. For more information, please read the documentation on ZkSyncMeta. + function getZkSyncMetaBytes() internal view returns (uint256 meta) { + address callAddr = META_CALL_ADDRESS; + assembly { + meta := staticcall(0, callAddr, 0, 0xFFFF, 0, 0) + } + } + + /// @notice Returns the bits [offset..offset+size-1] of the meta. + /// @param meta Packed representation of the ZkSyncMeta. + /// @param offset The offset of the bits. + /// @param size The size of the extracted number in bits. + /// @return result The extracted number. + function extractNumberFromMeta(uint256 meta, uint256 offset, uint256 size) internal pure returns (uint256 result) { + // Firstly, we delete all the bits after the field + uint256 shifted = (meta << (256 - size - offset)); + // Then we shift everything back + result = (shifted >> (256 - size)); + } + + /// @notice Given the packed representation of `ZkSyncMeta`, retrieves the number of pubdata + /// bytes published in the batch so far. + /// @notice NOTE: The behavior of this function will experience a breaking change in 2024. + /// @param meta Packed representation of the ZkSyncMeta. + /// @return pubdataPublished The amount of pubdata published in the system so far. + function getPubdataPublishedFromMeta(uint256 meta) internal pure returns (uint32 pubdataPublished) { + pubdataPublished = uint32(extractNumberFromMeta(meta, META_PUBDATA_PUBLISHED_OFFSET, 32)); + } + + /// @notice Given the packed representation of `ZkSyncMeta`, retrieves the number of the current size + /// of the heap in bytes. + /// @param meta Packed representation of the ZkSyncMeta. + /// @return heapSize The size of the memory in bytes byte. + /// @dev The following expression: getHeapSizeFromMeta(getZkSyncMetaBytes()) is + /// equivalent to the MSIZE in Solidity. + function getHeapSizeFromMeta(uint256 meta) internal pure returns (uint32 heapSize) { + heapSize = uint32(extractNumberFromMeta(meta, META_HEAP_SIZE_OFFSET, 32)); + } + + /// @notice Given the packed representation of `ZkSyncMeta`, retrieves the number of the current size + /// of the auxiliary heap in bytes. + /// @param meta Packed representation of the ZkSyncMeta. + /// @return auxHeapSize The size of the auxiliary memory in bytes byte. + /// @dev You can read more on auxiliary memory in the VM1.2 documentation. + function getAuxHeapSizeFromMeta(uint256 meta) internal pure returns (uint32 auxHeapSize) { + auxHeapSize = uint32(extractNumberFromMeta(meta, META_AUX_HEAP_SIZE_OFFSET, 32)); + } + + /// @notice Given the packed representation of `ZkSyncMeta`, retrieves the shardId of `this`. + /// @param meta Packed representation of the ZkSyncMeta. + /// @return shardId The shardId of `this`. + /// @dev Currently only shard 0 (zkRollup) is supported. + function getShardIdFromMeta(uint256 meta) internal pure returns (uint8 shardId) { + shardId = uint8(extractNumberFromMeta(meta, META_SHARD_ID_OFFSET, 8)); + } + + /// @notice Given the packed representation of `ZkSyncMeta`, retrieves the shardId of + /// the msg.sender. + /// @param meta Packed representation of the ZkSyncMeta. + /// @return callerShardId The shardId of the msg.sender. + /// @dev Currently only shard 0 (zkRollup) is supported. + function getCallerShardIdFromMeta(uint256 meta) internal pure returns (uint8 callerShardId) { + callerShardId = uint8(extractNumberFromMeta(meta, META_CALLER_SHARD_ID_OFFSET, 8)); + } + + /// @notice Given the packed representation of `ZkSyncMeta`, retrieves the shardId of + /// the currently executed code. + /// @param meta Packed representation of the ZkSyncMeta. + /// @return codeShardId The shardId of the currently executed code. + /// @dev Currently only shard 0 (zkRollup) is supported. + function getCodeShardIdFromMeta(uint256 meta) internal pure returns (uint8 codeShardId) { + codeShardId = uint8(extractNumberFromMeta(meta, META_CODE_SHARD_ID_OFFSET, 8)); + } + + /// @notice Retrieves the ZkSyncMeta structure. + /// @notice NOTE: The behavior of this function will experience a breaking change in 2024. + /// @return meta The ZkSyncMeta execution context parameters. + function getZkSyncMeta() internal view returns (ZkSyncMeta memory meta) { + uint256 metaPacked = getZkSyncMetaBytes(); + meta.pubdataPublished = getPubdataPublishedFromMeta(metaPacked); + meta.heapSize = getHeapSizeFromMeta(metaPacked); + meta.auxHeapSize = getAuxHeapSizeFromMeta(metaPacked); + meta.shardId = getShardIdFromMeta(metaPacked); + meta.callerShardId = getCallerShardIdFromMeta(metaPacked); + meta.codeShardId = getCodeShardIdFromMeta(metaPacked); + } + + /// @notice Returns the call flags for the current call. + /// @return callFlags The bitmask of the callflags. + /// @dev Call flags is the value of the first register + /// at the start of the call. + /// @dev The zero bit of the callFlags indicates whether the call is + /// a constructor call. The first bit of the callFlags indicates whether + /// the call is a system one. + function getCallFlags() internal view returns (uint256 callFlags) { + address callAddr = CALLFLAGS_CALL_ADDRESS; + assembly { + callFlags := staticcall(0, callAddr, 0, 0xFFFF, 0, 0) + } + } + + /// @notice Returns the current calldata pointer. + /// @return ptr The current calldata pointer. + /// @dev NOTE: This file is just an integer and it cannot be used + /// to forward the calldata to the next calls in any way. + function getCalldataPtr() internal view returns (uint256 ptr) { + address callAddr = PTR_CALLDATA_CALL_ADDRESS; + assembly { + ptr := staticcall(0, callAddr, 0, 0xFFFF, 0, 0) + } + } + + /// @notice Returns the N-th extraAbiParam for the current call. + /// @return extraAbiData The value of the N-th extraAbiParam for this call. + /// @dev It is equal to the value of the (N+2)-th register + /// at the start of the call. + function getExtraAbiData(uint256 index) internal view returns (uint256 extraAbiData) { + // Note that there are only 10 accessible registers (indices 0-9 inclusively) + if (index > 9) { + revert IndexOutOfBounds(); + } + + address callAddr = GET_EXTRA_ABI_DATA_ADDRESS; + assembly { + extraAbiData := staticcall(index, callAddr, 0, 0xFFFF, 0, 0) + } + } + + /// @notice Returns whether the current call is a system call. + /// @return `true` or `false` based on whether the current call is a system call. + function isSystemCall() internal view returns (bool) { + uint256 callFlags = getCallFlags(); + // When the system call is passed, the 2-bit is set to 1 + return (callFlags & 2) != 0; + } + + /// @notice Returns whether the address is a system contract. + /// @param _address The address to test + /// @return `true` or `false` based on whether the `_address` is a system contract. + function isSystemContract(address _address) internal pure returns (bool) { + return uint160(_address) <= uint160(MAX_SYSTEM_CONTRACT_ADDRESS); + } + + /// @notice Method used for burning a certain amount of gas. + /// @param _gasToPay The number of gas to burn. + /// @param _pubdataToSpend The number of pubdata bytes to burn during the call. + function burnGas(uint32 _gasToPay, uint32 _pubdataToSpend) internal view { + bool precompileCallSuccess = unsafePrecompileCall( + 0, // The precompile parameters are formal ones. We only need the precompile call to burn gas. + _gasToPay, + _pubdataToSpend + ); + if (!precompileCallSuccess) { + revert FailedToChargeGas(); + } + } +} diff --git a/contracts/system-contracts/libraries/SystemContractsCaller.sol b/contracts/system-contracts/libraries/SystemContractsCaller.sol new file mode 100644 index 0000000..9497b0c --- /dev/null +++ b/contracts/system-contracts/libraries/SystemContractsCaller.sol @@ -0,0 +1,268 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.20; + +import {MSG_VALUE_SYSTEM_CONTRACT, MSG_VALUE_SIMULATOR_IS_SYSTEM_BIT} from "../Constants.sol"; +import {Utils} from "./Utils.sol"; + +// Addresses used for the compiler to be replaced with the +// ZKsync-specific opcodes during the compilation. +// IMPORTANT: these are just compile-time constants and are used +// only if used in-place by Yul optimizer. +address constant TO_L1_CALL_ADDRESS = address((1 << 16) - 1); +address constant CODE_ADDRESS_CALL_ADDRESS = address((1 << 16) - 2); +address constant PRECOMPILE_CALL_ADDRESS = address((1 << 16) - 3); +address constant META_CALL_ADDRESS = address((1 << 16) - 4); +address constant MIMIC_CALL_CALL_ADDRESS = address((1 << 16) - 5); +address constant SYSTEM_MIMIC_CALL_CALL_ADDRESS = address((1 << 16) - 6); +address constant MIMIC_CALL_BY_REF_CALL_ADDRESS = address((1 << 16) - 7); +address constant SYSTEM_MIMIC_CALL_BY_REF_CALL_ADDRESS = address((1 << 16) - 8); +address constant RAW_FAR_CALL_CALL_ADDRESS = address((1 << 16) - 9); +address constant RAW_FAR_CALL_BY_REF_CALL_ADDRESS = address((1 << 16) - 10); +address constant SYSTEM_CALL_CALL_ADDRESS = address((1 << 16) - 11); +address constant SYSTEM_CALL_BY_REF_CALL_ADDRESS = address((1 << 16) - 12); +address constant SET_CONTEXT_VALUE_CALL_ADDRESS = address((1 << 16) - 13); +address constant SET_PUBDATA_PRICE_CALL_ADDRESS = address((1 << 16) - 14); +address constant INCREMENT_TX_COUNTER_CALL_ADDRESS = address((1 << 16) - 15); +address constant PTR_CALLDATA_CALL_ADDRESS = address((1 << 16) - 16); +address constant CALLFLAGS_CALL_ADDRESS = address((1 << 16) - 17); +address constant PTR_RETURNDATA_CALL_ADDRESS = address((1 << 16) - 18); +address constant EVENT_INITIALIZE_ADDRESS = address((1 << 16) - 19); +address constant EVENT_WRITE_ADDRESS = address((1 << 16) - 20); +address constant LOAD_CALLDATA_INTO_ACTIVE_PTR_CALL_ADDRESS = address((1 << 16) - 21); +address constant LOAD_LATEST_RETURNDATA_INTO_ACTIVE_PTR_CALL_ADDRESS = address((1 << 16) - 22); +address constant PTR_ADD_INTO_ACTIVE_CALL_ADDRESS = address((1 << 16) - 23); +address constant PTR_SHRINK_INTO_ACTIVE_CALL_ADDRESS = address((1 << 16) - 24); +address constant PTR_PACK_INTO_ACTIVE_CALL_ADDRESS = address((1 << 16) - 25); +address constant MULTIPLICATION_HIGH_ADDRESS = address((1 << 16) - 26); +address constant GET_EXTRA_ABI_DATA_ADDRESS = address((1 << 16) - 27); + +// All the offsets are in bits +uint256 constant META_PUBDATA_PUBLISHED_OFFSET = 0 * 8; +uint256 constant META_HEAP_SIZE_OFFSET = 8 * 8; +uint256 constant META_AUX_HEAP_SIZE_OFFSET = 12 * 8; +uint256 constant META_SHARD_ID_OFFSET = 28 * 8; +uint256 constant META_CALLER_SHARD_ID_OFFSET = 29 * 8; +uint256 constant META_CODE_SHARD_ID_OFFSET = 30 * 8; + +/// @notice The way to forward the calldata: +/// - Use the current heap (i.e. the same as on EVM). +/// - Use the auxiliary heap. +/// - Forward via a pointer +/// @dev Note, that currently, users do not have access to the auxiliary +/// heap and so the only type of forwarding that will be used by the users +/// are UseHeap and ForwardFatPointer for forwarding a slice of the current calldata +/// to the next call. +enum CalldataForwardingMode { + UseHeap, + ForwardFatPointer, + UseAuxHeap +} + +/** + * @author Matter Labs + * @custom:security-contact security@matterlabs.dev + * @notice A library that allows calling contracts with the `isSystem` flag. + * @dev It is needed to call ContractDeployer and NonceHolder. + */ +library SystemContractsCaller { + /// @notice Makes a call with the `isSystem` flag. + /// @param gasLimit The gas limit for the call. + /// @param to The address to call. + /// @param value The value to pass with the transaction. + /// @param data The calldata. + /// @return success Whether the transaction has been successful. + /// @dev Note, that the `isSystem` flag can only be set when calling system contracts. + function systemCall(uint32 gasLimit, address to, uint256 value, bytes memory data) internal returns (bool success) { + address callAddr = SYSTEM_CALL_CALL_ADDRESS; + + uint32 dataStart; + assembly { + dataStart := add(data, 0x20) + } + uint32 dataLength = Utils.safeCastToU32(data.length); + + uint256 farCallAbi = SystemContractsCaller.getFarCallABI({ + dataOffset: 0, + memoryPage: 0, + dataStart: dataStart, + dataLength: dataLength, + gasPassed: gasLimit, + // Only rollup is supported for now + shardId: 0, + forwardingMode: CalldataForwardingMode.UseHeap, + isConstructorCall: false, + isSystemCall: true + }); + + if (value == 0) { + // Doing the system call directly + assembly { + success := call(to, callAddr, 0, 0, farCallAbi, 0, 0) + } + } else { + address msgValueSimulator = MSG_VALUE_SYSTEM_CONTRACT; + // We need to supply the mask to the MsgValueSimulator to denote + // that the call should be a system one. + uint256 forwardMask = MSG_VALUE_SIMULATOR_IS_SYSTEM_BIT; + + assembly { + success := call(msgValueSimulator, callAddr, value, to, farCallAbi, forwardMask, 0) + } + } + } + + /// @notice Makes a call with the `isSystem` flag. + /// @param gasLimit The gas limit for the call. + /// @param to The address to call. + /// @param value The value to pass with the transaction. + /// @param data The calldata. + /// @return success Whether the transaction has been successful. + /// @return returnData The returndata of the transaction (revert reason in case the transaction has failed). + /// @dev Note, that the `isSystem` flag can only be set when calling system contracts. + function systemCallWithReturndata( + uint32 gasLimit, + address to, + uint128 value, + bytes memory data + ) internal returns (bool success, bytes memory returnData) { + success = systemCall(gasLimit, to, value, data); + + uint256 size; + assembly { + size := returndatasize() + } + + returnData = new bytes(size); + assembly { + returndatacopy(add(returnData, 0x20), 0, size) + } + } + + /// @notice Makes a call with the `isSystem` flag. + /// @param gasLimit The gas limit for the call. + /// @param to The address to call. + /// @param value The value to pass with the transaction. + /// @param data The calldata. + /// @return returnData The returndata of the transaction. In case the transaction reverts, the error + /// bubbles up to the parent frame. + /// @dev Note, that the `isSystem` flag can only be set when calling system contracts. + function systemCallWithPropagatedRevert( + uint32 gasLimit, + address to, + uint128 value, + bytes memory data + ) internal returns (bytes memory returnData) { + bool success; + (success, returnData) = systemCallWithReturndata(gasLimit, to, value, data); + + if (!success) { + assembly { + let size := mload(returnData) + revert(add(returnData, 0x20), size) + } + } + } + + /// @notice Calculates the packed representation of the FarCallABI. + /// @param dataOffset Calldata offset in memory. Provide 0 unless using custom pointer. + /// @param memoryPage Memory page to use. Provide 0 unless using custom pointer. + /// @param dataStart The start of the calldata slice. Provide the offset in memory + /// if not using custom pointer. + /// @param dataLength The calldata length. Provide the length of the calldata in bytes + /// unless using custom pointer. + /// @param gasPassed The gas to pass with the call. + /// @param shardId Of the account to call. Currently only 0 is supported. + /// @param forwardingMode The forwarding mode to use: + /// - provide CalldataForwardingMode.UseHeap when using your current memory + /// - provide CalldataForwardingMode.ForwardFatPointer when using custom pointer. + /// @param isConstructorCall Whether the call will be a call to the constructor + /// (ignored when the caller is not a system contract). + /// @param isSystemCall Whether the call will have the `isSystem` flag. + /// @return farCallAbi The far call ABI. + /// @dev The `FarCallABI` has the following structure: + /// pub struct FarCallABI { + /// pub memory_quasi_fat_pointer: FatPointer, + /// pub gas_passed: u32, + /// pub shard_id: u8, + /// pub forwarding_mode: FarCallForwardPageType, + /// pub constructor_call: bool, + /// pub to_system: bool, + /// } + /// + /// The FatPointer struct: + /// + /// pub struct FatPointer { + /// pub offset: u32, // offset relative to `start` + /// pub memory_page: u32, // memory page where slice is located + /// pub start: u32, // absolute start of the slice + /// pub length: u32, // length of the slice + /// } + /// + /// @dev Note, that the actual layout is the following: + /// + /// [0..32) bits -- the calldata offset + /// [32..64) bits -- the memory page to use. Can be left blank in most of the cases. + /// [64..96) bits -- the absolute start of the slice + /// [96..128) bits -- the length of the slice. + /// [128..192) bits -- empty bits. + /// [192..224) bits -- gasPassed. + /// [224..232) bits -- forwarding_mode + /// [232..240) bits -- shard id. + /// [240..248) bits -- constructor call flag + /// [248..256] bits -- system call flag + function getFarCallABI( + uint32 dataOffset, + uint32 memoryPage, + uint32 dataStart, + uint32 dataLength, + uint32 gasPassed, + uint8 shardId, + CalldataForwardingMode forwardingMode, + bool isConstructorCall, + bool isSystemCall + ) internal pure returns (uint256 farCallAbi) { + // Fill in the call parameter fields + farCallAbi = getFarCallABIWithEmptyFatPointer({ + gasPassed: gasPassed, + shardId: shardId, + forwardingMode: forwardingMode, + isConstructorCall: isConstructorCall, + isSystemCall: isSystemCall + }); + + // Fill in the fat pointer fields + farCallAbi |= dataOffset; + farCallAbi |= (uint256(memoryPage) << 32); + farCallAbi |= (uint256(dataStart) << 64); + farCallAbi |= (uint256(dataLength) << 96); + } + + /// @notice Calculates the packed representation of the FarCallABI with zero fat pointer fields. + /// @param gasPassed The gas to pass with the call. + /// @param shardId Of the account to call. Currently only 0 is supported. + /// @param forwardingMode The forwarding mode to use: + /// - provide CalldataForwardingMode.UseHeap when using your current memory + /// - provide CalldataForwardingMode.ForwardFatPointer when using custom pointer. + /// @param isConstructorCall Whether the call will be a call to the constructor + /// (ignored when the caller is not a system contract). + /// @param isSystemCall Whether the call will have the `isSystem` flag. + /// @return farCallAbiWithEmptyFatPtr The far call ABI with zero fat pointer fields. + function getFarCallABIWithEmptyFatPointer( + uint32 gasPassed, + uint8 shardId, + CalldataForwardingMode forwardingMode, + bool isConstructorCall, + bool isSystemCall + ) internal pure returns (uint256 farCallAbiWithEmptyFatPtr) { + farCallAbiWithEmptyFatPtr |= (uint256(gasPassed) << 192); + farCallAbiWithEmptyFatPtr |= (uint256(forwardingMode) << 224); + farCallAbiWithEmptyFatPtr |= (uint256(shardId) << 232); + if (isConstructorCall) { + farCallAbiWithEmptyFatPtr |= (1 << 240); + } + if (isSystemCall) { + farCallAbiWithEmptyFatPtr |= (1 << 248); + } + } +} diff --git a/contracts/system-contracts/libraries/TransactionHelper.sol b/contracts/system-contracts/libraries/TransactionHelper.sol new file mode 100644 index 0000000..467eb57 --- /dev/null +++ b/contracts/system-contracts/libraries/TransactionHelper.sol @@ -0,0 +1,422 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.20; + +import {IERC20} from "../openzeppelin/token/ERC20/IERC20.sol"; +import {SafeERC20} from "../openzeppelin/token/ERC20/utils/SafeERC20.sol"; + +import {IPaymasterFlow} from "../interfaces/IPaymasterFlow.sol"; +import {BASE_TOKEN_SYSTEM_CONTRACT, BOOTLOADER_FORMAL_ADDRESS} from "../Constants.sol"; +import {RLPEncoder} from "./RLPEncoder.sol"; +import {EfficientCall} from "./EfficientCall.sol"; +import {UnsupportedTxType, InvalidInput, UnsupportedPaymasterFlow} from "../SystemContractErrors.sol"; + +/// @dev The type id of ZKsync's EIP-712-signed transaction. +uint8 constant EIP_712_TX_TYPE = 0x71; + +/// @dev The type id of legacy transactions. +uint8 constant LEGACY_TX_TYPE = 0x0; +/// @dev The type id of legacy transactions. +uint8 constant EIP_2930_TX_TYPE = 0x01; +/// @dev The type id of EIP1559 transactions. +uint8 constant EIP_1559_TX_TYPE = 0x02; + +/// @notice Structure used to represent a ZKsync transaction. +struct Transaction { + // The type of the transaction. + uint256 txType; + // The caller. + uint256 from; + // The callee. + uint256 to; + // The gasLimit to pass with the transaction. + // It has the same meaning as Ethereum's gasLimit. + uint256 gasLimit; + // The maximum amount of gas the user is willing to pay for a byte of pubdata. + uint256 gasPerPubdataByteLimit; + // The maximum fee per gas that the user is willing to pay. + // It is akin to EIP1559's maxFeePerGas. + uint256 maxFeePerGas; + // The maximum priority fee per gas that the user is willing to pay. + // It is akin to EIP1559's maxPriorityFeePerGas. + uint256 maxPriorityFeePerGas; + // The transaction's paymaster. If there is no paymaster, it is equal to 0. + uint256 paymaster; + // The nonce of the transaction. + uint256 nonce; + // The value to pass with the transaction. + uint256 value; + // In the future, we might want to add some + // new fields to the struct. The `txData` struct + // is to be passed to account and any changes to its structure + // would mean a breaking change to these accounts. In order to prevent this, + // we should keep some fields as "reserved". + // It is also recommended that their length is fixed, since + // it would allow easier proof integration (in case we will need + // some special circuit for preprocessing transactions). + uint256[4] reserved; + // The transaction's calldata. + bytes data; + // The signature of the transaction. + bytes signature; + // The properly formatted hashes of bytecodes that must be published on L1 + // with the inclusion of this transaction. Note, that a bytecode has been published + // before, the user won't pay fees for its republishing. + bytes32[] factoryDeps; + // The input to the paymaster. + bytes paymasterInput; + // Reserved dynamic type for the future use-case. Using it should be avoided, + // But it is still here, just in case we want to enable some additional functionality. + bytes reservedDynamic; +} + +/** + * @author Matter Labs + * @custom:security-contact security@matterlabs.dev + * @notice Library is used to help custom accounts to work with common methods for the Transaction type. + */ +library TransactionHelper { + using SafeERC20 for IERC20; + + /// @notice The EIP-712 typehash for the contract's domain + bytes32 internal constant EIP712_DOMAIN_TYPEHASH = + keccak256("EIP712Domain(string name,string version,uint256 chainId)"); + + bytes32 internal constant EIP712_TRANSACTION_TYPE_HASH = + keccak256( + "Transaction(uint256 txType,uint256 from,uint256 to,uint256 gasLimit,uint256 gasPerPubdataByteLimit,uint256 maxFeePerGas,uint256 maxPriorityFeePerGas,uint256 paymaster,uint256 nonce,uint256 value,bytes data,bytes32[] factoryDeps,bytes paymasterInput)" + ); + + /// @notice Whether the token is Ethereum. + /// @param _addr The address of the token + /// @return `true` or `false` based on whether the token is Ether. + /// @dev This method assumes that address is Ether either if the address is 0 (for convenience) + /// or if the address is the address of the L2BaseToken system contract. + function isEthToken(uint256 _addr) internal pure returns (bool) { + return _addr == uint256(uint160(address(BASE_TOKEN_SYSTEM_CONTRACT))) || _addr == 0; + } + + /// @notice Calculate the suggested signed hash of the transaction, + /// i.e. the hash that is signed by EOAs and is recommended to be signed by other accounts. + function encodeHash(Transaction calldata _transaction) internal view returns (bytes32 resultHash) { + if (_transaction.txType == LEGACY_TX_TYPE) { + resultHash = _encodeHashLegacyTransaction(_transaction); + } else if (_transaction.txType == EIP_712_TX_TYPE) { + resultHash = _encodeHashEIP712Transaction(_transaction); + } else if (_transaction.txType == EIP_1559_TX_TYPE) { + resultHash = _encodeHashEIP1559Transaction(_transaction); + } else if (_transaction.txType == EIP_2930_TX_TYPE) { + resultHash = _encodeHashEIP2930Transaction(_transaction); + } else { + // Currently no other transaction types are supported. + // Any new transaction types will be processed in a similar manner. + revert UnsupportedTxType(_transaction.txType); + } + } + + /// @notice Encode hash of the ZKsync native transaction type. + /// @return keccak256 hash of the EIP-712 encoded representation of transaction + function _encodeHashEIP712Transaction(Transaction calldata _transaction) private view returns (bytes32) { + bytes32 structHash = keccak256( + // solhint-disable-next-line func-named-parameters + abi.encode( + EIP712_TRANSACTION_TYPE_HASH, + _transaction.txType, + _transaction.from, + _transaction.to, + _transaction.gasLimit, + _transaction.gasPerPubdataByteLimit, + _transaction.maxFeePerGas, + _transaction.maxPriorityFeePerGas, + _transaction.paymaster, + _transaction.nonce, + _transaction.value, + EfficientCall.keccak(_transaction.data), + keccak256(abi.encodePacked(_transaction.factoryDeps)), + EfficientCall.keccak(_transaction.paymasterInput) + ) + ); + + bytes32 domainSeparator = keccak256( + abi.encode(EIP712_DOMAIN_TYPEHASH, keccak256("zkSync"), keccak256("2"), block.chainid) + ); + + return keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + } + + /// @notice Encode hash of the legacy transaction type. + /// @return keccak256 of the serialized RLP encoded representation of transaction + function _encodeHashLegacyTransaction(Transaction calldata _transaction) private view returns (bytes32) { + // Hash of legacy transactions are encoded as one of the: + // - RLP(nonce, gasPrice, gasLimit, to, value, data, chainId, 0, 0) + // - RLP(nonce, gasPrice, gasLimit, to, value, data) + // + // In this RLP encoding, only the first one above list appears, so we encode each element + // inside list and then concatenate the length of all elements with them. + + bytes memory encodedNonce = RLPEncoder.encodeUint256(_transaction.nonce); + // Encode `gasPrice` and `gasLimit` together to prevent "stack too deep error". + bytes memory encodedGasParam; + { + bytes memory encodedGasPrice = RLPEncoder.encodeUint256(_transaction.maxFeePerGas); + bytes memory encodedGasLimit = RLPEncoder.encodeUint256(_transaction.gasLimit); + encodedGasParam = bytes.concat(encodedGasPrice, encodedGasLimit); + } + + bytes memory encodedTo = RLPEncoder.encodeAddress(address(uint160(_transaction.to))); + bytes memory encodedValue = RLPEncoder.encodeUint256(_transaction.value); + // Encode only the length of the transaction data, and not the data itself, + // so as not to copy to memory a potentially huge transaction data twice. + bytes memory encodedDataLength; + { + // Safe cast, because the length of the transaction data can't be so large. + uint64 txDataLen = uint64(_transaction.data.length); + if (txDataLen != 1) { + // If the length is not equal to one, then only using the length can it be encoded definitely. + encodedDataLength = RLPEncoder.encodeNonSingleBytesLen(txDataLen); + } else if (_transaction.data[0] >= 0x80) { + // If input is a byte in [0x80, 0xff] range, RLP encoding will concatenates 0x81 with the byte. + encodedDataLength = hex"81"; + } + // Otherwise the length is not encoded at all. + } + + // Encode `chainId` according to EIP-155, but only if the `chainId` is specified in the transaction. + bytes memory encodedChainId; + if (_transaction.reserved[0] != 0) { + encodedChainId = bytes.concat(RLPEncoder.encodeUint256(block.chainid), hex"80_80"); + } + + bytes memory encodedListLength; + unchecked { + uint256 listLength = encodedNonce.length + + encodedGasParam.length + + encodedTo.length + + encodedValue.length + + encodedDataLength.length + + _transaction.data.length + + encodedChainId.length; + + // Safe cast, because the length of the list can't be so large. + encodedListLength = RLPEncoder.encodeListLen(uint64(listLength)); + } + + return + keccak256( + // solhint-disable-next-line func-named-parameters + bytes.concat( + encodedListLength, + encodedNonce, + encodedGasParam, + encodedTo, + encodedValue, + encodedDataLength, + _transaction.data, + encodedChainId + ) + ); + } + + /// @notice Encode hash of the EIP2930 transaction type. + /// @return keccak256 of the serialized RLP encoded representation of transaction + function _encodeHashEIP2930Transaction(Transaction calldata _transaction) private view returns (bytes32) { + // Hash of EIP2930 transactions is encoded the following way: + // H(0x01 || RLP(chain_id, nonce, gas_price, gas_limit, destination, amount, data, access_list)) + // + // Note, that on ZKsync access lists are not supported and should always be empty. + + // Encode all fixed-length params to avoid "stack too deep error" + bytes memory encodedFixedLengthParams; + { + bytes memory encodedChainId = RLPEncoder.encodeUint256(block.chainid); + bytes memory encodedNonce = RLPEncoder.encodeUint256(_transaction.nonce); + bytes memory encodedGasPrice = RLPEncoder.encodeUint256(_transaction.maxFeePerGas); + bytes memory encodedGasLimit = RLPEncoder.encodeUint256(_transaction.gasLimit); + bytes memory encodedTo = RLPEncoder.encodeAddress(address(uint160(_transaction.to))); + bytes memory encodedValue = RLPEncoder.encodeUint256(_transaction.value); + // solhint-disable-next-line func-named-parameters + encodedFixedLengthParams = bytes.concat( + encodedChainId, + encodedNonce, + encodedGasPrice, + encodedGasLimit, + encodedTo, + encodedValue + ); + } + + // Encode only the length of the transaction data, and not the data itself, + // so as not to copy to memory a potentially huge transaction data twice. + bytes memory encodedDataLength; + { + // Safe cast, because the length of the transaction data can't be so large. + uint64 txDataLen = uint64(_transaction.data.length); + if (txDataLen != 1) { + // If the length is not equal to one, then only using the length can it be encoded definitely. + encodedDataLength = RLPEncoder.encodeNonSingleBytesLen(txDataLen); + } else if (_transaction.data[0] >= 0x80) { + // If input is a byte in [0x80, 0xff] range, RLP encoding will concatenates 0x81 with the byte. + encodedDataLength = hex"81"; + } + // Otherwise the length is not encoded at all. + } + + // On ZKsync, access lists are always zero length (at least for now). + bytes memory encodedAccessListLength = RLPEncoder.encodeListLen(0); + + bytes memory encodedListLength; + unchecked { + uint256 listLength = encodedFixedLengthParams.length + + encodedDataLength.length + + _transaction.data.length + + encodedAccessListLength.length; + + // Safe cast, because the length of the list can't be so large. + encodedListLength = RLPEncoder.encodeListLen(uint64(listLength)); + } + + return + keccak256( + // solhint-disable-next-line func-named-parameters + bytes.concat( + "\x01", + encodedListLength, + encodedFixedLengthParams, + encodedDataLength, + _transaction.data, + encodedAccessListLength + ) + ); + } + + /// @notice Encode hash of the EIP1559 transaction type. + /// @return keccak256 of the serialized RLP encoded representation of transaction + function _encodeHashEIP1559Transaction(Transaction calldata _transaction) private view returns (bytes32) { + // Hash of EIP1559 transactions is encoded the following way: + // H(0x02 || RLP(chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, destination, amount, data, access_list)) + // + // Note, that on ZKsync access lists are not supported and should always be empty. + + // Encode all fixed-length params to avoid "stack too deep error" + bytes memory encodedFixedLengthParams; + { + bytes memory encodedChainId = RLPEncoder.encodeUint256(block.chainid); + bytes memory encodedNonce = RLPEncoder.encodeUint256(_transaction.nonce); + bytes memory encodedMaxPriorityFeePerGas = RLPEncoder.encodeUint256(_transaction.maxPriorityFeePerGas); + bytes memory encodedMaxFeePerGas = RLPEncoder.encodeUint256(_transaction.maxFeePerGas); + bytes memory encodedGasLimit = RLPEncoder.encodeUint256(_transaction.gasLimit); + bytes memory encodedTo = RLPEncoder.encodeAddress(address(uint160(_transaction.to))); + bytes memory encodedValue = RLPEncoder.encodeUint256(_transaction.value); + // solhint-disable-next-line func-named-parameters + encodedFixedLengthParams = bytes.concat( + encodedChainId, + encodedNonce, + encodedMaxPriorityFeePerGas, + encodedMaxFeePerGas, + encodedGasLimit, + encodedTo, + encodedValue + ); + } + + // Encode only the length of the transaction data, and not the data itself, + // so as not to copy to memory a potentially huge transaction data twice. + bytes memory encodedDataLength; + { + // Safe cast, because the length of the transaction data can't be so large. + uint64 txDataLen = uint64(_transaction.data.length); + if (txDataLen != 1) { + // If the length is not equal to one, then only using the length can it be encoded definitely. + encodedDataLength = RLPEncoder.encodeNonSingleBytesLen(txDataLen); + } else if (_transaction.data[0] >= 0x80) { + // If input is a byte in [0x80, 0xff] range, RLP encoding will concatenates 0x81 with the byte. + encodedDataLength = hex"81"; + } + // Otherwise the length is not encoded at all. + } + + // On ZKsync, access lists are always zero length (at least for now). + bytes memory encodedAccessListLength = RLPEncoder.encodeListLen(0); + + bytes memory encodedListLength; + unchecked { + uint256 listLength = encodedFixedLengthParams.length + + encodedDataLength.length + + _transaction.data.length + + encodedAccessListLength.length; + + // Safe cast, because the length of the list can't be so large. + encodedListLength = RLPEncoder.encodeListLen(uint64(listLength)); + } + + return + keccak256( + // solhint-disable-next-line func-named-parameters + bytes.concat( + "\x02", + encodedListLength, + encodedFixedLengthParams, + encodedDataLength, + _transaction.data, + encodedAccessListLength + ) + ); + } + + /// @notice Processes the common paymaster flows, e.g. setting proper allowance + /// for tokens, etc. For more information on the expected behavior, check out + /// the "Paymaster flows" section in the documentation. + function processPaymasterInput(Transaction calldata _transaction) internal { + if (_transaction.paymasterInput.length < 4) { + revert InvalidInput(); + } + + bytes4 paymasterInputSelector = bytes4(_transaction.paymasterInput[0:4]); + if (paymasterInputSelector == IPaymasterFlow.approvalBased.selector) { + if (_transaction.paymasterInput.length < 68) { + revert InvalidInput(); + } + + // While the actual data consists of address, uint256 and bytes data, + // the data is needed only for the paymaster, so we ignore it here for the sake of optimization + (address token, uint256 minAllowance) = abi.decode(_transaction.paymasterInput[4:68], (address, uint256)); + address paymaster = address(uint160(_transaction.paymaster)); + + uint256 currentAllowance = IERC20(token).allowance(address(this), paymaster); + if (currentAllowance < minAllowance) { + // Some tokens, e.g. USDT require that the allowance is firsty set to zero + // and only then updated to the new value. + + IERC20(token).safeApprove(paymaster, 0); + IERC20(token).safeApprove(paymaster, minAllowance); + } + } else if (paymasterInputSelector == IPaymasterFlow.general.selector) { + // Do nothing. general(bytes) paymaster flow means that the paymaster must interpret these bytes on his own. + } else { + revert UnsupportedPaymasterFlow(); + } + } + + /// @notice Pays the required fee for the transaction to the bootloader. + /// @dev Currently it pays the maximum amount "_transaction.maxFeePerGas * _transaction.gasLimit", + /// it will change in the future. + function payToTheBootloader(Transaction calldata _transaction) internal returns (bool success) { + address bootloaderAddr = BOOTLOADER_FORMAL_ADDRESS; + uint256 amount = _transaction.maxFeePerGas * _transaction.gasLimit; + + assembly { + success := call(gas(), bootloaderAddr, amount, 0, 0, 0, 0) + } + } + + // Returns the balance required to process the transaction. + function totalRequiredBalance(Transaction calldata _transaction) internal pure returns (uint256 requiredBalance) { + if (address(uint160(_transaction.paymaster)) != address(0)) { + // Paymaster pays for the fee + requiredBalance = _transaction.value; + } else { + // The user should have enough balance for both the fee and the value of the transaction + requiredBalance = _transaction.maxFeePerGas * _transaction.gasLimit + _transaction.value; + } + } +} diff --git a/contracts/system-contracts/libraries/UnsafeBytesCalldata.sol b/contracts/system-contracts/libraries/UnsafeBytesCalldata.sol new file mode 100644 index 0000000..82b4c5c --- /dev/null +++ b/contracts/system-contracts/libraries/UnsafeBytesCalldata.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.20; + +/** + * @author Matter Labs + * @custom:security-contact security@matterlabs.dev + * @dev The library provides a set of functions that help read data from calldata bytes. + * @dev Each of the functions accepts the `bytes calldata` and the offset where data should be read and returns a value of a certain type. + * + * @dev WARNING! + * 1) Functions don't check the length of the bytes array, so it can go out of bounds. + * The user of the library must check for bytes length before using any functions from the library! + * + * 2) Read variables are not cleaned up - https://docs.soliditylang.org/en/v0.8.16/internals/variable_cleanup.html. + * Using data in inline assembly can lead to unexpected behavior! + */ +library UnsafeBytesCalldata { + function readUint16(bytes calldata _bytes, uint256 _start) internal pure returns (uint16 result) { + assembly { + let offset := sub(_bytes.offset, 30) + result := calldataload(add(offset, _start)) + } + } + + function readUint32(bytes calldata _bytes, uint256 _start) internal pure returns (uint32 result) { + assembly { + let offset := sub(_bytes.offset, 28) + result := calldataload(add(offset, _start)) + } + } + + function readUint64(bytes calldata _bytes, uint256 _start) internal pure returns (uint64 result) { + assembly { + let offset := sub(_bytes.offset, 24) + result := calldataload(add(offset, _start)) + } + } + + function readBytes32(bytes calldata _bytes, uint256 _start) internal pure returns (bytes32 result) { + assembly { + result := calldataload(add(_bytes.offset, _start)) + } + } + + function readUint256(bytes calldata _bytes, uint256 _start) internal pure returns (uint256 result) { + assembly { + result := calldataload(add(_bytes.offset, _start)) + } + } +} diff --git a/contracts/system-contracts/libraries/Utils.sol b/contracts/system-contracts/libraries/Utils.sol new file mode 100644 index 0000000..fc23de9 --- /dev/null +++ b/contracts/system-contracts/libraries/Utils.sol @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: MIT +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.20; + +import {EfficientCall} from "./EfficientCall.sol"; +import {MalformedBytecode, BytecodeError, Overflow} from "../SystemContractErrors.sol"; + +/** + * @author Matter Labs + * @custom:security-contact security@matterlabs.dev + * @dev Common utilities used in ZKsync system contracts + */ +library Utils { + /// @dev Bit mask of bytecode hash "isConstructor" marker + bytes32 internal constant IS_CONSTRUCTOR_BYTECODE_HASH_BIT_MASK = + 0x00ff000000000000000000000000000000000000000000000000000000000000; + + /// @dev Bit mask to set the "isConstructor" marker in the bytecode hash + bytes32 internal constant SET_IS_CONSTRUCTOR_MARKER_BIT_MASK = + 0x0001000000000000000000000000000000000000000000000000000000000000; + + function safeCastToU128(uint256 _x) internal pure returns (uint128) { + if (_x > type(uint128).max) { + revert Overflow(); + } + + return uint128(_x); + } + + function safeCastToU32(uint256 _x) internal pure returns (uint32) { + if (_x > type(uint32).max) { + revert Overflow(); + } + + return uint32(_x); + } + + function safeCastToU24(uint256 _x) internal pure returns (uint24) { + if (_x > type(uint24).max) { + revert Overflow(); + } + + return uint24(_x); + } + + /// @return codeLength The bytecode length in bytes + function bytecodeLenInBytes(bytes32 _bytecodeHash) internal pure returns (uint256 codeLength) { + codeLength = bytecodeLenInWords(_bytecodeHash) << 5; // _bytecodeHash * 32 + } + + /// @return codeLengthInWords The bytecode length in machine words + function bytecodeLenInWords(bytes32 _bytecodeHash) internal pure returns (uint256 codeLengthInWords) { + unchecked { + codeLengthInWords = uint256(uint8(_bytecodeHash[2])) * 256 + uint256(uint8(_bytecodeHash[3])); + } + } + + /// @notice Denotes whether bytecode hash corresponds to a contract that already constructed + function isContractConstructed(bytes32 _bytecodeHash) internal pure returns (bool) { + return _bytecodeHash[1] == 0x00; + } + + /// @notice Denotes whether bytecode hash corresponds to a contract that is on constructor or has already been constructed + function isContractConstructing(bytes32 _bytecodeHash) internal pure returns (bool) { + return _bytecodeHash[1] == 0x01; + } + + /// @notice Sets "isConstructor" flag to TRUE for the bytecode hash + /// @param _bytecodeHash The bytecode hash for which it is needed to set the constructing flag + /// @return The bytecode hash with "isConstructor" flag set to TRUE + function constructingBytecodeHash(bytes32 _bytecodeHash) internal pure returns (bytes32) { + // Clear the "isConstructor" marker and set it to 0x01. + return constructedBytecodeHash(_bytecodeHash) | SET_IS_CONSTRUCTOR_MARKER_BIT_MASK; + } + + /// @notice Sets "isConstructor" flag to FALSE for the bytecode hash + /// @param _bytecodeHash The bytecode hash for which it is needed to set the constructing flag + /// @return The bytecode hash with "isConstructor" flag set to FALSE + function constructedBytecodeHash(bytes32 _bytecodeHash) internal pure returns (bytes32) { + return _bytecodeHash & ~IS_CONSTRUCTOR_BYTECODE_HASH_BIT_MASK; + } + + /// @notice Validate the bytecode format and calculate its hash. + /// @param _bytecode The bytecode to hash. + /// @return hashedBytecode The 32-byte hash of the bytecode. + /// Note: The function reverts the execution if the bytecode has non expected format: + /// - Bytecode bytes length is not a multiple of 32 + /// - Bytecode bytes length is not less than 2^21 bytes (2^16 words) + /// - Bytecode words length is not odd + function hashL2Bytecode(bytes calldata _bytecode) internal view returns (bytes32 hashedBytecode) { + // Note that the length of the bytecode must be provided in 32-byte words. + if (_bytecode.length % 32 != 0) { + revert MalformedBytecode(BytecodeError.Length); + } + + uint256 lengthInWords = _bytecode.length / 32; + // bytecode length must be less than 2^16 words + if (lengthInWords >= 2 ** 16) { + revert MalformedBytecode(BytecodeError.NumberOfWords); + } + // bytecode length in words must be odd + if (lengthInWords % 2 == 0) { + revert MalformedBytecode(BytecodeError.WordsMustBeOdd); + } + hashedBytecode = + EfficientCall.sha(_bytecode) & + 0x00000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; + // Setting the version of the hash + hashedBytecode = (hashedBytecode | bytes32(uint256(1 << 248))); + // Setting the length + hashedBytecode = hashedBytecode | bytes32(lengthInWords << 224); + } +} diff --git a/contracts/system-contracts/openzeppelin/token/ERC20/IERC20.sol b/contracts/system-contracts/openzeppelin/token/ERC20/IERC20.sol new file mode 100644 index 0000000..18b39a7 --- /dev/null +++ b/contracts/system-contracts/openzeppelin/token/ERC20/IERC20.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.6.0) (token/ERC20/IERC20.sol) +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.0; + +/** + * @dev Interface of the ERC20 standard as defined in the EIP. + */ +interface IERC20 { + /** + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approve}. `value` is the new allowance. + */ + event Approval(address indexed owner, address indexed spender, uint256 value); + + /** + * @dev Returns the amount of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the amount of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Moves `amount` tokens from the caller's account to `to`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address to, uint256 amount) external returns (bool); + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowance(address owner, address spender) external view returns (uint256); + + /** + * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 amount) external returns (bool); + + /** + * @dev Moves `amount` tokens from `from` to `to` using the + * allowance mechanism. `amount` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom( + address from, + address to, + uint256 amount + ) external returns (bool); +} diff --git a/contracts/system-contracts/openzeppelin/token/ERC20/extensions/IERC20Permit.sol b/contracts/system-contracts/openzeppelin/token/ERC20/extensions/IERC20Permit.sol new file mode 100644 index 0000000..5e08754 --- /dev/null +++ b/contracts/system-contracts/openzeppelin/token/ERC20/extensions/IERC20Permit.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (token/ERC20/extensions/IERC20Permit.sol) +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.0; + +/** + * @dev Interface of the ERC20 Permit extension allowing approvals to be made via signatures, as defined in + * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. + * + * Adds the {permit} method, which can be used to change an account's ERC20 allowance (see {IERC20-allowance}) by + * presenting a message signed by the account. By not relying on {IERC20-approve}, the token holder account doesn't + * need to send a transaction, and thus is not required to hold Ether at all. + */ +interface IERC20Permit { + /** + * @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens, + * given ``owner``'s signed approval. + * + * IMPORTANT: The same issues {IERC20-approve} has related to transaction + * ordering also apply here. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `deadline` must be a timestamp in the future. + * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner` + * over the EIP712-formatted function arguments. + * - the signature must use ``owner``'s current nonce (see {nonces}). + * + * For more information on the signature format, see the + * https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP + * section]. + */ + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; + + /** + * @dev Returns the current nonce for `owner`. This value must be + * included whenever a signature is generated for {permit}. + * + * Every successful call to {permit} increases ``owner``'s nonce by one. This + * prevents a signature from being used multiple times. + */ + function nonces(address owner) external view returns (uint256); + + /** + * @dev Returns the domain separator used in the encoding of the signature for {permit}, as defined by {EIP712}. + */ + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view returns (bytes32); +} diff --git a/contracts/system-contracts/openzeppelin/token/ERC20/utils/SafeERC20.sol b/contracts/system-contracts/openzeppelin/token/ERC20/utils/SafeERC20.sol new file mode 100644 index 0000000..a23e6d1 --- /dev/null +++ b/contracts/system-contracts/openzeppelin/token/ERC20/utils/SafeERC20.sol @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC20/utils/SafeERC20.sol) +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.0; + +import {IERC20} from "../IERC20.sol"; +import {IERC20Permit} from "../extensions/IERC20Permit.sol"; +import {Address} from "../../../utils/Address.sol"; + +/** + * @title SafeERC20 + * @dev Wrappers around ERC20 operations that throw on failure (when the token + * contract returns false). Tokens that return no value (and instead revert or + * throw on failure) are also supported, non-reverting calls are assumed to be + * successful. + * To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract, + * which allows you to call the safe operations as `token.safeTransfer(...)`, etc. + */ +library SafeERC20 { + using Address for address; + + function safeTransfer( + IERC20 token, + address to, + uint256 value + ) internal { + _callOptionalReturn( + token, + abi.encodeWithSelector(token.transfer.selector, to, value) + ); + } + + function safeTransferFrom( + IERC20 token, + address from, + address to, + uint256 value + ) internal { + _callOptionalReturn( + token, + abi.encodeWithSelector(token.transferFrom.selector, from, to, value) + ); + } + + /** + * @dev Deprecated. This function has issues similar to the ones found in + * {IERC20-approve}, and its usage is discouraged. + * + * Whenever possible, use {safeIncreaseAllowance} and + * {safeDecreaseAllowance} instead. + */ + function safeApprove( + IERC20 token, + address spender, + uint256 value + ) internal { + // safeApprove should only be called when setting an initial allowance, + // or when resetting it to zero. To increase and decrease it, use + // 'safeIncreaseAllowance' and 'safeDecreaseAllowance' + require( + (value == 0) || (token.allowance(address(this), spender) == 0), + "SafeERC20: approve from non-zero to non-zero allowance" + ); + _callOptionalReturn( + token, + abi.encodeWithSelector(token.approve.selector, spender, value) + ); + } + + function safeIncreaseAllowance( + IERC20 token, + address spender, + uint256 value + ) internal { + uint256 newAllowance = token.allowance(address(this), spender) + value; + _callOptionalReturn( + token, + abi.encodeWithSelector( + token.approve.selector, + spender, + newAllowance + ) + ); + } + + function safeDecreaseAllowance( + IERC20 token, + address spender, + uint256 value + ) internal { + unchecked { + uint256 oldAllowance = token.allowance(address(this), spender); + require( + oldAllowance >= value, + "SafeERC20: decreased allowance below zero" + ); + uint256 newAllowance = oldAllowance - value; + _callOptionalReturn( + token, + abi.encodeWithSelector( + token.approve.selector, + spender, + newAllowance + ) + ); + } + } + + function safePermit( + IERC20Permit token, + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) internal { + uint256 nonceBefore = token.nonces(owner); + token.permit({owner: owner, spender: spender, value: value, deadline : deadline, v: v, r: r, s: s}); + uint256 nonceAfter = token.nonces(owner); + require( + nonceAfter == nonceBefore + 1, + "SafeERC20: permit did not succeed" + ); + } + + /** + * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement + * on the return value: the return value is optional (but if data is returned, it must not be false). + * @param token The token targeted by the call. + * @param data The call data (encoded using abi.encode or one of its variants). + */ + function _callOptionalReturn(IERC20 token, bytes memory data) private { + // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since + // we're implementing it ourselves. We use {Address-functionCall} to perform this call, which verifies that + // the target address contains contract code and also asserts for success in the low-level call. + + bytes memory returndata = address(token).functionCall( + data, + "SafeERC20: low-level call failed" + ); + if (returndata.length > 0) { + // Return data is optional + require( + abi.decode(returndata, (bool)), + "SafeERC20: ERC20 operation did not succeed" + ); + } + } +} diff --git a/contracts/system-contracts/openzeppelin/utils/Address.sol b/contracts/system-contracts/openzeppelin/utils/Address.sol new file mode 100644 index 0000000..5d6de78 --- /dev/null +++ b/contracts/system-contracts/openzeppelin/utils/Address.sol @@ -0,0 +1,308 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.8.0) (utils/Address.sol) +// We use a floating point pragma here so it can be used within other projects that interact with the ZKsync ecosystem without using our exact pragma version. +pragma solidity ^0.8.1; + +/** + * @dev Collection of functions related to the address type + */ +library Address { + /** + * @dev Returns true if `account` is a contract. + * + * [IMPORTANT] + * ==== + * It is unsafe to assume that an address for which this function returns + * false is an externally-owned account (EOA) and not a contract. + * + * Among others, `isContract` will return false for the following + * types of addresses: + * + * - an externally-owned account + * - a contract in construction + * - an address where a contract will be created + * - an address where a contract lived, but was destroyed + * ==== + * + * [IMPORTANT] + * ==== + * You shouldn't rely on `isContract` to protect against flash loan attacks! + * + * Preventing calls from contracts is highly discouraged. It breaks composability, breaks support for smart wallets + * like Gnosis Safe, and does not provide security since it can be circumvented by calling from a contract + * constructor. + * ==== + */ + function isContract(address account) internal view returns (bool) { + // This method relies on extcodesize/address.code.length, which returns 0 + // for contracts in construction, since the code is only stored at the end + // of the constructor execution. + + return account.code.length > 0; + } + + /** + * @dev Replacement for Solidity's `transfer`: sends `amount` wei to + * `recipient`, forwarding all available gas and reverting on errors. + * + * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost + * of certain opcodes, possibly making contracts go over the 2300 gas limit + * imposed by `transfer`, making them unable to receive funds via + * `transfer`. {sendValue} removes this limitation. + * + * https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/[Learn more]. + * + * IMPORTANT: because control is transferred to `recipient`, care must be + * taken to not create reentrancy vulnerabilities. Consider using + * {ReentrancyGuard} or the + * https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. + */ + function sendValue(address payable recipient, uint256 amount) internal { + require( + address(this).balance >= amount, + "Address: insufficient balance" + ); + + (bool success, ) = recipient.call{value: amount}(""); + require( + success, + "Address: unable to send value, recipient may have reverted" + ); + } + + /** + * @dev Performs a Solidity function call using a low level `call`. A + * plain `call` is an unsafe replacement for a function call: use this + * function instead. + * + * If `target` reverts with a revert reason, it is bubbled up by this + * function (like regular Solidity function calls). + * + * Returns the raw returned data. To convert to the expected return value, + * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`]. + * + * Requirements: + * + * - `target` must be a contract. + * - calling `target` with `data` must not revert. + * + * _Available since v3.1._ + */ + function functionCall(address target, bytes memory data) + internal + returns (bytes memory) + { + return + functionCallWithValue( + target, + data, + 0, + "Address: low-level call failed" + ); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with + * `errorMessage` as a fallback revert reason when `target` reverts. + * + * _Available since v3.1._ + */ + function functionCall( + address target, + bytes memory data, + string memory errorMessage + ) internal returns (bytes memory) { + return functionCallWithValue(target, data, 0, errorMessage); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but also transferring `value` wei to `target`. + * + * Requirements: + * + * - the calling contract must have an ETH balance of at least `value`. + * - the called Solidity function must be `payable`. + * + * _Available since v3.1._ + */ + function functionCallWithValue( + address target, + bytes memory data, + uint256 value + ) internal returns (bytes memory) { + return + functionCallWithValue( + target, + data, + value, + "Address: low-level call with value failed" + ); + } + + /** + * @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but + * with `errorMessage` as a fallback revert reason when `target` reverts. + * + * _Available since v3.1._ + */ + function functionCallWithValue( + address target, + bytes memory data, + uint256 value, + string memory errorMessage + ) internal returns (bytes memory) { + require( + address(this).balance >= value, + "Address: insufficient balance for call" + ); + (bool success, bytes memory returndata) = target.call{value: value}( + data + ); + return + verifyCallResultFromTarget( + target, + success, + returndata, + errorMessage + ); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but performing a static call. + * + * _Available since v3.3._ + */ + function functionStaticCall(address target, bytes memory data) + internal + view + returns (bytes memory) + { + return + functionStaticCall( + target, + data, + "Address: low-level static call failed" + ); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], + * but performing a static call. + * + * _Available since v3.3._ + */ + function functionStaticCall( + address target, + bytes memory data, + string memory errorMessage + ) internal view returns (bytes memory) { + (bool success, bytes memory returndata) = target.staticcall(data); + return + verifyCallResultFromTarget( + target, + success, + returndata, + errorMessage + ); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but performing a delegate call. + * + * _Available since v3.4._ + */ + function functionDelegateCall(address target, bytes memory data) + internal + returns (bytes memory) + { + return + functionDelegateCall( + target, + data, + "Address: low-level delegate call failed" + ); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], + * but performing a delegate call. + * + * _Available since v3.4._ + */ + function functionDelegateCall( + address target, + bytes memory data, + string memory errorMessage + ) internal returns (bytes memory) { + (bool success, bytes memory returndata) = target.delegatecall(data); + return + verifyCallResultFromTarget( + target, + success, + returndata, + errorMessage + ); + } + + /** + * @dev Tool to verify that a low level call to smart-contract was successful, and revert (either by bubbling + * the revert reason or using the provided one) in case of unsuccessful call or if target was not a contract. + * + * _Available since v4.8._ + */ + function verifyCallResultFromTarget( + address target, + bool success, + bytes memory returndata, + string memory errorMessage + ) internal view returns (bytes memory) { + if (success) { + if (returndata.length == 0) { + // only check isContract if the call was successful and the return data is empty + // otherwise we already know that it was a contract + require(isContract(target), "Address: call to non-contract"); + } + return returndata; + } else { + _revert(returndata, errorMessage); + } + } + + /** + * @dev Tool to verify that a low level call was successful, and revert if it wasn't, either by bubbling the + * revert reason or using the provided one. + * + * _Available since v4.3._ + */ + function verifyCallResult( + bool success, + bytes memory returndata, + string memory errorMessage + ) internal pure returns (bytes memory) { + if (success) { + return returndata; + } else { + _revert(returndata, errorMessage); + } + } + + function _revert(bytes memory returndata, string memory errorMessage) + private + pure + { + // Look for revert reason and bubble it up if present + if (returndata.length > 0) { + // The easiest way to bubble the revert reason is using memory via assembly + /// @solidity memory-safe-assembly + assembly { + let returndata_size := mload(returndata) + revert(add(32, returndata), returndata_size) + } + } else { + revert(errorMessage); + } + } +} diff --git a/contracts/system-contracts/precompiles/CodeOracle.yul b/contracts/system-contracts/precompiles/CodeOracle.yul new file mode 100644 index 0000000..820b8df --- /dev/null +++ b/contracts/system-contracts/precompiles/CodeOracle.yul @@ -0,0 +1,147 @@ +/** + * @author Matter Labs + * @custom:security-contact security@matterlabs.dev + * @notice The contract used to emulate EVM's `extcodecopy` behavior. + */ +object "CodeOracle" { + code { + return(0, 0) + } + object "CodeOracle_deployed" { + code { + //////////////////////////////////////////////////////////////// + // CONSTANTS + //////////////////////////////////////////////////////////////// + + /// @notice The fixed address of the known code storage contract. + function KNOWN_CODES_CONTRACT_ADDR() -> ret { + ret := 0x0000000000000000000000000000000000008004 + } + + /// @notice The maximum value of the `uint32` type. + function UINT32_MAX() -> ret { + // 2^32 - 1 + ret := 4294967295 + } + + //////////////////////////////////////////////////////////////// + // HELPER FUNCTIONS + //////////////////////////////////////////////////////////////// + + /// @notice The function that returns whether a certain versioned hash is marked as `known` + /// @param versionedHash The versioned hash to check + /// @return Whether the versioned hash is known + function isCodeHashKnown(versionedHash) -> ret { + // 1. Selector for `KnownCodesStorage.getMarker(bytes32)` + mstore(0, 0x4c6314f000000000000000000000000000000000000000000000000000000000) + // 2. Input for `KnownCodesStorage.getMarker(bytes32)` + mstore(4, versionedHash) + + let success := staticcall( + gas(), + KNOWN_CODES_CONTRACT_ADDR(), + 0, + 36, + 0, + 32 + ) + + if iszero(success) { + // For some reason the call to the KnownCodesStorage failed. + // Most likely out of gas. + revert(0,0) + } + + ret := mload(0) + } + + /// @notice The cost for decommitment of a single 32-byte word. + function decommmitCostPerWord() -> ret { + ret := 4 + } + + /// @notice The function that performs that `decommit` operation, i.e. + /// given a versioned hash (i.e. `commitment` to some blob), it unpacks it + /// into the memory and returns it. + /// @param versionedHash The versioned hash to decommit. + /// @param lenInWords The length of the data in bytes to decommit. + function decommit(versionedHash, lenInWords) { + // The operation below are never expected to overflow since the `lenInWords` is at most 2 bytes long. + let gasCost := mul(decommmitCostPerWord(), lenInWords) + + // The cost of the decommit operation can not exceed the maximum value of the `uint32` type. + // This should never happen in practice, since `lenInWords` is an u16 value, but we add this check + // just in case. + if gt(gasCost, UINT32_MAX()) { + gasCost := UINT32_MAX() + } + + // We execute the `decommit` opcode that, given a versioned hash, unpacks the data into the memory. + // Note, that this memory does not necessarily have to be the memory of this contract. If an unpack + // has happened before, we will reuse the memory page where the first unpack happened. + // + // This means that we have to be careful with the memory management, since in case this memory page was the first + // one where the `decommit` happened, its memory page will be always used as a cache for this versioned hash, + // regardless of correctness. + let success := verbatim_2i_1o("decommit", versionedHash, gasCost) + if iszero(success) { + // Decommitment failed + revert(0,0) + } + + // The "real" result of the `decommit` operation is a pointer to the memory page where the data was unpacked. + // We do not know whether the data was unpacked into the memory of this contract or not. + // + // Also, for technical reasons we can not access pointers directly, so we have to copy the pointer returned by the + // decommit operation into the `active` pointer. + verbatim_0i_0o("decommit_ptr_to_active") + + // This operation is never expected to overflow since the `lenInWords` is at most 2 bytes long. + let lenInBytes := mul(lenInWords, 32) + + // To avoid the complexity of calculating the length of the preimage in circuits, the length of the pointer is always fixed to 2^21 bytes. + // So the amount of data actually copied is determined here. + // Note, that here we overwrite the first `lenInBytes` bytes of the memory, but it is fine since the written values are equivalent + // to the bytes previously written there by the `decommit` operation (in case this is the first page where the decommit happened). + // In the future we won't do this and simply return the pointer returned by the `decommit` operation, shrunk to the `lenInBytes` length. + verbatim_3i_0o("active_ptr_data_copy", 0, 0, lenInBytes) + + return(0, lenInBytes) + } + + //////////////////////////////////////////////////////////////// + // FALLBACK + //////////////////////////////////////////////////////////////// + + let versionedCodeHash := calldataload(0) + + // Can not decommit unknown code + if iszero(isCodeHashKnown(versionedCodeHash)) { + revert(0, 0) + } + + let version := shr(248, versionedCodeHash) + // Currently, only a single version of the code hash is supported: + // 1. The standard zkEVM bytecode. It has the following format: + // - hash[0] -- version (0x01) + // - hash[1] -- whether the contract is being constructed + // - hash[2..3] -- big endian length of the bytecode in 32-byte words. This number must be odd. + // - hash[4..31] -- the last 28 bytes of the sha256 hash. + // + // Note, that in theory it can represent just some random blob of bytes, while + // in practice it only represents only the corresponding bytecodes. + + switch version + case 1 { + // We do not double check whether it is odd, since it assumed that only valid bytecodes + // can pass the `isCodeHashKnown` check. + let lengthInWords := and(shr(224, versionedCodeHash), 0xffff) + decommit(versionedCodeHash, lengthInWords) + } + default { + // Unsupported + revert(0,0) + } + } + } +} diff --git a/contracts/system-contracts/precompiles/EcAdd.yul b/contracts/system-contracts/precompiles/EcAdd.yul new file mode 100644 index 0000000..5771df8 --- /dev/null +++ b/contracts/system-contracts/precompiles/EcAdd.yul @@ -0,0 +1,441 @@ +object "EcAdd" { + code { + return(0, 0) + } + object "EcAdd_deployed" { + code { + //////////////////////////////////////////////////////////////// + // CONSTANTS + //////////////////////////////////////////////////////////////// + + /// @notice Constant function for value three in Montgomery form. + /// @dev This value was precomputed using Python. + /// @return m_three The value three in Montgomery form. + function MONTGOMERY_THREE() -> m_three { + m_three := 19052624634359457937016868847204597229365286637454337178037183604060995791063 + } + + /// @notice Constant function for the alt_bn128 field order. + /// @dev See https://eips.ethereum.org/EIPS/eip-196 for further details. + /// @return ret The alt_bn128 field order. + function P() -> ret { + ret := 21888242871839275222246405745257275088696311157297823662689037894645226208583 + } + + /// @notice Constant function for the pre-computation of R^2 % N for the Montgomery REDC algorithm. + /// @dev R^2 is the Montgomery residue of the value 2^512. + /// @dev See https://en.wikipedia.org/wiki/Montgomery_modular_multiplication#The_REDC_algorithm for further details. + /// @dev This value was precomputed using Python. + /// @return ret The value R^2 modulus the curve field order. + function R2_MOD_P() -> ret { + ret := 3096616502983703923843567936837374451735540968419076528771170197431451843209 + } + + /// @notice Constant function for the pre-computation of N' for the Montgomery REDC algorithm. + /// @dev N' is a value such that NN' = -1 mod R, with N being the curve field order. + /// @dev See https://en.wikipedia.org/wiki/Montgomery_modular_multiplication#The_REDC_algorithm for further details. + /// @dev This value was precomputed using Python. + /// @return ret The value N'. + function N_PRIME() -> ret { + ret := 111032442853175714102588374283752698368366046808579839647964533820976443843465 + } + + ////////////////////////////////////////////////////////////////// + // HELPER FUNCTIONS + ////////////////////////////////////////////////////////////////// + + /// @dev Executes the `precompileCall` opcode. + function precompileCall(precompileParams, gasToBurn) -> ret { + // Compiler simulation for calling `precompileCall` opcode + ret := verbatim_2i_1o("precompile", precompileParams, gasToBurn) + } + + /// @notice Burns remaining gas until revert. + /// @dev This function is used to burn gas in the case of a failed precompile call. + function burnGas() { + // Precompiles that do not have a circuit counterpart + // will burn the provided gas by calling this function. + precompileCall(0, gas()) + } + + /// @notice Retrieves the highest half of the multiplication result. + /// @param multiplicand The value to multiply. + /// @param multiplier The multiplier. + /// @return ret The highest half of the multiplication result. + function getHighestHalfOfMultiplication(multiplicand, multiplier) -> ret { + ret := verbatim_2i_1o("mul_high", multiplicand, multiplier) + } + + /// @notice Computes the modular subtraction of two values. + /// @param minuend The value to subtract from. + /// @param subtrahend The value to subtract. + /// @param modulus The modulus. + /// @return difference The modular subtraction of the two values. + function submod(minuend, subtrahend, modulus) -> difference { + difference := addmod(minuend, sub(modulus, subtrahend), modulus) + } + + /// @notice Computes an addition and checks for overflow. + /// @param augend The value to add to. + /// @param addend The value to add. + /// @return sum The sum of the two values. + /// @return overflowed True if the addition overflowed, false otherwise. + function overflowingAdd(augend, addend) -> sum, overflowed { + sum := add(augend, addend) + overflowed := lt(sum, augend) + } + + // @notice Checks if a point is on the curve. + // @dev The curve in question is the alt_bn128 curve. + // @dev The Short Weierstrass equation of the curve is y^2 = x^3 + 3. + // @param x The x coordinate of the point in Montgomery form. + // @param y The y coordinate of the point in Montgomery form. + // @return ret True if the point is on the curve, false otherwise. + function pointIsInCurve(x, y) -> ret { + let ySquared := montgomeryMul(y, y) + let xSquared := montgomeryMul(x, x) + let xQubed := montgomeryMul(xSquared, x) + let xQubedPlusThree := montgomeryAdd(xQubed, MONTGOMERY_THREE()) + + ret := eq(ySquared, xQubedPlusThree) + } + + /// @notice Checks if a point is the point at infinity. + /// @dev The point at infinity is defined as the point (0, 0). + /// @dev See https://eips.ethereum.org/EIPS/eip-196 for further details. + /// @param x The x coordinate of the point. + /// @param y The y coordinate of the point. + /// @return ret True if the point is the point at infinity, false otherwise. + function isInfinity(x, y) -> ret { + ret := iszero(or(x, y)) + } + + /// @notice Checks if a coordinate is on the curve field order. + /// @dev A coordinate is on the curve field order if it is on the range [0, curveFieldOrder). + /// @dev This check is required in the precompile specification. See https://eips.ethereum.org/EIPS/eip-196 for further details. + /// @param coordinate The coordinate to check. + /// @return ret True if the coordinate is in the range, false otherwise. + function isOnFieldOrder(coordinate) -> ret { + ret := lt(coordinate, P()) + } + + /// @notice Computes the inverse in Montgomery Form of a number in Montgomery Form. + /// @dev Reference: https://github.com/lambdaclass/lambdaworks/blob/main/math/src/field/fields/montgomery_backed_prime_fields.rs#L169 + /// @dev Let `base` be a number in Montgomery Form, then base = a*R mod P() being `a` the base number (not in Montgomery Form) + /// @dev Let `inv` be the inverse of a number `a` in Montgomery Form, then inv = a^(-1)*R mod P() + /// @dev The original binary extended euclidean algorithms takes a number a and returns a^(-1) mod N + /// @dev In our case N is P(), and we'd like the input and output to be in Montgomery Form (a*R mod P() + /// @dev and a^(-1)*R mod P() respectively). + /// @dev If we just pass the input as a number in Montgomery Form the result would be a^(-1)*R^(-1) mod P(), + /// @dev but we want it to be a^(-1)*R mod P(). + /// @dev For that, we take advantage of the algorithm's linearity and multiply the result by R^2 mod P() + /// @dev to get R^2*a^(-1)*R^(-1) mod P() = a^(-1)*R mod P() as the desired result in Montgomery Form. + /// @dev `inv` takes the value of `b` or `c` being the result sometimes `b` and sometimes `c`. In paper + /// @dev multiplying `b` or `c` by R^2 mod P() results on starting their values as b = R2_MOD_P() and c = 0. + /// @param base A number `a` in Montgomery Form, then base = a*R mod P(). + /// @return inv The inverse of a number `a` in Montgomery Form, then inv = a^(-1)*R mod P(). + function binaryExtendedEuclideanAlgorithm(base) -> inv { + let modulus := P() + let u := base + let v := modulus + // Avoids unnecessary reduction step. + let b := R2_MOD_P() + let c := 0 + + for {} and(iszero(eq(u, 1)), iszero(eq(v, 1))) {} { + for {} iszero(and(u, 1)) {} { + u := shr(1, u) + let current := b + switch and(current, 1) + case 0 { + b := shr(1, b) + } + case 1 { + b := shr(1, add(b, modulus)) + } + } + + for {} iszero(and(v, 1)) {} { + v := shr(1, v) + let current := c + switch and(current, 1) + case 0 { + c := shr(1, c) + } + case 1 { + c := shr(1, add(c, modulus)) + } + } + + switch gt(v, u) + case 0 { + u := sub(u, v) + if lt(b, c) { + b := add(b, modulus) + } + b := sub(b, c) + } + case 1 { + v := sub(v, u) + if lt(c, b) { + c := add(c, modulus) + } + c := sub(c, b) + } + } + + switch eq(u, 1) + case 0 { + inv := c + } + case 1 { + inv := b + } + } + + /// @notice Implementation of the Montgomery reduction algorithm (a.k.a. REDC). + /// @dev See https://en.wikipedia.org/wiki/Montgomery_modular_multiplication#The_REDC_algorithm + /// @param lowestHalfOfT The lowest half of the value T. + /// @param higherHalfOfT The higher half of the value T. + /// @return S The result of the Montgomery reduction. + function REDC(lowestHalfOfT, higherHalfOfT) -> S { + let m := mul(lowestHalfOfT, N_PRIME()) + let hi := add(higherHalfOfT, getHighestHalfOfMultiplication(m, P())) + let lo, overflowed := overflowingAdd(lowestHalfOfT, mul(m, P())) + if overflowed { + hi := add(hi, 1) + } + S := hi + if iszero(lt(hi, P())) { + S := sub(hi, P()) + } + } + + /// @notice Encodes a field element into the Montgomery form using the Montgomery reduction algorithm (REDC). + /// @dev See https://en.wikipedia.org/wiki/Montgomery_modular_multiplication#The_REDC_algorithm for further details on transforming a field element into the Montgomery form. + /// @param a The field element to encode. + /// @return ret The field element in Montgomery form. + function intoMontgomeryForm(a) -> ret { + let hi := getHighestHalfOfMultiplication(a, R2_MOD_P()) + let lo := mul(a, R2_MOD_P()) + ret := REDC(lo, hi) + } + + /// @notice Decodes a field element out of the Montgomery form using the Montgomery reduction algorithm (REDC). + /// @dev See https://en.wikipedia.org/wiki/Montgomery_modular_multiplication#The_REDC_algorithm for further details on transforming a field element out of the Montgomery form. + /// @param m The field element in Montgomery form to decode. + /// @return ret The decoded field element. + function outOfMontgomeryForm(m) -> ret { + let hi := 0 + let lo := m + ret := REDC(lo, hi) + } + + /// @notice Computes the Montgomery addition. + /// @dev See https://en.wikipedia.org/wiki/Montgomery_modular_multiplication#The_REDC_algorithm for further details on the Montgomery multiplication. + /// @param augend The augend in Montgomery form. + /// @param addend The addend in Montgomery form. + /// @return ret The result of the Montgomery addition. + function montgomeryAdd(augend, addend) -> ret { + ret := add(augend, addend) + if iszero(lt(ret, P())) { + ret := sub(ret, P()) + } + } + + /// @notice Computes the Montgomery subtraction. + /// @dev See https://en.wikipedia.org/wiki/Montgomery_modular_multiplication#The_REDC_algorithm for further details on the Montgomery multiplication. + /// @param minuend The minuend in Montgomery form. + /// @param subtrahend The subtrahend in Montgomery form. + /// @return ret The result of the Montgomery subtraction. + function montgomerySub(minuend, subtrahend) -> ret { + ret := montgomeryAdd(minuend, sub(P(), subtrahend)) + } + + /// @notice Computes the Montgomery multiplication using the Montgomery reduction algorithm (REDC). + /// @dev See https://en.wikipedia.org/wiki/Montgomery_modular_multiplication#The_REDC_algorithm for further details on the Montgomery multiplication. + /// @param multiplicand The multiplicand in Montgomery form. + /// @param multiplier The multiplier in Montgomery form. + /// @return ret The result of the Montgomery multiplication. + function montgomeryMul(multiplicand, multiplier) -> ret { + let higherHalfOfProduct := getHighestHalfOfMultiplication(multiplicand, multiplier) + let lowestHalfOfProduct := mul(multiplicand, multiplier) + ret := REDC(lowestHalfOfProduct, higherHalfOfProduct) + } + + /// @notice Computes the Montgomery modular inverse skipping the Montgomery reduction step. + /// @dev The Montgomery reduction step is skipped because a modification in the binary extended Euclidean algorithm is used to compute the modular inverse. + /// @dev See the function `binaryExtendedEuclideanAlgorithm` for further details. + /// @param a The field element in Montgomery form to compute the modular inverse of. + /// @return invmod The result of the Montgomery modular inverse (in Montgomery form). + function montgomeryModularInverse(a) -> invmod { + invmod := binaryExtendedEuclideanAlgorithm(a) + } + + /// @notice Computes the Montgomery division. + /// @dev The Montgomery division is computed by multiplying the dividend with the modular inverse of the divisor. + /// @param dividend The dividend in Montgomery form. + /// @param divisor The divisor in Montgomery form. + /// @return quotient The result of the Montgomery division. + function montgomeryDiv(dividend, divisor) -> quotient { + quotient := montgomeryMul(dividend, montgomeryModularInverse(divisor)) + } + + //////////////////////////////////////////////////////////////// + // FALLBACK + //////////////////////////////////////////////////////////////// + + // Retrieve the coordinates from the calldata + let x1 := calldataload(0) + let y1 := calldataload(32) + let x2 := calldataload(64) + let y2 := calldataload(96) + + let p1IsInfinity := isInfinity(x1, y1) + let p2IsInfinity := isInfinity(x2, y2) + + if and(p1IsInfinity, p2IsInfinity) { + // Infinity + Infinity = Infinity + mstore(0, 0) + mstore(32, 0) + return(0, 64) + } + if and(p1IsInfinity, iszero(p2IsInfinity)) { + // Infinity + P = P + + // Ensure that the coordinates are between 0 and the field order. + if or(iszero(isOnFieldOrder(x2)), iszero(isOnFieldOrder(y2))) { + burnGas() + } + + let m_x2 := intoMontgomeryForm(x2) + let m_y2 := intoMontgomeryForm(y2) + + // Ensure that the point is in the curve (Y^2 = X^3 + 3). + if iszero(pointIsInCurve(m_x2, m_y2)) { + burnGas() + } + + // We just need to go into the Montgomery form to perform the + // computations in pointIsInCurve, but we do not need to come back. + + mstore(0, x2) + mstore(32, y2) + return(0, 64) + } + if and(iszero(p1IsInfinity), p2IsInfinity) { + // P + Infinity = P + + // Ensure that the coordinates are between 0 and the field order. + if or(iszero(isOnFieldOrder(x1)), iszero(isOnFieldOrder(y1))) { + burnGas() + } + + let m_x1 := intoMontgomeryForm(x1) + let m_y1 := intoMontgomeryForm(y1) + + // Ensure that the point is in the curve (Y^2 = X^3 + 3). + if iszero(pointIsInCurve(m_x1, m_y1)) { + burnGas() + } + + // We just need to go into the Montgomery form to perform the + // computations in pointIsInCurve, but we do not need to come back. + + mstore(0, x1) + mstore(32, y1) + return(0, 64) + } + + // Ensure that the coordinates are between 0 and the field order. + if or(iszero(isOnFieldOrder(x1)), iszero(isOnFieldOrder(y1))) { + burnGas() + } + + // Ensure that the coordinates are between 0 and the field order. + if or(iszero(isOnFieldOrder(x2)), iszero(isOnFieldOrder(y2))) { + burnGas() + } + + // There's no need for transforming into Montgomery form + // for this case. + if and(eq(x1, x2), eq(submod(0, y1, P()), y2)) { + // P + (-P) = Infinity + + let m_x1 := intoMontgomeryForm(x1) + let m_y1 := intoMontgomeryForm(y1) + let m_x2 := intoMontgomeryForm(x2) + let m_y2 := intoMontgomeryForm(y2) + + // Ensure that the points are in the curve (Y^2 = X^3 + 3). + if or(iszero(pointIsInCurve(m_x1, m_y1)), iszero(pointIsInCurve(m_x2, m_y2))) { + burnGas() + } + + // We just need to go into the Montgomery form to perform the + // computations in pointIsInCurve, but we do not need to come back. + + mstore(0, 0) + mstore(32, 0) + return(0, 64) + } + + if and(eq(x1, x2), and(iszero(eq(y1, y2)), iszero(eq(y1, submod(0, y2, P()))))) { + burnGas() + } + + if and(eq(x1, x2), eq(y1, y2)) { + // P + P = 2P + + let x := intoMontgomeryForm(x1) + let y := intoMontgomeryForm(y1) + + // Ensure that the points are in the curve (Y^2 = X^3 + 3). + if iszero(pointIsInCurve(x, y)) { + burnGas() + } + + // (3 * x1^2 + a) / (2 * y1) + let x1_squared := montgomeryMul(x, x) + let slope := montgomeryDiv(addmod(x1_squared, addmod(x1_squared, x1_squared, P()), P()), addmod(y, y, P())) + // x3 = slope^2 - 2 * x1 + let x3 := submod(montgomeryMul(slope, slope), addmod(x, x, P()), P()) + // y3 = slope * (x1 - x3) - y1 + let y3 := submod(montgomeryMul(slope, submod(x, x3, P())), y, P()) + + x3 := outOfMontgomeryForm(x3) + y3 := outOfMontgomeryForm(y3) + + mstore(0, x3) + mstore(32, y3) + return(0, 64) + } + + // P1 + P2 = P3 + + x1 := intoMontgomeryForm(x1) + y1 := intoMontgomeryForm(y1) + x2 := intoMontgomeryForm(x2) + y2 := intoMontgomeryForm(y2) + + // Ensure that the points are in the curve (Y^2 = X^3 + 3). + if or(iszero(pointIsInCurve(x1, y1)), iszero(pointIsInCurve(x2, y2))) { + burnGas() + } + + // (y2 - y1) / (x2 - x1) + let slope := montgomeryDiv(submod(y2, y1, P()), submod(x2, x1, P())) + // x3 = slope^2 - x1 - x2 + let x3 := submod(montgomeryMul(slope, slope), addmod(x1, x2, P()), P()) + // y3 = slope * (x1 - x3) - y1 + let y3 := submod(montgomeryMul(slope, submod(x1, x3, P())), y1, P()) + + x3 := outOfMontgomeryForm(x3) + y3 := outOfMontgomeryForm(y3) + + mstore(0, x3) + mstore(32, y3) + return(0, 64) + } + } +} diff --git a/contracts/system-contracts/precompiles/EcMul.yul b/contracts/system-contracts/precompiles/EcMul.yul new file mode 100644 index 0000000..84838ec --- /dev/null +++ b/contracts/system-contracts/precompiles/EcMul.yul @@ -0,0 +1,495 @@ +object "EcMul" { + code { + return(0, 0) + } + object "EcMul_deployed" { + code { + //////////////////////////////////////////////////////////////// + // CONSTANTS + //////////////////////////////////////////////////////////////// + + /// @notice Constant function for value one in Montgomery form. + /// @dev This value was precomputed using Python. + /// @return m_one The value one in Montgomery form. + function MONTGOMERY_ONE() -> m_one { + m_one := 6350874878119819312338956282401532409788428879151445726012394534686998597021 + } + + /// @notice Constant function for value three in Montgomery form. + /// @dev This value was precomputed using Python. + /// @return m_three The value three in Montgomery form. + function MONTGOMERY_THREE() -> m_three { + m_three := 19052624634359457937016868847204597229365286637454337178037183604060995791063 + } + + /// @notice Constant function for value 3*b (i.e. 9) in Montgomery form. + /// @dev This value was precomputed using Python. + /// @return m_b3 The value 9 in Montgomery form. + function MONTGOMERY_B3() -> m_b3 { + m_b3 := 13381388159399823366557795051099241510703237597767364208733475022892534956023 + } + + /// @notice Constant function for the alt_bn128 field order. + /// @dev See https://eips.ethereum.org/EIPS/eip-196 for further details. + /// @return ret The alt_bn128 field order. + function P() -> ret { + ret := 21888242871839275222246405745257275088696311157297823662689037894645226208583 + } + + /// @notice Constant function for the pre-computation of R^2 % N for the Montgomery REDC algorithm. + /// @dev R^2 is the Montgomery residue of the value 2^512. + /// @dev See https://en.wikipedia.org/wiki/Montgomery_modular_multiplication#The_REDC_algorithm for further details. + /// @dev This value was precomputed using Python. + /// @return ret The value R^2 modulus the curve field order. + function R2_MOD_P() -> ret { + ret := 3096616502983703923843567936837374451735540968419076528771170197431451843209 + } + + /// @notice Constant function for the pre-computation of N' for the Montgomery REDC algorithm. + /// @dev N' is a value such that NN' = -1 mod R, with N being the curve field order. + /// @dev See https://en.wikipedia.org/wiki/Montgomery_modular_multiplication#The_REDC_algorithm for further details. + /// @dev This value was precomputed using Python. + /// @return ret The value N'. + function N_PRIME() -> ret { + ret := 111032442853175714102588374283752698368366046808579839647964533820976443843465 + } + + // //////////////////////////////////////////////////////////////// + // HELPER FUNCTIONS + // //////////////////////////////////////////////////////////////// + + /// @dev Executes the `precompileCall` opcode. + function precompileCall(precompileParams, gasToBurn) -> ret { + // Compiler simulation for calling `precompileCall` opcode + ret := verbatim_2i_1o("precompile", precompileParams, gasToBurn) + } + + /// @notice Burns remaining gas until revert. + /// @dev This function is used to burn gas in the case of a failed precompile call. + function burnGas() { + // Precompiles that do not have a circuit counterpart + // will burn the provided gas by calling this function. + precompileCall(0, gas()) + } + + /// @notice Retrieves the highest half of the multiplication result. + /// @param multiplicand The value to multiply. + /// @param multiplier The multiplier. + /// @return ret The highest half of the multiplication result. + function getHighestHalfOfMultiplication(multiplicand, multiplier) -> ret { + ret := verbatim_2i_1o("mul_high", multiplicand, multiplier) + } + + /// @notice Computes an addition and checks for overflow. + /// @param augend The value to add to. + /// @param addend The value to add. + /// @return sum The sum of the two values. + /// @return overflowed True if the addition overflowed, false otherwise. + function overflowingAdd(augend, addend) -> sum, overflowed { + sum := add(augend, addend) + overflowed := lt(sum, augend) + } + + /// @notice Checks if the LSB of a number is 1. + /// @param x The number to check. + /// @return ret True if the LSB is 1, false otherwise. + function lsbIsOne(x) -> ret { + ret := and(x, 1) + } + + /// @notice Computes the inverse in Montgomery Form of a number in Montgomery Form. + /// @dev Reference: https://github.com/lambdaclass/lambdaworks/blob/main/math/src/field/fields/montgomery_backed_prime_fields.rs#L169 + /// @dev Let `base` be a number in Montgomery Form, then base = a*R mod P() being `a` the base number (not in Montgomery Form) + /// @dev Let `inv` be the inverse of a number `a` in Montgomery Form, then inv = a^(-1)*R mod P() + /// @dev The original binary extended euclidean algorithms takes a number a and returns a^(-1) mod N + /// @dev In our case N is P(), and we'd like the input and output to be in Montgomery Form (a*R mod P() + /// @dev and a^(-1)*R mod P() respectively). + /// @dev If we just pass the input as a number in Montgomery Form the result would be a^(-1)*R^(-1) mod P(), + /// @dev but we want it to be a^(-1)*R mod P(). + /// @dev For that, we take advantage of the algorithm's linearity and multiply the result by R^2 mod P() + /// @dev to get R^2*a^(-1)*R^(-1) mod P() = a^(-1)*R mod P() as the desired result in Montgomery Form. + /// @dev `inv` takes the value of `b` or `c` being the result sometimes `b` and sometimes `c`. In paper + /// @dev multiplying `b` or `c` by R^2 mod P() results on starting their values as b = R2_MOD_P() and c = 0. + /// @param base A number `a` in Montgomery Form, then base = a*R mod P(). + /// @return inv The inverse of a number `a` in Montgomery Form, then inv = a^(-1)*R mod P(). + function binaryExtendedEuclideanAlgorithm(base) -> inv { + let modulus := P() + let u := base + let v := modulus + // Avoids unnecessary reduction step. + let b := R2_MOD_P() + let c := 0 + + for {} and(iszero(eq(u, 1)), iszero(eq(v, 1))) {} { + for {} iszero(and(u, 1)) {} { + u := shr(1, u) + let current := b + switch and(current, 1) + case 0 { + b := shr(1, b) + } + case 1 { + b := shr(1, add(b, modulus)) + } + } + + for {} iszero(and(v, 1)) {} { + v := shr(1, v) + let current := c + switch and(current, 1) + case 0 { + c := shr(1, c) + } + case 1 { + c := shr(1, add(c, modulus)) + } + } + + switch gt(v, u) + case 0 { + u := sub(u, v) + if lt(b, c) { + b := add(b, modulus) + } + b := sub(b, c) + } + case 1 { + v := sub(v, u) + if lt(c, b) { + c := add(c, modulus) + } + c := sub(c, b) + } + } + + switch eq(u, 1) + case 0 { + inv := c + } + case 1 { + inv := b + } + } + + /// @notice Implementation of the Montgomery reduction algorithm (a.k.a. REDC). + /// @dev See https://en.wikipedia.org/wiki/Montgomery_modular_multiplication#The_The_REDC_algorithm + /// @param lowestHalfOfT The lowest half of the value T. + /// @param higherHalfOfT The higher half of the value T. + /// @return S The result of the Montgomery reduction. + function REDC(lowestHalfOfT, higherHalfOfT) -> S { + let m := mul(lowestHalfOfT, N_PRIME()) + let hi := add(higherHalfOfT, getHighestHalfOfMultiplication(m, P())) + let lo, overflowed := overflowingAdd(lowestHalfOfT, mul(m, P())) + if overflowed { + hi := add(hi, 1) + } + S := hi + if iszero(lt(hi, P())) { + S := sub(hi, P()) + } + } + + /// @notice Encodes a field element into the Montgomery form using the Montgomery reduction algorithm (REDC). + /// @dev See https://en.wikipedia.org/wiki/Montgomery_modular_multiplication#The_The_REDC_algorithm for further details on transforming a field element into the Montgomery form. + /// @param a The field element to encode. + /// @return ret The field element in Montgomery form. + function intoMontgomeryForm(a) -> ret { + let hi := getHighestHalfOfMultiplication(a, R2_MOD_P()) + let lo := mul(a, R2_MOD_P()) + ret := REDC(lo, hi) + } + + /// @notice Decodes a field element out of the Montgomery form using the Montgomery reduction algorithm (REDC). + /// @dev See https://en.wikipedia.org/wiki/Montgomery_modular_multiplication#The_The_REDC_algorithm for further details on transforming a field element out of the Montgomery form. + /// @param m The field element in Montgomery form to decode. + /// @return ret The decoded field element. + function outOfMontgomeryForm(m) -> ret { + let hi := 0 + let lo := m + ret := REDC(lo, hi) + } + + /// @notice Computes the Montgomery addition. + /// @dev See https://en.wikipedia.org/wiki/Montgomery_modular_multiplication#The_The_REDC_algorithm for further details on the Montgomery multiplication. + /// @param augend The augend in Montgomery form. + /// @param addend The addend in Montgomery form. + /// @return ret The result of the Montgomery addition. + function montgomeryAdd(augend, addend) -> ret { + ret := add(augend, addend) + if iszero(lt(ret, P())) { + ret := sub(ret, P()) + } + } + + /// @notice Computes the Montgomery subtraction. + /// @dev See https://en.wikipedia.org/wiki/Montgomery_modular_multiplication#The_The_REDC_algorithm for further details on the Montgomery multiplication. + /// @param minuend The minuend in Montgomery form. + /// @param subtrahend The subtrahend in Montgomery form. + /// @return ret The result of the Montgomery subtraction. + function montgomerySub(minuend, subtrahend) -> ret { + ret := montgomeryAdd(minuend, sub(P(), subtrahend)) + } + + /// @notice Computes the Montgomery multiplication using the Montgomery reduction algorithm (REDC). + /// @dev See https://en.wikipedia.org/wiki/Montgomery_modular_multiplication#The_The_REDC_algorithm for further details on the Montgomery multiplication. + /// @param multiplicand The multiplicand in Montgomery form. + /// @param multiplier The multiplier in Montgomery form. + /// @return ret The result of the Montgomery multiplication. + function montgomeryMul(multiplicand, multiplier) -> ret { + let hi := getHighestHalfOfMultiplication(multiplicand, multiplier) + let lo := mul(multiplicand, multiplier) + ret := REDC(lo, hi) + } + + /// @notice Computes the Montgomery modular inverse skipping the Montgomery reduction step. + /// @dev The Montgomery reduction step is skipped because a modification in the binary extended Euclidean algorithm is used to compute the modular inverse. + /// @dev See the function `binaryExtendedEuclideanAlgorithm` for further details. + /// @param a The field element in Montgomery form to compute the modular inverse of. + /// @return invmod The result of the Montgomery modular inverse (in Montgomery form). + function montgomeryModularInverse(a) -> invmod { + invmod := binaryExtendedEuclideanAlgorithm(a) + } + + /// @notice Checks if a coordinate is on the curve field order. + /// @dev A coordinate is on the curve field order if it is on the range [0, curveFieldOrder). + /// @param coordinate The coordinate to check. + /// @return ret True if the coordinate is in the range, false otherwise. + function coordinateIsOnFieldOrder(coordinate) -> ret { + ret := lt(coordinate, P()) + } + + /// @notice Checks if affine coordinates are on the curve field order. + /// @dev Affine coordinates are on the curve field order if both coordinates are on the range [0, curveFieldOrder). + /// @param x The x coordinate to check. + /// @param y The y coordinate to check. + /// @return ret True if the coordinates are in the range, false otherwise. + function affinePointCoordinatesAreOnFieldOrder(x, y) -> ret { + ret := and(coordinateIsOnFieldOrder(x), coordinateIsOnFieldOrder(y)) + } + + /// @notice Checks if projective coordinates are on the curve field order. + /// @dev Projective coordinates are on the curve field order if the coordinates are on the range [0, curveFieldOrder) and the z coordinate is not zero. + /// @param x The x coordinate to check. + /// @param y The y coordinate to check. + /// @param z The z coordinate to check. + /// @return ret True if the coordinates are in the range, false otherwise. + function projectivePointCoordinatesAreOnFieldOrder(x, y, z) -> ret { + let _x, _y := projectiveIntoAffine(x, y, z) + ret := and(z, affinePointCoordinatesAreOnFieldOrder(_x, _y)) + } + + // @notice Checks if a point in affine coordinates in Montgomery form is on the curve. + // @dev The curve in question is the alt_bn128 curve. + // @dev The Short Weierstrass equation of the curve is y^2 = x^3 + 3. + // @param x The x coordinate of the point in Montgomery form. + // @param y The y coordinate of the point in Montgomery form. + // @return ret True if the point is on the curve, false otherwise. + function affinePointIsOnCurve(x, y) -> ret { + let ySquared := montgomeryMul(y, y) + let xSquared := montgomeryMul(x, x) + let xQubed := montgomeryMul(xSquared, x) + let xQubedPlusThree := montgomeryAdd(xQubed, MONTGOMERY_THREE()) + + ret := eq(ySquared, xQubedPlusThree) + } + + /// @notice Checks if a point in affine coordinates is the point at infinity. + /// @dev The point at infinity is defined as the point (0, 0). + /// @dev See https://eips.ethereum.org/EIPS/eip-196 for further details. + /// @param x The x coordinate of the point in Montgomery form. + /// @param y The y coordinate of the point in Montgomery form. + /// @return ret True if the point is the point at infinity, false otherwise. + function affinePointIsInfinity(x, y) -> ret { + ret := and(iszero(x), iszero(y)) + } + + /// @notice Checks if a point in projective coordinates in Montgomery form is the point at infinity. + /// @dev The point at infinity is defined as the point (0, 0, 0). + /// @param x The x coordinate of the point in Montgomery form. + /// @param y The y coordinate of the point in Montgomery form. + /// @param z The z coordinate of the point in Montgomery form. + /// @return ret True if the point is the point at infinity, false otherwise. + function projectivePointIsInfinity(x, y, z) -> ret { + ret := iszero(z) + } + + /// @notice Converts a point in affine coordinates to projective coordinates in Montgomery form. + /// @dev The point at infinity is defined as the point (0, 0, 0). + /// @dev For performance reasons, the point is assumed to be previously checked to be on the + /// @dev curve and not the point at infinity. + /// @param xp The x coordinate of the point P in affine coordinates in Montgomery form. + /// @param yp The y coordinate of the point P in affine coordinates in Montgomery form. + /// @return xr The x coordinate of the point P in projective coordinates in Montgomery form. + /// @return yr The y coordinate of the point P in projective coordinates in Montgomery form. + /// @return zr The z coordinate of the point P in projective coordinates in Montgomery form. + function projectiveFromAffine(xp, yp) -> xr, yr, zr { + xr := xp + yr := yp + zr := MONTGOMERY_ONE() + } + + /// @notice Converts a point in projective coordinates to affine coordinates in Montgomery form. + /// @dev See https://www.nayuki.io/page/elliptic-curve-point-addition-in-projective-coordinates for further details. + /// @dev Reverts if the point is not on the curve. + /// @param xp The x coordinate of the point P in projective coordinates in Montgomery form. + /// @param yp The y coordinate of the point P in projective coordinates in Montgomery form. + /// @param zp The z coordinate of the point P in projective coordinates in Montgomery form. + /// @return xr The x coordinate of the point P in affine coordinates in Montgomery form. + /// @return yr The y coordinate of the point P in affine coordinates in Montgomery form. + function projectiveIntoAffine(xp, yp, zp) -> xr, yr { + if zp { + let zp_inv := montgomeryModularInverse(zp) + xr := montgomeryMul(xp, zp_inv) + yr := montgomeryMul(yp, zp_inv) + } + } + + /// @notice Doubles a point in projective coordinates in Montgomery form. + /// @dev See Algorithm 9 in https://eprint.iacr.org/2015/1060.pdf for further details. + /// @dev The point is assumed to be on the curve. + /// @dev It works correctly for the point at infinity. + /// @param xp The x coordinate of the point P in projective coordinates in Montgomery form. + /// @param yp The y coordinate of the point P in projective coordinates in Montgomery form. + /// @param zp The z coordinate of the point P in projective coordinates in Montgomery form. + /// @return xr The x coordinate of the point 2P in projective coordinates in Montgomery form. + /// @return yr The y coordinate of the point 2P in projective coordinates in Montgomery form. + /// @return zr The z coordinate of the point 2P in projective coordinates in Montgomery form. + function projectiveDouble(xp, yp, zp) -> xr, yr, zr { + let t0 := montgomeryMul(yp, yp) + zr := montgomeryAdd(t0, t0) + zr := montgomeryAdd(zr, zr) + zr := montgomeryAdd(zr, zr) + let t1 := montgomeryMul(yp, zp) + let t2 := montgomeryMul(zp, zp) + t2 := montgomeryMul(MONTGOMERY_B3(), t2) + xr := montgomeryMul(t2, zr) + yr := montgomeryAdd(t0, t2) + zr := montgomeryMul(t1, zr) + t1 := montgomeryAdd(t2, t2) + t2 := montgomeryAdd(t1, t2) + t0 := montgomerySub(t0, t2) + yr := montgomeryMul(t0, yr) + yr := montgomeryAdd(xr, yr) + t1 := montgomeryMul(xp, yp) + xr := montgomeryMul(t0, t1) + xr := montgomeryAdd(xr, xr) + } + + //////////////////////////////////////////////////////////////// + // FALLBACK + //////////////////////////////////////////////////////////////// + + // Retrieve the coordinates from the calldata + let x := calldataload(0) + let y := calldataload(32) + if iszero(affinePointCoordinatesAreOnFieldOrder(x, y)) { + burnGas() + } + let scalar := calldataload(64) + + if affinePointIsInfinity(x, y) { + // Infinity * scalar = Infinity + return(0x00, 0x40) + } + + let m_x := intoMontgomeryForm(x) + let m_y := intoMontgomeryForm(y) + + // Ensure that the point is in the curve (Y^2 = X^3 + 3). + if iszero(affinePointIsOnCurve(m_x, m_y)) { + burnGas() + } + + if eq(scalar, 0) { + // P * 0 = Infinity + return(0x00, 0x40) + } + if eq(scalar, 1) { + // P * 1 = P + mstore(0x00, x) + mstore(0x20, y) + return(0x00, 0x40) + } + + let xp, yp, zp := projectiveFromAffine(m_x, m_y) + + if eq(scalar, 2) { + let xr, yr, zr := projectiveDouble(xp, yp, zp) + + xr, yr := projectiveIntoAffine(xr, yr, zr) + xr := outOfMontgomeryForm(xr) + yr := outOfMontgomeryForm(yr) + + mstore(0x00, xr) + mstore(0x20, yr) + return(0x00, 0x40) + } + + let xq := xp + let yq := yp + let zq := zp + let xr := 0 + let yr := MONTGOMERY_ONE() + let zr := 0 + for {} scalar {} { + if lsbIsOne(scalar) { + let rIsInfinity := projectivePointIsInfinity(xr, yr, zr) + + if rIsInfinity { + // Infinity + P = P + xr := xq + yr := yq + zr := zq + + xq, yq, zq := projectiveDouble(xq, yq, zq) + // Check next bit + scalar := shr(1, scalar) + continue + } + + let t0 := montgomeryMul(yq, zr) + let t1 := montgomeryMul(yr, zq) + let t := montgomerySub(t0, t1) + let u0 := montgomeryMul(xq, zr) + let u1 := montgomeryMul(xr, zq) + let u := montgomerySub(u0, u1) + + // t = (yq*zr - yr*zq); u = (xq*zr - xr*zq) + if iszero(or(t, u)) { + // P + P = 2P + xr, yr, zr := projectiveDouble(xr, yr, zr) + + xq := xr + yq := yr + zq := zr + // Check next bit + scalar := shr(1, scalar) + continue + } + + // P1 + P2 = P3 + let u2 := montgomeryMul(u, u) + let u3 := montgomeryMul(u2, u) + let v := montgomeryMul(zq, zr) + let w := montgomerySub(montgomeryMul(montgomeryMul(t, t), v), montgomeryMul(u2, montgomeryAdd(u0, u1))) + + xr := montgomeryMul(u, w) + yr := montgomerySub(montgomeryMul(t, montgomerySub(montgomeryMul(u0, u2), w)), montgomeryMul(t0, u3)) + zr := montgomeryMul(u3, v) + } + + xq, yq, zq := projectiveDouble(xq, yq, zq) + // Check next bit + scalar := shr(1, scalar) + } + + xr, yr := projectiveIntoAffine(xr, yr, zr) + xr := outOfMontgomeryForm(xr) + yr := outOfMontgomeryForm(yr) + + mstore(0, xr) + mstore(32, yr) + return(0, 64) + } + } +} diff --git a/contracts/system-contracts/precompiles/EcPairing.yul b/contracts/system-contracts/precompiles/EcPairing.yul new file mode 100644 index 0000000..6ea6e92 --- /dev/null +++ b/contracts/system-contracts/precompiles/EcPairing.yul @@ -0,0 +1,1745 @@ +object "EcPairing" { + code { + return(0, 0) + } + object "EcPairing_deployed" { + code { + // CONSTANTS + + /// @notice Constant function for value one in Montgomery form. + /// @dev This value was precomputed using Python. + /// @return m_one The value one in Montgomery form. + function MONTGOMERY_ONE() -> m_one { + m_one := 6350874878119819312338956282401532409788428879151445726012394534686998597021 + } + + /// @notice Constant function for value three in Montgomery form. + /// @dev This value was precomputed using Python. + /// @return m_three The value three in Montgomery form. + function MONTGOMERY_THREE() -> m_three { + m_three := 19052624634359457937016868847204597229365286637454337178037183604060995791063 + } + + /// @notice Constant function for the inverse of two on the alt_bn128 group in Montgomery form. + /// @dev This value was precomputed using Python. + /// @return two_inv The value of the inverse of two on the alt_bn128 group in Montgomery form. + function MONTGOMERY_TWO_INV() -> two_inv { + two_inv := 14119558874979547267292681013829403749242370018224634694350716214666112402802 + } + /// @notice constant function for the coeffitients of the sextic twist of the BN256 curve. + /// @dev E': y' ** 2 = x' ** 3 + 3 / (9 + u) + /// @dev the curve E' is defined over Fp2 elements. + /// @dev See https://hackmd.io/@jpw/bn254#Twists for further details. + /// @return coefficients of the sextic twist of the BN256 curve + function MONTGOMERY_TWISTED_CURVE_COEFFS() -> z0, z1 { + z0 := 16772280239760917788496391897731603718812008455956943122563801666366297604776 + z1 := 568440292453150825972223760836185707764922522371208948902804025364325400423 + } + + /// @notice Constant function for the alt_bn128 group order. + /// @dev See https://eips.ethereum.org/EIPS/eip-196 for further details. + /// @return ret The alt_bn128 group order. + function P() -> ret { + ret := 21888242871839275222246405745257275088696311157297823662689037894645226208583 + } + + /// @notice Constant function for the twisted curve subgroup order. + /// @dev See https://hackmd.io/@jpw/bn254#Parameter-for-BN254 for further details. + /// @return ret The twisted curve subgroup orde. + function TWISTED_SUBGROUP_ORDER() -> ret { + ret := 21888242871839275222246405745257275088548364400416034343698204186575808495617 + } + + /// @notice Constant function for the pre-computation of R^2 % N for the Montgomery REDC algorithm. + /// @dev R^2 is the Montgomery residue of the value 2^512. + /// @dev See https://en.wikipedia.org/wiki/Montgomery_modular_multiplication#The_REDC_algorithm for further details. + /// @dev This value was precomputed using Python. + /// @return ret The value R^2 modulus the curve group order. + function R2_MOD_P() -> ret { + ret := 3096616502983703923843567936837374451735540968419076528771170197431451843209 + } + + /// @notice Constant function for the pre-computation of N' for the Montgomery REDC algorithm. + /// @dev N' is a value such that NN' = -1 mod R, with N being the curve group order. + /// @dev See https://en.wikipedia.org/wiki/Montgomery_modular_multiplication#The_REDC_algorithm for further details. + /// @dev This value was precomputed using Python. + /// @return ret The value N'. + function N_PRIME() -> ret { + ret := 111032442853175714102588374283752698368366046808579839647964533820976443843465 + } + + /// @notice Constant function for the alt_bn128 curve seed (parameter `x`). + /// @dev See https://eips.ethereum.org/EIPS/eip-196 for further details. + /// @return ret The alt_bn128 curve seed. + function X() -> ret { + ret := 4965661367192848881 + } + + /// @notice Constant function for decimal representation of the NAF for the Millers Loop. + /// @dev Millers loop uses to iterate the NAF representation of the value t = 6x^2. Where x = 4965661367192848881 is a parameter of the BN 256 curve. + /// @dev For details of the x parameter: https://hackmd.io/@jpw/bn254#Barreto-Naehrig-curves. + /// @dev A NAF representation uses values: -1, 0 and 1. https://en.wikipedia.org/wiki/Non-adjacent_form. + /// @dev For iterating between this values we represent the 0 as 00, the 1 as 01 and the -1 as 10. + /// @dev Then we concatenate all and represent the result as a decimal. E.g. [0,-1,0,1] -> 00 10 00 01 -> 33 + /// @dev In each step of the iteration we just need to compute the operation AND between the number and 1 and 2 to check the original value. + /// @dev Finally we shift 2 bits to the right to get the next value. + /// @dev For this implementation, the first two iterations of the Miller loop are skipped, so the last two digits of the NAF representation of t are not used. + /// @dev This value was precomputed using Python. + /// @return ret The value of the decimal representation of the NAF. + function NAF_REPRESENTATIVE() -> ret { + // NAF rep in binary form + // 000000010001001000001000000001000010001000000001001000000000100000010010000001000000000010000010000100100000001000100000000100 + ret := 355712981487968141245753120442583044 + } + + /// @notice Constant function for the zero element in Fp6 representation. + /// @return z00, z01, z10, z11, z20, z21 The values of zero in Fp6. + function FP6_ZERO() -> z00, z01, z10, z11, z20, z21 { + z00 := 0 + z01 := 0 + z10 := 0 + z11 := 0 + z20 := 0 + z21 := 0 + } + + /// @notice Constant function for the zero element in the twisted curve on affine representation. + /// @return z00, z01, z10, z11, z20, z21 The values of infinity point on affine representation. + function G2_INFINITY() -> z00, z01, z02, z10, z11, z12 { + z00 := 0 + z01 := 0 + z02 := 0 + z10 := 0 + z11 := 0 + z12 := 0 + } + + /// @notice Constant function for element one in Fp12 representation. + /// @return the values of one in Fp12. + function FP12_ONE() -> z000, z001, z010, z011, z100, z101, z110, z111, z200, z201, z210, z211 { + z000 := MONTGOMERY_ONE() + z001 := 0 + z010 := 0 + z011 := 0 + z100 := 0 + z101 := 0 + z110 := 0 + z111 := 0 + z200 := 0 + z201 := 0 + z210 := 0 + z211 := 0 + } + + /// @notice Constant function for the length of the input of a single pair of points to compute the pairing. + /// @return ret The length of a pair of points input. + function PAIR_LENGTH() -> ret { + ret := 0xc0 + } + + // HELPER FUNCTIONS + + /// @dev Executes the `precompileCall` opcode. + function precompileCall(precompileParams, gasToBurn) -> ret { + // Compiler simulation for calling `precompileCall` opcode + ret := verbatim_2i_1o("precompile", precompileParams, gasToBurn) + } + + /// @notice Burns remaining gas until revert. + /// @dev This function is used to burn gas in the case of a failed precompile call. + function burnGas() { + // Precompiles that do not have a circuit counterpart + // will burn the provided gas by calling this function. + precompileCall(0, gas()) + } + + /// @notice Calculate the bit length of a number. + /// @param x The number to calculate the bit length of. + /// @return ret The bit length of the number. + function bitLen(x) -> ret { + ret := 0 + for {} x {} { + ret := add(ret, 1) + x := shr(1, x) + } + } + + /// @notice Checks if the bit of a number at a given index is 1. + /// @dev The index is counted from the right, starting at 0. + /// @param index The index of the bit to check. + /// @param n The number to check the bit of. + /// @return ret The value of the bit at the given index. + function checkBit(index, n) -> ret { + ret := and(shr(index, n), 1) + } + + // MONTGOMERY + + /// @notice Computes the inverse in Montgomery Form of a number in Montgomery Form. + /// @dev Reference: https://github.com/lambdaclass/lambdaworks/blob/main/math/src/field/fields/montgomery_backed_prime_fields.rs#L169 + /// @dev Let `base` be a number in Montgomery Form, then base = a*R mod P() being `a` the base number (not in Montgomery Form) + /// @dev Let `inv` be the inverse of a number `a` in Montgomery Form, then inv = a^(-1)*R mod P() + /// @dev The original binary extended euclidean algorithms takes a number a and returns a^(-1) mod N + /// @dev In our case N is P(), and we'd like the input and output to be in Montgomery Form (a*R mod P() + /// @dev and a^(-1)*R mod P() respectively). + /// @dev If we just pass the input as a number in Montgomery Form the result would be a^(-1)*R^(-1) mod P(), + /// @dev but we want it to be a^(-1)*R mod P(). + /// @dev For that, we take advantage of the algorithm's linearity and multiply the result by R^2 mod P() + /// @dev to get R^2*a^(-1)*R^(-1) mod P() = a^(-1)*R mod P() as the desired result in Montgomery Form. + /// @dev `inv` takes the value of `b` or `c` being the result sometimes `b` and sometimes `c`. In paper + /// @dev multiplying `b` or `c` by R^2 mod P() results on starting their values as b = R2_MOD_P() and c = 0. + /// @param base A number `a` in Montgomery Form, then base = a*R mod P(). + /// @return inv The inverse of a number `a` in Montgomery Form, then inv = a^(-1)*R mod P(). + function binaryExtendedEuclideanAlgorithm(base) -> inv { + let modulus := P() + let u := base + let v := modulus + // Avoids unnecessary reduction step. + let b := R2_MOD_P() + let c := 0 + + for {} and(iszero(eq(u, 1)), iszero(eq(v, 1))) {} { + for {} iszero(and(u, 1)) {} { + u := shr(1, u) + let current := b + switch and(current, 1) + case 0 { + b := shr(1, b) + } + case 1 { + b := shr(1, add(b, modulus)) + } + } + + for {} iszero(and(v, 1)) {} { + v := shr(1, v) + let current := c + switch and(current, 1) + case 0 { + c := shr(1, c) + } + case 1 { + c := shr(1, add(c, modulus)) + } + } + + switch gt(v, u) + case 0 { + u := sub(u, v) + if lt(b, c) { + b := add(b, modulus) + } + b := sub(b, c) + } + case 1 { + v := sub(v, u) + if lt(c, b) { + c := add(c, modulus) + } + c := sub(c, b) + } + } + + switch eq(u, 1) + case 0 { + inv := c + } + case 1 { + inv := b + } + } + + /// @notice Computes an addition and checks for overflow. + /// @param augend The value to add to. + /// @param addend The value to add. + /// @return sum The sum of the two values. + /// @return overflowed True if the addition overflowed, false otherwise. + function overflowingAdd(augend, addend) -> sum, overflowed { + sum := add(augend, addend) + overflowed := lt(sum, augend) + } + + /// @notice Retrieves the highest half of the multiplication result. + /// @param multiplicand The value to multiply. + /// @param multiplier The multiplier. + /// @return ret The highest half of the multiplication result. + function getHighestHalfOfMultiplication(multiplicand, multiplier) -> ret { + ret := verbatim_2i_1o("mul_high", multiplicand, multiplier) + } + + /// @notice Implementation of the Montgomery reduction algorithm (a.k.a. REDC). + /// @dev See https://en.wikipedia.org/wiki/Montgomery_modular_multiplication#The_REDC_algorithm + /// @param lowestHalfOfT The lowest half of the value T. + /// @param higherHalfOfT The higher half of the value T. + /// @return S The result of the Montgomery reduction. + function REDC(lowest_half_of_T, higher_half_of_T) -> S { + let q := mul(lowest_half_of_T, N_PRIME()) + let a_high := add(higher_half_of_T, getHighestHalfOfMultiplication(q, P())) + let a_low, overflowed := overflowingAdd(lowest_half_of_T, mul(q, P())) + if overflowed { + a_high := add(a_high, 1) + } + S := a_high + if iszero(lt(a_high, P())) { + S := sub(a_high, P()) + } + } + + /// @notice Encodes a field element into the Montgomery form using the Montgomery reduction algorithm (REDC). + /// @dev See https://en.wikipedia.org/wiki/Montgomery_modular_multiplication#The_REDC_algorithm for further details on transforming a field element into the Montgomery form. + /// @param a The field element to encode. + /// @return ret The field element in Montgomery form. + function intoMontgomeryForm(a) -> ret { + let hi := getHighestHalfOfMultiplication(a, R2_MOD_P()) + let lo := mul(a, R2_MOD_P()) + ret := REDC(lo, hi) + } + + /// @notice Decodes a field element out of the Montgomery form using the Montgomery reduction algorithm (REDC). + /// @dev See https://en.wikipedia.org/wiki/Montgomery_modular_multiplication#The_REDC_algorithm for further details on transforming a field element out of the Montgomery form. + /// @param m The field element in Montgomery form to decode. + /// @return ret The decoded field element. + function outOfMontgomeryForm(m) -> ret { + let higher_half_of_m := 0 + let lowest_half_of_m := m + ret := REDC(lowest_half_of_m, higher_half_of_m) + } + + /// @notice Computes the Montgomery addition. + /// @dev See https://en.wikipedia.org/wiki/Montgomery_modular_multiplication#The_REDC_algorithm for further details on the Montgomery multiplication. + /// @param augend The augend in Montgomery form. + /// @param addend The addend in Montgomery form. + /// @return ret The result of the Montgomery addition. + function montgomeryAdd(augend, addend) -> ret { + ret := add(augend, addend) + if iszero(lt(ret, P())) { + ret := sub(ret, P()) + } + } + + /// @notice Computes the Montgomery subtraction. + /// @dev See https://en.wikipedia.org/wiki/Montgomery_modular_multiplication#The_REDC_algorithm for further details on the Montgomery multiplication. + /// @param minuend The minuend in Montgomery form. + /// @param subtrahend The subtrahend in Montgomery form. + /// @return ret The result of the Montgomery addition. + function montgomerySub(minuend, subtrahend) -> ret { + ret := sub(minuend, subtrahend) + if lt(minuend, subtrahend) { + ret := add(ret, P()) + } + } + + /// @notice Computes the Montgomery multiplication using the Montgomery reduction algorithm (REDC). + /// @dev See https://en.wikipedia.org/wiki/Montgomery_modular_multiplication#The_REDC_algorithm for further details on the Montgomery multiplication. + /// @param multiplicand The multiplicand in Montgomery form. + /// @param multiplier The multiplier in Montgomery form. + /// @return ret The result of the Montgomery multiplication. + function montgomeryMul(multiplicand, multiplier) -> ret { + let higher_half_of_product := getHighestHalfOfMultiplication(multiplicand, multiplier) + let lowest_half_of_product := mul(multiplicand, multiplier) + ret := REDC(lowest_half_of_product, higher_half_of_product) + } + + /// @notice Computes the Montgomery modular inverse skipping the Montgomery reduction step. + /// @dev The Montgomery reduction step is skipped because a modification in the binary extended Euclidean algorithm is used to compute the modular inverse. + /// @dev See the function `binaryExtendedEuclideanAlgorithm` for further details. + /// @param a The field element in Montgomery form to compute the modular inverse of. + /// @return invmod The result of the Montgomery modular inverse (in Montgomery form). + function montgomeryModularInverse(a) -> invmod { + invmod := binaryExtendedEuclideanAlgorithm(a) + } + + // CURVE ARITHMETIC + + /// @notice Checks if a coordinate is on the curve group order. + /// @dev A coordinate is on the curve group order if it is on the range [0, curveFieldOrder). + /// @param coordinate The coordinate to check. + /// @return ret True if the coordinate is in the range, false otherwise. + function coordinateIsOnFieldOrder(coordinate) -> ret { + ret := lt(coordinate, P()) + } + + // G1 + + /// @notice Checks if a point of the G1 curve is infinity. + /// @dev In affine coordinates the infinity is represented by the point (0,0). + /// @param x The x coordinate to check. + /// @param y The y coordinate to check. + /// @return ret True if the point is infinity, false otherwise. + function g1AffinePointIsInfinity(x, y) -> ret { + ret := iszero(or(x, y)) + } + + /// @notice Checks if a point in affine coordinates in Montgomery form is on the curve. + /// @dev The curve in question is the alt_bn128 curve. + /// @dev The Short Weierstrass equation of the curve is y^2 = x^3 + 3. + /// @param x The x coordinate of the point in Montgomery form. + /// @param y The y coordinate of the point in Montgomery form. + /// @return ret True if the point is on the curve, false otherwise. + function g1AffinePointIsOnCurve(x, y) -> ret { + let ySquared := montgomeryMul(y, y) + let xSquared := montgomeryMul(x, x) + let xQubed := montgomeryMul(xSquared, x) + let xQubedPlusThree := montgomeryAdd(xQubed, MONTGOMERY_THREE()) + + ret := eq(ySquared, xQubedPlusThree) + } + + // G2 + + /// @notice Converts a G2 point in affine coordinates to projective coordinates. + /// @dev Both input and output coordinates are encoded in Montgomery form. + /// @dev If x or y differ from 0, just add z = (1,0). + /// @dev If x and y are equal to 0, then P is the infinity point, and z = (0,0). + /// @param xp0, xp1 The x coordinate to transform. + /// @param yp0, yp1 The y coordinate to transform. + /// @return xr0, xr1, yr0, yr1, zr0, zr1 The projective coordinates of the given G2 point. + function g2ProjectiveFromAffine(xp0, xp1, yp0, yp1) -> xr0, xr1, yr0, yr1, zr0, zr1 { + xr0 := xp0 + xr1 := xp1 + yr0 := yp0 + yr1 := yp1 + zr0 := MONTGOMERY_ONE() + zr1 := 0 + } + + /// @notice Checks if a G2 point in affine coordinates is the point at infinity. + /// @dev The coordinates are encoded in Montgomery form. + /// @dev in Affine coordinates the point represents the infinity if both coordinates are 0. + /// @param x0, x1 The x coordinate to check. + /// @param y0, y1 The y coordinate to check. + /// @return ret True if the point is the point at infinity, false otherwise. + function g2AffinePointIsInfinity(x0, x1, y0, y1) -> ret { + ret := iszero(or(or(x0, x1), or(y0, y1))) + } + + /// @notice Checks if a G2 point in affine coordinates belongs to the twisted curve. + /// @dev The coordinates are encoded in Montgomery form. + /// @dev in Affine coordinates the point belongs to the curve if it satisfies the equation: y^2 = x^3 + 3. + /// @dev See https://hackmd.io/@jpw/bn254#Twists for further details. + /// @param x0, x1 The x coordinate to check. + /// @param y0, y1 The y coordinate to check. + /// @return ret True if the point is in the curve, false otherwise. + function g2AffinePointIsOnCurve(x0, x1, y0, y1) -> ret { + let a0, a1 := MONTGOMERY_TWISTED_CURVE_COEFFS() + let b0, b1 := fp2Mul(x0, x1, x0, x1) + b0, b1 := fp2Mul(b0, b1, x0, x1) + b0, b1 := fp2Add(b0, b1, a0, a1) + let c0, c1 := fp2Mul(y0, y1, y0, y1) + ret := and(eq(b0, c0), eq(b1, c1)) + } + + /// @notice Checks if a G2 point in projective coordinates is the point at infinity. + /// @dev The coordinates are encoded in Montgomery form. + /// @dev A projective point is at infinity if the z coordinate is (0, 0). + /// @param x0, x1 The x coordinate of the point. + /// @param y0, y1 The y coordinate of the point. + /// @param z0, z1 The z coordinate of the point. + /// @return ret True if the point is the point at infinity, false otherwise. + function g2ProjectivePointIsInfinity(x0, x1, y0, y1, z0, z1) -> ret { + ret := iszero(or(z0, z1)) + } + + /// @notice Negates a G2 point in affine coordinates. + /// @dev The coordinates are encoded in Montgomery form. + /// @dev The negation of a point (x, y) is (x, -y). + /// @param x0, x1 The x coordinate of the point. + /// @param y0, y1 The y coordinate of the point. + /// @return nx0, nx1, ny0, ny1 The coordinates of the negated point. + function g2AffineNeg(x0, x1, y0, y1) -> nx0, nx1, ny0, ny1 { + nx0 := x0 + nx1 := x1 + ny0, ny1 := fp2Neg(y0, y1) + } + + /// @notice Constant function for the alt_bn128 returning `(xi)^ ((N - 1) // 2)`. Where `xi` is D-type twist param. + /// @dev See https://eprint.iacr.org/2022/352.pdf (2 Preliminaries) for further details. + /// @return ret Twisted curve `xi2 = (xi)^ ((N - 1) // 2)` value in Montgomery form. + function xi2() -> xi0, xi1 { + xi0 := intoMontgomeryForm(2821565182194536844548159561693502659359617185244120367078079554186484126554) + xi1 := intoMontgomeryForm(3505843767911556378687030309984248845540243509899259641013678093033130930403) + } + + /// @notice Constant function for the alt_bn128 returning `(xi)^ ((N - 1) // 2)`. Where `xi` is D-type twist param. + /// @dev See https://eprint.iacr.org/2022/352.pdf (2 Preliminaries) for further details. + /// @return ret Twisted curve `xi2 = (xi)^ ((N - 1) // 2)` value in Montgomery form. + function xi3() -> xi0, xi1 { + xi0 := intoMontgomeryForm(21575463638280843010398324269430826099269044274347216827212613867836435027261) + xi1 := intoMontgomeryForm(10307601595873709700152284273816112264069230130616436755625194854815875713954) + } + + /// @notice Frobenius endomophism used to G2 sub group check for twisted curve. + /// @dev For more datail see https://eprint.iacr.org/2022/348.pdf + /// @param xp0, xp1 The x coordinate of the point on twisted curve. + /// @param yp0, yp1 The y coordinate of the point on twisted curve. + /// @param zp0, zp1 The z coordinate of the point on twisted curve. + /// @return Point on twisted curve transformed by the phi endomorphism + function endomorphism(xp0, xp1, yp0, yp1, zp0, zp1) -> xr0, xr1, yr0, yr1, zr0, zr1 { + let xp0_c, xp1_c := fp2Conjugate(xp0, xp1) + let yp0_c, yp1_c := fp2Conjugate(yp0, yp1) + + let xi2_0, xi2_1 := xi2() + let xi3_0, xi3_1 := xi3() + + xr0, xr1 := fp2Mul(xp0_c, xp1_c, xi3_0, xi3_1) + yr0, yr1 := fp2Mul(yp0_c, yp1_c, xi2_0, xi2_1) + zr0, zr1 := fp2Conjugate(zp0, zp1) + } + /// @notice Check if a G2 point in jacobian coordinates is in the subgroup of the twisted curve. + /// @dev The coordinates are encoded in Montgomery form. + /// @param xp0, xp1 The x coordinate of the point. + /// @param yp0, yp1 The y coordinate of the point. + /// @param zp0, zp1 The z coordinate of the point. + /// @return ret True if the point is in the subgroup, false otherwise. + function g2IsInSubGroup(xp0, xp1, yp0, yp1, zp0, zp1) -> ret { + // P * X + let px_xp0, px_xp1, px_yp0, px_yp1, px_zp0, px_zp1 := g2ScalarMul(xp0, xp1, yp0, yp1, zp0, zp1, X()) + // P * (X + 1) + let px1_xp0, px1_xp1, px1_yp0, px1_yp1, px1_zp0, px1_zp1 := g2JacobianAdd(px_xp0, px_xp1, px_yp0, px_yp1, px_zp0, px_zp1, xp0, xp1, yp0, yp1, zp0, zp1) + // P * 2X + let p2x_xp0, p2x_xp1, p2x_yp0, p2x_yp1, p2x_zp0, p2x_zp1 := g2JacobianDouble(px_xp0, px_xp1, px_yp0, px_yp1, px_zp0, px_zp1) + + // phi(P * X) + let e_px_xp0, e_px_xp1, e_px_yp0, e_px_yp1, e_px_zp0, e_px_zp1 := endomorphism(px_xp0, px_xp1, px_yp0, px_yp1, px_zp0, px_zp1) + // phi(phi(P * X)) + let e2_px_xp0, e2_px_xp1, e2_px_yp0, e2_px_yp1, e2_px_zp0, e2_px_zp1 := endomorphism(e_px_xp0, e_px_xp1, e_px_yp0, e_px_yp1, e_px_zp0, e_px_zp1) + + // phi(phi(phi(P * 2X))) + p2x_xp0, p2x_xp1, p2x_yp0, p2x_yp1, p2x_zp0, p2x_zp1 := endomorphism(p2x_xp0, p2x_xp1, p2x_yp0, p2x_yp1, p2x_zp0, p2x_zp1) + p2x_xp0, p2x_xp1, p2x_yp0, p2x_yp1, p2x_zp0, p2x_zp1 := endomorphism(p2x_xp0, p2x_xp1, p2x_yp0, p2x_yp1, p2x_zp0, p2x_zp1) + p2x_xp0, p2x_xp1, p2x_yp0, p2x_yp1, p2x_zp0, p2x_zp1 := endomorphism(p2x_xp0, p2x_xp1, p2x_yp0, p2x_yp1, p2x_zp0, p2x_zp1) + + let l1x0, l1x2, l1y0, l1y2, l1z0, l1z2 := g2JacobianAdd(px1_xp0, px1_xp1, px1_yp0, px1_yp1, px1_zp0, px1_zp1, e_px_xp0, e_px_xp1, e_px_yp0, e_px_yp1, e_px_zp0, e_px_zp1) + l1x0, l1x2, l1y0, l1y2, l1z0, l1z2 := g2JacobianAdd(l1x0, l1x2, l1y0, l1y2, l1z0, l1z2, e2_px_xp0, e2_px_xp1, e2_px_yp0, e2_px_yp1, e2_px_zp0, e2_px_zp1) + + let l1z0_square, l1z2_square := fp2Mul(l1z0, l1z2, l1z0, l1z2) + let p2x_zp0_square, p2x_zp1_square := fp2Mul(p2x_zp0, p2x_zp1, p2x_zp0, p2x_zp1) + + let r00, r01 := fp2Mul(p2x_xp0, p2x_xp1, l1z0_square, l1z2_square) + let r10, r11 := fp2Mul(l1x0, l1x2, p2x_zp0_square, p2x_zp1_square) + + let l1z0_cube, l1z2_cube := fp2Mul(l1z0_square, l1z2_square, l1z0, l1z2) + let p2x_zp0_cube, p2x_zp1_cube := fp2Mul(p2x_zp0_square, p2x_zp1_square, p2x_zp0, p2x_zp1) + + let l00, l01 := fp2Mul(p2x_yp0, p2x_yp1, l1z0_cube, l1z2_cube) + let l10, l11 := fp2Mul(l1y0, l1y2, p2x_zp0_cube, p2x_zp1_cube) + + let r1 := and(eq(r00, r10), eq(r01, r11)) + let r2 := and(eq(l00, l10), eq(l01, l11)) + ret := and(r1, r2) + } + + + /// @notice Check if a G2 point in jacobian coordinates is in the subgroup of the twisted curve. + /// @dev The coordinates are encoded in Montgomery form. + /// @param xp0, xp1 The x coordinate of the point. + /// @param yp0, yp1 The y coordinate of the point. + /// @param zp0, zp1 The z coordinate of the point. + /// @return ret True if the point is in the subgroup, false otherwise. + function g2IsInSubGroupNaive(xp0, xp1, yp0, yp1, zp0, zp1) -> ret { + let xr0, xr1, yr0, yr1, zr0, zr1 := g2ScalarMul(xp0, xp1, yp0, yp1, zp0, zp1, TWISTED_SUBGROUP_ORDER()) + ret := and(iszero(zr0), iszero(zr1)) + } + + /// @notice Double a g2 point represented in jacobian coordinates. + /// @dev The coordinates must be encoded in Montgomery form. + /// @param xp0, xp1 The x coordinate of the point. + /// @param yp0, yp1 The y coordinate of the point. + /// @param zp0, zp1 The z coordinate of the point. + /// @return xr0, xr1, yr0, yr1, zr0, zr1 The coordinates of the doubled point. + function g2JacobianDouble(xp0, xp1, yp0, yp1, zp0, zp1) -> xr0, xr1, yr0, yr1, zr0, zr1 { + let a00, a01 := fp2Mul(xp0, xp1, xp0, xp1) // A = X1^2 + let b00, b01 := fp2Mul(yp0, yp1, yp0, yp1) // B = Y1^2 + let c00, c01 := fp2Mul(b00, b01, b00, b01) // C = B^2 + let t00, t01 := fp2Add(xp0, xp1, b00, b01) // t0 = X1+B + let t10, t11 := fp2Mul(t00, t01, t00, t01) // t1 = t0^2 + let t20, t21 := fp2Sub(t10, t11, a00, a01) // t2 = t1-A + let t30, t31 := fp2Sub(t20, t21, c00, c01) // t3 = t2-C + let d00, d01 := fp2Add(t30, t31, t30, t31) // D = 2*t3 + let e00, e01 := fp2Add(a00, a01, a00, a01) // E = 3*A + e00, e01 := fp2Add(e00, e01, a00, a01) + let f00, f01 := fp2Mul(e00, e01, e00, e01) // F = E^2 + let t40, t41 := fp2Add(d00, d01, d00, d01) // t4 = 2*D + xr0, xr1 := fp2Sub(f00, f01, t40, t41) // X3 = F-t4 + let t50, t51 := fp2Sub(d00, d01, xr0, xr1) // t5 = D-X3 + let t60, t61 := fp2Add(c00, c01, c00, c01) // t6 = 8*C + t60, t61 := fp2Add(t60, t61, t60, t61) + t60, t61 := fp2Add(t60, t61, t60, t61) + let t70, t71 := fp2Mul(e00, e01, t50, t51) // t7 = E*t5 + yr0, yr1 := fp2Sub(t70, t71, t60, t61) // Y3 = t7-t6 + let t80, t81 := fp2Mul(yp0, yp1, zp0, zp1) // t8 = Y1*Z1 + zr0, zr1 := fp2Add(t80, t81, t80, t81) // Z3 = 2*t8 + } + + /// @notice Add two g2 points represented in jacobian coordinates. + /// @dev The coordinates must be encoded in Montgomery form. + /// @dev The points to be added must be different, if not the function will return infinity. The function `g2JacobianDouble` should be used in that case. + /// @param xq0, xq1 The x coordinate of the first point. + /// @param yq0, yq1 The y coordinate of the first point. + /// @param zq0, zq1 The z coordinate of the first point. + /// @param xr0, xr1 The x coordinate of the second point. + /// @param yr0, yr1 The y coordinate of the second point. + /// @param zr0, zr1 The z coordinate of the second point. + /// @return c00, c01, c10, c11, c20, c21 The coordinates of the added points. + function g2JacobianAdd(xq0, xq1, yq0, yq1, zq0, zq1, xr0, xr1, yr0, yr1, zr0, zr1) -> c00, c01, c10, c11, c20, c21 { + // Check for infinity in projective coordinates is the same as jacobian + let qIsInfinity := g2ProjectivePointIsInfinity(xq0, xq1, yq0, yq1, zq0, zq1) + let rIsInfinity := g2ProjectivePointIsInfinity(xr0, xr1, yr0, yr1, zr0, zr1) + if rIsInfinity { + // Infinity + P = P + c00 := xq0 + c01 := xq1 + c10 := yq0 + c11 := yq1 + c20 := zq0 + c21 := zq1 + leave + } + if qIsInfinity { + // P + Infinity = P + c00 := xr0 + c01 := xr1 + c10 := yr0 + c11 := yr1 + c20 := zr0 + c21 := zr1 + leave + } + + // Z1Z1 = Z1^2 + let zqzq0, zqzq1 := fp2Mul(zq0, zq1, zq0, zq1) + // Z2Z2 = Z2^2 + let zrzr0, zrzr1 := fp2Mul(zr0, zr1, zr0, zr1) + // U1 = X1*Z2Z2 + let u0, u1 := fp2Mul(xq0, xq1, zrzr0, zrzr1) + // U2 = X2*Z1Z1 + let u2, u3 := fp2Mul(xr0, xr1, zqzq0, zqzq1) + // t0 = Z2*Z2Z2 + let t0, t1 := fp2Mul(zr0, zr1, zrzr0, zrzr1) + // S1 = Y1*t0 + let s0, s1 := fp2Mul(yq0, yq1, t0, t1) + // t1 = Z1*Z1Z1 + let t2, t3 := fp2Mul(zq0, zq1, zqzq0, zqzq1) + // S2 = Y2*t1 + let s2, s3 := fp2Mul(yr0, yr1, t2, t3) + // H = U2-U1 + let h0, h1 := fp2Sub(u2, u3, u0, u1) + // t2 = 2*H + let t4, t5 := fp2Add(h0, h1, h0, h1) + // I = t2^2 + let i0, i1 := fp2Mul(t4, t5, t4, t5) + // J = H*I + let j0, j1 := fp2Mul(h0, h1, i0, i1) + // t3 = S2-S1 + let t6, t7 := fp2Sub(s2, s3, s0, s1) + // r = 2*t3 + let r0, r1 := fp2Add(t6, t7, t6, t7) + // V = U1*I + let v0, v1 := fp2Mul(u0, u1, i0, i1) + // t4 = r^2 + let t8, t9 := fp2Mul(r0, r1, r0, r1) + // t5 = 2*V + let t10, t11 := fp2Add(v0, v1, v0, v1) + // t6 = t4-J + let t12, t13 := fp2Sub(t8, t9, j0, j1) + // X3 = t6-t5 + c00, c01 := fp2Sub(t12, t13, t10, t11) + // t7 = V-X3 + let t14, t15 := fp2Sub(v0, v1, c00, c01) + // t8 = S1*J + let t16, t17 := fp2Mul(s0, s1, j0, j1) + // t9 = 2*t8 + let t18, t19 := fp2Add(t16, t17, t16, t17) + // t10 = r*t7 + let t20, t21 := fp2Mul(r0, r1, t14, t15) + // Y3 = t10-t9 + c10, c11 := fp2Sub(t20, t21, t18, t19) + // t11 = Z1+Z2 + let t22, t23 := fp2Add(zq0, zq1, zr0, zr1) + // t12 = t11^2 + let t24, t25 := fp2Mul(t22, t23, t22, t23) + // t13 = t12-Z1Z1 + let t26, t27 := fp2Sub(t24, t25, zqzq0, zqzq1) + // t14 = t13-Z2Z2 + let t28, t29 := fp2Sub(t26, t27, zrzr0, zrzr1) + // Z3 = t14*H + c20, c21 := fp2Mul(t28, t29, h0, h1) + } + + /// @notice Multiplies a G2 point represented in jacobian coordinates by a scalar. + /// @dev The coordinates must be encoded in Montgomery form. + /// @dev The scalar must not be encoded in Montgomery form. + /// @param xp0, xp1 The x coordinate of the point. + /// @param yp0, yp1 The y coordinate of the point. + /// @param zp0, zp1 The z coordinate of the point. + /// @param scalar The scalar to multiply the point by. + /// @return xr0, xr1, yr0, yr1, zr0, zr1 The coordinates of the multiplied point. + function g2ScalarMul(xp0, xp1, yp0, yp1, zp0, zp1, scalar) -> xr0, xr1, yr0, yr1, zr0, zr1 { + let scalarBitIndex := bitLen(scalar) + switch scalar + case 0x02 { + xr0, xr1, yr0, yr1, zr0, zr1 := g2JacobianDouble(xp0, xp1, yp0, yp1, zp0, yp1) + } + default { + xr0 := 0 + xr1 := 0 + yr0 := MONTGOMERY_ONE() + yr1 := 0 + zr0 := 0 + zr1 := 0 + for {} scalarBitIndex {} { + scalarBitIndex := sub(scalarBitIndex, 1) + xr0, xr1, yr0, yr1, zr0, zr1 := g2JacobianDouble(xr0, xr1, yr0, yr1, zr0, zr1) + let bitindex := checkBit(scalarBitIndex, scalar) + if bitindex { + xr0, xr1, yr0, yr1, zr0, zr1 := g2JacobianAdd(xp0, xp1, yp0, yp1, zp0, zp1, xr0, xr1, yr0, yr1, zr0, zr1) + } + + } + } + } + + // FP2 ARITHMETHICS + + /// @notice Computes the sum of two Fp2 elements. + /// @dev Algorithm 5 in: https://eprint.iacr.org/2010/354.pdf. + /// @param a00, a01 The coefficients of the A element to sum. + /// @param b00, b01 The coefficients of the B element to sum. + /// @return c00, c01 The coefficients of the element C = A + B. + function fp2Add(a00, a01, b00, b01) -> c00, c01 { + c00 := montgomeryAdd(a00, b00) + c01 := montgomeryAdd(a01, b01) + } + + /// @notice Computes the subtraction of two Fp2 elements. + /// @dev Algorithm 6 in: https://eprint.iacr.org/2010/354.pdf. + /// @param a00, a01 The coefficients of the minuend A. + /// @param b00, b01 The coefficients of the subtrahend B. + /// @return c00, c01 The coefficients of the element C = A - B. + function fp2Sub(a00, a01, b00, b01) -> c00, c01 { + c00 := montgomerySub(a00, b00) + c01 := montgomerySub(a01, b01) + } + + /// @notice Computes the multiplication between a Fp2 element a Fp element. + /// @dev Algorithm 7 in: https://eprint.iacr.org/2010/354.pdf. + /// @param a00, a01 The coefficients of the Fp2 element A. + /// @param scalar The value of the Fp element k. + /// @return c00, c01 The coefficients of the element C = k * A. + function fp2ScalarMul(a00, a01, scalar) -> c00, c01 { + c00 := montgomeryMul(a00, scalar) + c01 := montgomeryMul(a01, scalar) + } + + /// @notice Computes the multiplication between two Fp2 elements. + /// @dev Algorithm 7 in: https://eprint.iacr.org/2010/354.pdf. + /// @param a00, a01 The coefficients of the Fp2 element A. + /// @param a00, a01 The coefficients of the Fp2 element B. + /// @return c00, c01 The coefficients of the element C = A * B. + function fp2Mul(a00, a01, b00, b01) -> c00, c01 { + c00 := montgomerySub(montgomeryMul(a00, b00), montgomeryMul(a01, b01)) + c01 := montgomeryAdd(montgomeryMul(a00, b01), montgomeryMul(a01, b00)) + } + + /// @notice Computes the negative of a Fp2 elements. + /// @param a00, a01 The coefficients of the Fp2 element A. + /// @return c00, c01 The coefficients of the element C = -A. + function fp2Neg(a00, a01) -> c00, c01 { + c00, c01 := fp2Sub(0, 0, a00, a01) + } + + /// @notice Computes the inverse of a Fp2 element. + /// @dev Algorithm 8 in: https://eprint.iacr.org/2010/354.pdf. + /// @param a00, a01 The coefficients of the Fp2 element A. + /// @return c00, c01 The coefficients of the element C = A^(-1). + function fp2Inv(a00, a01) -> c00, c01 { + let t0 := montgomeryMul(a00, a00) + let t1 := montgomeryMul(a01, a01) + t0 := montgomeryAdd(t0, t1) + t1 := montgomeryModularInverse(t0) + + c00 := montgomeryMul(a00, t1) + c01 := montgomerySub(0, montgomeryMul(a01, t1)) + } + + /// @notice Computes the multiplication of a Fp2 element with xi. + /// @dev Where xi = u in Fp + /// @dev See https://hackmd.io/@jpw/bn254#Field-extension-towers for further details. + /// @param a00, a01 The coefficients of the Fp2 element A. + /// @return c00, c01 The coefficients of the element C = A * xi. + function mulByXi(a00, a01) -> c00, c01 { + let t0, t1 := fp2ScalarMul(a00, a01, intoMontgomeryForm(8)) + c00 := montgomerySub(montgomeryAdd(t0, a00), a01) + c01 := montgomeryAdd(montgomeryAdd(t1, a00), a01) + } + + /// @notice Computes the conjugation of a Fp2 element. + /// @param a00, a01 The coefficients of the Fp2 element A. + /// @return c00, c01 The coefficients of the element C = A'. + function fp2Conjugate(a00, a01) -> c00, c01 { + c00 := a00 + c01 := montgomerySub(0, a01) + } + + // FP6 ARITHMETHICS + + /// @notice Computes the sum of two Fp6 elements. + /// @dev Algorithm 10 in: https://eprint.iacr.org/2010/354.pdf. + /// @param a00, a01, a10, a11, a20, a21 The coefficients of the A element to sum. + /// @param b00, b01, b10, b11, b20, b21 The coefficients of the B element to sum. + /// @return c00, c01, c10, c11, c20, c21 The coefficients of the element C = A + B. + function fp6Add(a00, a01, a10, a11, a20, a21, b00, b01, b10, b11, b20, b21) -> c00, c01, c10, c11, c20, c21 { + c00, c01 := fp2Add(a00, a01, b00, b01) + c10, c11 := fp2Add(a10, a11, b10, b11) + c20, c21 := fp2Add(a20, a21, b20, b21) + } + + /// @notice Computes the subtraction of two Fp6 elements. + /// @dev Algorithm 11 in: https://eprint.iacr.org/2010/354.pdf. + /// @param a00, a01, a10, a11, a20, a21 The coefficients of the minuend A. + /// @param b00, b01, b10, b11, b20, b21 The coefficients of the subtrahend B. + /// @return c00, c01, c10, c11, c20, c21 The coefficients of the element C = A - B. + function fp6Sub(a00, a01, a10, a11, a20, a21, b00, b01, b10, b11, b20, b21) -> c00, c01, c10, c11, c20, c21 { + c00, c01 := fp2Sub(a00, a01, b00, b01) + c10, c11 := fp2Sub(a10, a11, b10, b11) + c20, c21 := fp2Sub(a20, a21, b20, b21) + } + + /// @notice Computes the multiplication of a Fp6 element with g. + /// @dev Algorithm 12 in: https://eprint.iacr.org/2010/354.pdf. + /// @param a00, a01, a10, a11, a20, a21 The coefficients of the Fp6 element A. + /// @return c00, c01, c10, c11, c20, c21 The coefficients of the element C = A * g. + function mulByGamma(a00, a01, a10, a11, a20, a21) -> c00, c01, c10, c11, c20, c21 { + c00, c01 := mulByXi(a20, a21) + c10 := a00 + c11 := a01 + c20 := a10 + c21 := a11 + } + + /// @notice Computes the multiplication between two Fp6 elements. + /// @dev Algorithm 13 in: https://eprint.iacr.org/2010/354.pdf. + /// @param a00, a01, a10, a11, a20, a21 The coefficients of the Fp6 element A. + /// @param b00, b01, b10, b11, b20, b21 The coefficients of the Fp6 element B. + /// @return c00, c01, c10, c11, c20, c21 The coefficients of the element C = A * B. + function fp6Mul(a00, a01, a10, a11, a20, a21, b00, b01, b10, b11, b20, b21) -> c00, c01, c10, c11, c20, c21 { + let t00, t01 := fp2Mul(a00, a01, b00, b01) + let t10, t11 := fp2Mul(a10, a11, b10, b11) + let t20, t21 := fp2Mul(a20, a21, b20, b21) + + let tmp0, temp1 := fp2Add(a10, a11, a20, a21) + let tmp2, tmp3 := fp2Add(b10, b11, b20, b21) + let tmp4, tmp5 := fp2Mul(tmp0, temp1, tmp2, tmp3) + let tmp6, tmp7 := fp2Sub(tmp4, tmp5, t10, t11) + let tmp8, tmp9 := fp2Sub(tmp6, tmp7, t20, t21) + let tmp10, tmp11 := mulByXi(tmp8, tmp9) + c00, c01 := fp2Add(tmp10, tmp11, t00, t01) + + tmp0, temp1 := fp2Add(a00, a01, a10, a11) + tmp2, tmp3 := fp2Add(b00, b01, b10, b11) + tmp4, tmp5 := fp2Mul(tmp0, temp1, tmp2, tmp3) + tmp6, tmp7 := fp2Sub(tmp4, tmp5, t00, t01) + tmp8, tmp9 := fp2Sub(tmp6, tmp7, t10, t11) + tmp10, tmp11 := mulByXi(t20, t21) + c10, c11 := fp2Add(tmp8, tmp9, tmp10, tmp11) + + tmp0, temp1 := fp2Add(a00, a01, a20, a21) + tmp2, tmp3 := fp2Add(b00, b01, b20, b21) + tmp4, tmp5 := fp2Mul(tmp0, temp1, tmp2, tmp3) + tmp6, tmp7 := fp2Sub(tmp4, tmp5, t00, t01) + tmp8, tmp9 := fp2Sub(tmp6, tmp7, t20, t21) + c20, c21 := fp2Add(tmp8, tmp9, t10, t11) + } + + /// @notice Computes the negative of a Fp6 element. + /// @param a00, a01, a10, a11, a20, a21 The coefficients of the Fp2 element A. + /// @return c00, c01, c10, c11, c20, c21 The coefficients of the element C = -A. + function fp6Neg(a00, a01, a10, a11, a20, a21) -> c00, c01, c10, c11, c20, c21 { + c00, c01 := fp2Neg(a00, a01) + c10, c11 := fp2Neg(a10, a11) + c20, c21 := fp2Neg(a20, a21) + } + + /// @notice Computes the square of a Fp6 element. + /// @dev Algorithm 16 in: https://eprint.iacr.org/2010/354.pdf. + /// @param a00, a01, a10, a11, a20, a21 The coefficients of the Fp6 element A. + /// @return c00, c01, c10, c11, c20, c21 The coefficients of the element C = A^2. + function fp6Square(a00, a01, a10, a11, a20, a21) -> c00, c01, c10, c11, c20, c21 { + let tmp0, tmp1 := fp2Mul(a00, a01, a10, a11) + tmp0, tmp1 := fp2Add(tmp0, tmp1, tmp0, tmp1) + + let tmp2, tmp3 := fp2Mul(a20, a21, a20, a21) + let tmp4, tmp5 := mulByXi(tmp2, tmp3) + c10, c11 := fp2Add(tmp4, tmp5, tmp0, tmp1) + + c20, c21 := fp2Sub(tmp0, tmp1, tmp2, tmp3) + + let tmp6, tmp7 := fp2Mul(a00, a01, a00, a01) + let tmp8, tmp9 := fp2Sub(a00, a01, a10, a11) + tmp0, tmp1 := fp2Add(tmp8, tmp9, a20, a21) + + let tmp10, tmp11 := fp2Mul(a10, a11, a20, a21) + tmp2, tmp3 := fp2Add(tmp10, tmp11, tmp10, tmp11) + tmp0, tmp1 := fp2Mul(tmp0, tmp1, tmp0, tmp1) + + let tmp12, tmp13 := mulByXi(tmp2, tmp3) + c00, c01 := fp2Add(tmp12, tmp13, tmp6, tmp7) + + let tmp14, tmp15 := fp2Add(c20, c21, tmp0, tmp1) + tmp14, tmp15 := fp2Add(tmp14, tmp15, tmp2, tmp3) + c20, c21 := fp2Sub(tmp14, tmp15, tmp6, tmp7) + + } + + /// @notice Computes the inverse of a Fp6 element. + /// @dev Algorithm 17 in: https://eprint.iacr.org/2010/354.pdf. + /// @param a00, a01, a10, a11, a20, a21 The coefficients of the Fp6 element A. + /// @return c00, c01, c10, c11, c20, c21 The coefficients of the element C = A^(-1). + function fp6Inv(a00, a01, a10, a11, a20, a21) -> c00, c01, c10, c11, c20, c21 { + let t00, t01 := fp2Mul(a00, a01, a00, a01) + let t10, t11 := fp2Mul(a10, a11, a10, a11) + let t20, t21 := fp2Mul(a20, a21, a20, a21) + let t30, t31 := fp2Mul(a00, a01, a10, a11) + let t40, t41 := fp2Mul(a00, a01, a20, a21) + let t50, t51 := fp2Mul(a20, a21, a10, a11) + let t50Xi, t51Xi := mulByXi(t50, t51) + c00, c01 := fp2Sub(t00, t01, t50Xi, t51Xi) + let t20Xi, t21Xi := mulByXi(t20, t21) + c10, c11 := fp2Sub(t20Xi, t21Xi, t30, t31) + c20, c21 := fp2Sub(t10, t11, t40, t41) + let t60, t61 := fp2Mul(a00, a01, c00, c01) + let a20Xi, a21Xi := mulByXi(a20, a21) + let a20XiC10, a21XiC11 := fp2Mul(a20Xi, a21Xi, c10, c11) + t60, t61 := fp2Add(t60, t61, a20XiC10, a21XiC11) + let a10Xi, a11Xi := mulByXi(a10, a11) + let a10XiC20, a11XiC21 := fp2Mul(a10Xi, a11Xi, c20, c21) + t60, t61 := fp2Add(t60, t61, a10XiC20, a11XiC21) + t60, t61 := fp2Inv(t60, t61) + c00, c01 := fp2Mul(c00, c01, t60, t61) + c10, c11 := fp2Mul(c10, c11, t60, t61) + c20, c21 := fp2Mul(c20, c21, t60, t61) + } + + // FP12 ARITHMETHICS + + /// @notice Computes the sum of two Fp12 elements. + /// @dev Algorithm 18 in: https://eprint.iacr.org/2010/354.pdf. + /// @param a000, a001, a010, a011, a020, a021, a100, a101, a110, a111, a120, a121 The coefficients of the A element to sum. + /// @param b000, b001, b010, b011, b020, b021, b100, b101, b110, b111, b120, b121 The coefficients of the B element to sum. + /// @return c000, c001, c010, c011, c020, c021, c100, c101, c110, c111, c120, c121 The coefficients of the element C = A + B. + function fp12Add(a000, a001, a010, a011, a020, a021, a100, a101, a110, a111, a120, a121, b000, b001, b010, b011, b020, b021, b100, b101, b110, b111, b120, b121) -> c000, c001, c010, c011, c020, c021, c100, c101, c110, c111, c120, c121 { + c000, c001, c010, c011, c020, c021 := fp6Add(a000, a001, a010, a011, a020, a021, b000, b001, b010, b011, b020, b021) + c100, c101, c110, c111, c120, c121 := fp6Add(a100, a101, a110, a111, a120, a121, b100, b101, b110, b111, b120, b121) + } + + /// @notice Computes the subtraction of two Fp12 elements. + /// @dev Algorithm 19 in: https://eprint.iacr.org/2010/354.pdf. + /// @param a000, a001, a010, a011, a020, a021, a100, a101, a110, a111, a120, a121 The coefficients of the minuend A. + /// @param b000, b001, b010, b011, b020, b021, b100, b101, b110, b111, b120, b121 The coefficients of the subtrahend B. + /// @return c000, c001, c010, c011, c020, c021, c100, c101, c110, c111, c120, c121 The coefficients of the element C = A - B. + function fp12Sub(a000, a001, a010, a011, a020, a021, a100, a101, a110, a111, a120, a121, b000, b001, b010, b011, b020, b021, b100, b101, b110, b111, b120, b121) -> c000, c001, c010, c011, c020, c021, c100, c101, c110, c111, c120, c121 { + c000, c001, c010, c011, c020, c021 := fp6Sub(a000, a001, a010, a011, a020, a021, b000, b001, b010, b011, b020, b021) + c100, c101, c110, c111, c120, c121 := fp6Sub(a100, a101, a110, a111, a120, a121, b100, b101, b110, b111, b120, b121) + } + + /// @notice Computes the multiplication between two Fp12 elements. + /// @dev Algorithm 20 in: https://eprint.iacr.org/2010/354.pdf. + /// @param a000, a001, a010, a011, a020, a021, a100, a101, a110, a111, a120, a121 The coefficients of the Fp12 element A. + /// @param b000, b001, b010, b011, b020, b021, b100, b101, b110, b111, b120, b121 The coefficients of the Fp12 element B. + /// @return c000, c001, c010, c011, c020, c021, c100, c101, c110, c111, c120, c121 The coefficients of the element C = A * B. + function fp12Mul(a000, a001, a010, a011, a020, a021, a100, a101, a110, a111, a120, a121, b000, b001, b010, b011, b020, b021, b100, b101, b110, b111, b120, b121) -> c000, c001, c010, c011, c020, c021, c100, c101, c110, c111, c120, c121 { + let t000, t001, t010, t011, t020, t021 := fp6Mul(a000, a001, a010, a011, a020, a021, b000, b001, b010, b011, b020, b021) + let t100, t101, t110, t111, t120, t121 := fp6Mul(a100, a101, a110, a111, a120, a121, b100, b101, b110, b111, b120, b121) + let t200, t201, t210, t211, t220, t221 := mulByGamma(t100, t101, t110, t111, t120, t121) + c000, c001, c010, c011, c020, c021 := fp6Add(t000, t001, t010, t011, t020, t021, t200, t201, t210, t211, t220, t221) + let t300, t301, t310, t311, t320, t321 := fp6Add(a000, a001, a010, a011, a020, a021, a100, a101, a110, a111, a120, a121) + let t400, t401, t410, t411, t420, t421 := fp6Add(b000, b001, b010, b011, b020, b021, b100, b101, b110, b111, b120, b121) + c100, c101, c110, c111, c120, c121 := fp6Mul(t300, t301, t310, t311, t320, t321, t400, t401, t410, t411, t420, t421) + c100, c101, c110, c111, c120, c121 := fp6Sub(c100, c101, c110, c111, c120, c121, t000, t001, t010, t011, t020, t021) + c100, c101, c110, c111, c120, c121 := fp6Sub(c100, c101, c110, c111, c120, c121, t100, t101, t110, t111, t120, t121) + } + + /// @notice Computes the square of a Fp12 element. + /// @dev Algorithm 22 in: https://eprint.iacr.org/2010/354.pdf. + /// @param a000, a001, a010, a011, a020, a021, a100, a101, a110, a111, a120, a121 The coefficients of the Fp12 element A. + /// @return c000, c001, c010, c011, c020, c021, c100, c101, c110, c111, c120, c121 The coefficients of the element C = A^2. + function fp12Square(a000, a001, a010, a011, a020, a021, a100, a101, a110, a111, a120, a121) -> c000, c001, c010, c011, c020, c021, c100, c101, c110, c111, c120, c121 { + let t100, t101, t110, t111, t120, t121 := fp6Sub(a000, a001, a010, a011, a020, a021, a100, a101, a110, a111, a120, a121) + let t200, t201, t210, t211, t220, t221 := mulByGamma(a100, a101, a110, a111, a120, a121) + let t300, t301, t310, t311, t320, t321 := fp6Sub(a000, a001, a010, a011, a020, a021, t200, t201, t210, t211, t220, t221) + let t400, t401, t410, t411, t420, t421 := fp6Mul(a000, a001, a010, a011, a020, a021, a100, a101, a110, a111, a120, a121) + let t500, t501, t510, t511, t520, t521 := fp6Mul(t100, t101, t110, t111, t120, t121, t300, t301, t310, t311, t320, t321) + let t600, t601, t610, t611, t620, t621 := fp6Add(t400, t401, t410, t411, t420, t421, t500, t501, t510, t511, t520, t521) + c100, c101, c110, c111, c120, c121 := fp6Add(t400, t401, t410, t411, t420, t421, t400, t401, t410, t411, t420, t421) + let t700, t701, t710, t711, t720, t721 := mulByGamma(t400, t401, t410, t411, t420, t421) + c000, c001, c010, c011, c020, c021 := fp6Add(t600, t601, t610, t611, t620, t621, t700, t701, t710, t711, t720, t721) + } + + /// @notice Computes the inverse of a Fp12 element. + /// @dev Algorithm 23 in: https://eprint.iacr.org/2010/354.pdf. + /// @param a000, a001, a010, a011, a020, a021, a100, a101, a110, a111, a120, a121 The coefficients of the Fp12 element A. + /// @return c000, c001, c010, c011, c020, c021, c100, c101, c110, c111, c120, c121 The coefficients of the element C = A^(-1). + function fp12Inv(a000, a001, a010, a011, a020, a021, a100, a101, a110, a111, a120, a121) -> c000, c001, c010, c011, c020, c021, c100, c101, c110, c111, c120, c121 { + let t000, t001, t010, t011, t020, t021 := fp6Square(a000, a001, a010, a011, a020, a021) + let t100, t101, t110, t111, t120, t121 := fp6Square(a100, a101, a110, a111, a120, a121) + let t200, t201, t210, t211, t220, t221 := mulByGamma(t100, t101, t110, t111, t120, t121) + t000, t001, t010, t011, t020, t021 := fp6Sub(t000, t001, t010, t011, t020, t021, t200, t201, t210, t211, t220, t221) + t100, t101, t110, t111, t120, t121 := fp6Inv(t000, t001, t010, t011, t020, t021) + c000, c001, c010, c011, c020, c021 := fp6Mul(a000, a001, a010, a011, a020, a021, t100, t101, t110, t111, t120, t121) + let z00, z01, z10, z11, z20, z21 := FP6_ZERO() + c100, c101, c110, c111, c120, c121 := fp6Mul(a100, a101, a110, a111, a120, a121,t100, t101, t110, t111, t120, t121) + c100, c101, c110, c111, c120, c121 := fp6Sub(z00, z01, z10, z11, z20, z21, c100, c101, c110, c111, c120, c121) + } + + /// @notice Computes the exponentiation of a Fp12 element in the cyclotomic subgroup to t = 4965661367192848881. + /// @dev We make use of an addition chain to optimize the operation. + /// @dev See https://eprint.iacr.org/2015/192.pdf for further details. + /// @param a000, a001, a010, a011, a020, a021, a100, a101, a110, a111, a120, a121 The coefficients of the Fp12 element A. + /// @return c000, c001, c010, c011, c020, c021, c100, c101, c110, c111, c120, c121 The coefficients of the element C = A^t. + function fp12Expt(a000, a001, a010, a011, a020, a021, a100, a101, a110, a111, a120, a121) -> c000, c001, c010, c011, c020, c021, c100, c101, c110, c111, c120, c121 { + let t000, t001, t010, t011, t020, t021, t100, t101, t110, t111, t120, t121 := fp12CyclotomicSquare(a000, a001, a010, a011, a020, a021, a100, a101, a110, a111, a120, a121) + let t200, t201, t210, t211, t220, t221, t300, t301, t310, t311, t320, t321 := fp12CyclotomicSquare(t000, t001, t010, t011, t020, t021, t100, t101, t110, t111, t120, t121) + c000, c001, c010, c011, c020, c021, c100, c101, c110, c111, c120, c121 := fp12CyclotomicSquare(t200, t201, t210, t211, t220, t221, t300, t301, t310, t311, t320, t321) + let t400, t401, t410, t411, t420, t421, t500, t501, t510, t511, t520, t521 := fp12CyclotomicSquare(c000, c001, c010, c011, c020, c021, c100, c101, c110, c111, c120, c121) + + let t600, t601, t610, t611, t620, t621, t700, t701, t710, t711, t720, t721 := fp12Mul(a000, a001, a010, a011, a020, a021, a100, a101, a110, a111, a120, a121, t400, t401, t410, t411, t420, t421, t500, t501, t510, t511, t520, t521) + t400, t401, t410, t411, t420, t421, t500, t501, t510, t511, t520, t521 := fp12Mul(t600, t601, t610, t611, t620, t621, t700, t701, t710, t711, t720, t721, t000, t001, t010, t011, t020, t021, t100, t101, t110, t111, t120, t121) + let t800, t801, t810, t811, t820, t821, t900, t901, t910, t911, t920, t921 := fp12Mul(a000, a001, a010, a011, a020, a021, a100, a101, a110, a111, a120, a121, t400, t401, t410, t411, t420, t421, t500, t501, t510, t511, t520, t521) + let t1000, t1001, t1010, t1011, t1020, t1021, t1100, t1101, t1110, t1111, t1120, t1121 := fp12Mul(c000, c001, c010, c011, c020, c021, c100, c101, c110, c111, c120, c121, t600, t601, t610, t611, t620, t621, t700, t701, t710, t711, t720, t721) + let t1200, t1201, t1210, t1211, t1220, t1221, t1300, t1301, t1310, t1311, t1320, t1321 := fp12CyclotomicSquare(t600, t601, t610, t611, t620, t621, t700, t701, t710, t711, t720, t721) + t800, t801, t810, t811, t820, t821, t900, t901, t910, t911, t920, t921 := fp12Mul(t800, t801, t810, t811, t820, t821, t900, t901, t910, t911, t920, t921, t400, t401, t410, t411, t420, t421, t500, t501, t510, t511, t520, t521) + t400, t401, t410, t411, t420, t421, t500, t501, t510, t511, t520, t521 := fp12Mul(t800, t801, t810, t811, t820, t821, t900, t901, t910, t911, t920, t921, t000, t001, t010, t011, t020, t021, t100, t101, t110, t111, t120, t121) + t1200, t1201, t1210, t1211, t1220, t1221, t1300, t1301, t1310, t1311, t1320, t1321 := nSquare(t1200, t1201, t1210, t1211, t1220, t1221, t1300, t1301, t1310, t1311, t1320, t1321, 6) + t200, t201, t210, t211, t220, t221, t300, t301, t310, t311, t320, t321 := fp12Mul(t1200, t1201, t1210, t1211, t1220, t1221, t1300, t1301, t1310, t1311, t1320, t1321, t200, t201, t210, t211, t220, t221, t300, t301, t310, t311, t320, t321) + t200, t201, t210, t211, t220, t221, t300, t301, t310, t311, t320, t321 := fp12Mul(t200, t201, t210, t211, t220, t221, t300, t301, t310, t311, t320, t321, t1000, t1001, t1010, t1011, t1020, t1021, t1100, t1101, t1110, t1111, t1120, t1121) + t200, t201, t210, t211, t220, t221, t300, t301, t310, t311, t320, t321 := nSquare(t200, t201, t210, t211, t220, t221, t300, t301, t310, t311, t320, t321, 7) + t1000, t1001, t1010, t1011, t1020, t1021, t1100, t1101, t1110, t1111, t1120, t1121 := fp12Mul(t200, t201, t210, t211, t220, t221, t300, t301, t310, t311, t320, t321, t1000, t1001, t1010, t1011, t1020, t1021, t1100, t1101, t1110, t1111, t1120, t1121) + t1000, t1001, t1010, t1011, t1020, t1021, t1100, t1101, t1110, t1111, t1120, t1121 := nSquare(t1000, t1001, t1010, t1011, t1020, t1021, t1100, t1101, t1110, t1111, t1120, t1121, 8) + t1000, t1001, t1010, t1011, t1020, t1021, t1100, t1101, t1110, t1111, t1120, t1121 := fp12Mul(t1000, t1001, t1010, t1011, t1020, t1021, t1100, t1101, t1110, t1111, t1120, t1121, t400, t401, t410, t411, t420, t421, t500, t501, t510, t511, t520, t521) + t000, t001, t010, t011, t020, t021, t100, t101, t110, t111, t120, t121 := fp12Mul(t1000, t1001, t1010, t1011, t1020, t1021, t1100, t1101, t1110, t1111, t1120, t1121, t000, t001, t010, t011, t020, t021, t100, t101, t110, t111, t120, t121) + t000, t001, t010, t011, t020, t021, t100, t101, t110, t111, t120, t121 := nSquare(t000, t001, t010, t011, t020, t021, t100, t101, t110, t111, t120, t121, 6) + t600, t601, t610, t611, t620, t621, t700, t701, t710, t711, t720, t721 := fp12Mul(t000, t001, t010, t011, t020, t021, t100, t101, t110, t111, t120, t121, t600, t601, t610, t611, t620, t621, t700, t701, t710, t711, t720, t721) + t600, t601, t610, t611, t620, t621, t700, t701, t710, t711, t720, t721 := nSquare(t600, t601, t610, t611, t620, t621, t700, t701, t710, t711, t720, t721, 8) + t600, t601, t610, t611, t620, t621, t700, t701, t710, t711, t720, t721 := fp12Mul(t600, t601, t610, t611, t620, t621, t700, t701, t710, t711, t720, t721, t400, t401, t410, t411, t420, t421, t500, t501, t510, t511, t520, t521) + t600, t601, t610, t611, t620, t621, t700, t701, t710, t711, t720, t721 := nSquare(t600, t601, t610, t611, t620, t621, t700, t701, t710, t711, t720, t721, 6) + t600, t601, t610, t611, t620, t621, t700, t701, t710, t711, t720, t721 := fp12Mul(t600, t601, t610, t611, t620, t621, t700, t701, t710, t711, t720, t721, t400, t401, t410, t411, t420, t421, t500, t501, t510, t511, t520, t521) + t600, t601, t610, t611, t620, t621, t700, t701, t710, t711, t720, t721 := nSquare(t600, t601, t610, t611, t620, t621, t700, t701, t710, t711, t720, t721, 10) + t800, t801, t810, t811, t820, t821, t900, t901, t910, t911, t920, t921 := fp12Mul(t600, t601, t610, t611, t620, t621, t700, t701, t710, t711, t720, t721, t800, t801, t810, t811, t820, t821, t900, t901, t910, t911, t920, t921) + t800, t801, t810, t811, t820, t821, t900, t901, t910, t911, t920, t921 := nSquare(t800, t801, t810, t811, t820, t821, t900, t901, t910, t911, t920, t921, 6) + t400, t401, t410, t411, t420, t421, t500, t501, t510, t511, t520, t521 := fp12Mul(t400, t401, t410, t411, t420, t421, t500, t501, t510, t511, t520, t521, t800, t801, t810, t811, t820, t821, t900, t901, t910, t911, t920, t921) + c000, c001, c010, c011, c020, c021, c100, c101, c110, c111, c120, c121 := fp12Mul(t400, t401, t410, t411, t420, t421, t500, t501, t510, t511, t520, t521, c000, c001, c010, c011, c020, c021, c100, c101, c110, c111, c120, c121) + } + + /// @notice Computes the conjugation of a Fp12 element. + /// @param a000, a001, a010, a011, a020, a021, a100, a101, a110, a111, a120, a121 The coefficients of the Fp12 element A. + /// @return c000, c001, c010, c011, c020, c021, c100, c101, c110, c111, c120, c121 The coefficients of the element C = A'. + function fp12Conjugate(a000, a001, a010, a011, a020, a021, a100, a101, a110, a111, a120, a121) -> c000, c001, c010, c011, c020, c021, c100, c101, c110, c111, c120, c121 { + c000 := a000 + c001 := a001 + c010 := a010 + c011 := a011 + c020 := a020 + c021 := a021 + c100, c101, c110, c111, c120, c121 := fp6Neg(a100, a101, a110, a111, a120, a121) + } + + /// @notice Computes the square of a Fp12 element in the cyclotomic subgroup. + /// @dev See https://eprint.iacr.org/2010/354.pdf for further details. + /// @param a000, a001, a010, a011, a020, a021, a100, a101, a110, a111, a120, a121 The coefficients of the Fp12 element A. + /// @return c000, c001, c010, c011, c020, c021, c100, c101, c110, c111, c120, c121 The coefficients of the element C = A^2. + function fp12CyclotomicSquare(a000, a001, a010, a011, a020, a021, a100, a101, a110, a111, a120, a121) -> c000, c001, c010, c011, c020, c021, c100, c101, c110, c111, c120, c121 { + let t00, t01 := fp2Mul(a110, a111, a110, a111) + let t10, t11 := fp2Mul(a000, a001, a000, a001) + let t20, t21 := fp2Add(a110, a111, a000, a001) + t20, t21 := fp2Mul(t20, t21, t20, t21) + t20, t21 := fp2Sub(t20, t21, t00, t01) + t20, t21 := fp2Sub(t20, t21, t10, t11) + let t30, t31 := fp2Mul(a020, a021, a020, a021) + let t40, t41 := fp2Mul(a100, a101, a100, a101) + let t50, t51 := fp2Add(a020, a021, a100, a101) + t50, t51 := fp2Mul(t50, t51, t50, t51) + t50, t51 := fp2Sub(t50, t51, t30, t31) + t50, t51 := fp2Sub(t50, t51, t40, t41) + let t60, t61 := fp2Mul(a120, a121, a120, a121) + let t70, t71 := fp2Mul(a010, a011, a010, a011) + let t80, t81 := fp2Add(a120, a121, a010, a011) + t80, t81 := fp2Mul(t80, t81, t80, t81) + t80, t81 := fp2Sub(t80, t81, t60, t61) + t80, t81 := fp2Sub(t80, t81, t70, t71) + t80, t81 := mulByXi(t80, t81) + t00, t01 := mulByXi(t00, t01) + t00, t01 := fp2Add(t00, t01, t10, t11) + t30, t31 := mulByXi(t30, t31) + t30, t31 := fp2Add(t30, t31, t40, t41) + t60, t61 := mulByXi(t60, t61) + t60, t61 := fp2Add(t60, t61, t70, t71) + + c000, c001 := fp2Sub(t00, t01, a000, a001) + c000, c001 := fp2Add(c000, c001, c000, c001) + c000, c001 := fp2Add(c000, c001, t00, t01) + + c010, c011 := fp2Sub(t30, t31, a010, a011) + c010, c011 := fp2Add(c010, c011, c010, c011) + c010, c011 := fp2Add(c010, c011, t30, t31) + + c020, c021 := fp2Sub(t60, t61, a020, a021) + c020, c021 := fp2Add(c020, c021, c020, c021) + c020, c021 := fp2Add(c020, c021, t60, t61) + + c100, c101 := fp2Add(t80, t81, a100, a101) + c100, c101 := fp2Add(c100, c101, c100, c101) + c100, c101 := fp2Add(c100, c101, t80, t81) + + c110, c111 := fp2Add(t20, t21, a110, a111) + c110, c111 := fp2Add(c110, c111, c110, c111) + c110, c111 := fp2Add(c110, c111, t20, t21) + + c120, c121 := fp2Add(t50, t51, a120, a121) + c120, c121 := fp2Add(c120, c121, c120, c121) + c120, c121 := fp2Add(c120, c121, t50, t51) + } + + /// @notice Computes the exponentiation of a Fp12 element in the cyclotomic subgroup to 2n. + /// @dev We compute A^2n as n cyclotomic squares. + /// @param a000, a001, a010, a011, a020, a021, a100, a101, a110, a111, a120, a121 The coefficients of the Fp12 element A. + /// @return c000, c001, c010, c011, c020, c021, c100, c101, c110, c111, c120, c121 The coefficients of the element C = A^2n. + function nSquare(a000, a001, a010, a011, a020, a021, a100, a101, a110, a111, a120, a121, n) -> c000, c001, c010, c011, c020, c021, c100, c101, c110, c111, c120, c121 { + c000 := a000 + c001 := a001 + c010 := a010 + c011 := a011 + c020 := a020 + c021 := a021 + c100 := a100 + c101 := a101 + c110 := a110 + c111 := a111 + c120 := a120 + c121 := a121 + for { let i := 0 } lt(i, n) { i := add(i, 1) } { + c000, c001, c010, c011, c020, c021, c100, c101, c110, c111, c120, c121 := fp12CyclotomicSquare(c000, c001, c010, c011, c020, c021, c100, c101, c110, c111, c120, c121) + } + } + + // FROBENIUS + + + /// @notice Computes the exponentiation of a Fp12 element to p. + /// @dev Algorithm 28 in: https://eprint.iacr.org/2010/354.pdf. + /// @param a000, a001, a010, a011, a020, a021, a100, a101, a110, a111, a120, a121 The coefficients of the Fp12 element A. + /// @return c000, c001, c010, c011, c020, c021, c100, c101, c110, c111, c120, c121 The coefficients of the element C = A^p. + function frobenius(a000, a001, a010, a011, a020, a021, a100, a101, a110, a111, a120, a121) -> c00, c01, c10, c11, c20, c21, c30, c31, c40, c41, c50, c51 { + let t10, t11 := fp2Conjugate(a000, a001) + let t20, t21 := fp2Conjugate(a100, a101) + let t30, t31 := fp2Conjugate(a010, a011) + let t40, t41 := fp2Conjugate(a110, a111) + let t50, t51 := fp2Conjugate(a020, a021) + let t60, t61 := fp2Conjugate(a120, a121) + + t20, t21 := mulByGamma11(t20, t21) + t30, t31 := mulByGamma12(t30, t31) + t40, t41 := mulByGamma13(t40, t41) + t50, t51 := mulByGamma14(t50, t51) + t60, t61 := mulByGamma15(t60, t61) + + c00 := t10 + c01 := t11 + c10 := t30 + c11 := t31 + c20 := t50 + c21 := t51 + c30 := t20 + c31 := t21 + c40 := t40 + c41 := t41 + c50 := t60 + c51 := t61 + } + + /// @notice Computes the exponentiation of a Fp12 element to p^2. + /// @dev Algorithm 29 in: https://eprint.iacr.org/2010/354.pdf. + /// @param a000, a001, a010, a011, a020, a021, a100, a101, a110, a111, a120, a121 The coefficients of the Fp12 element A. + /// @return c000, c001, c010, c011, c020, c021, c100, c101, c110, c111, c120, c121 The coefficients of the element C = A^(p^2). + function frobeniusSquare(a000, a001, a010, a011, a020, a021, a100, a101, a110, a111, a120, a121) -> c00, c01, c10, c11, c20, c21, c30, c31, c40, c41, c50, c51 { + let t10 := a000 + let t11 := a001 + let t20, t21 := mulByGamma21(a100, a101) + let t30, t31 := mulByGamma22(a010, a011) + let t40, t41 := mulByGamma23(a110, a111) + let t50, t51 := mulByGamma24(a020, a021) + let t60, t61 := mulByGamma25(a120, a121) + + c00 := t10 + c01 := t11 + c10 := t30 + c11 := t31 + c20 := t50 + c21 := t51 + c30 := t20 + c31 := t21 + c40 := t40 + c41 := t41 + c50 := t60 + c51 := t61 + } + + /// @notice Computes the exponentiation of a Fp12 element to p^3. + /// @dev @dev Algorithm 29 in: https://eprint.iacr.org/2010/354.pdf. + /// @param a000, a001, a010, a011, a020, a021, a100, a101, a110, a111, a120, a121 The coefficients of the Fp12 element A. + /// @return c000, c001, c010, c011, c020, c021, c100, c101, c110, c111, c120, c121 The coefficients of the element C = A^(p^3). + function frobeniusCube(a000, a001, a010, a011, a020, a021, a100, a101, a110, a111, a120, a121) -> c00, c01, c10, c11, c20, c21, c30, c31, c40, c41, c50, c51 { + let t10, t11 := fp2Conjugate(a000, a001) + let t20, t21 := fp2Conjugate(a100, a101) + let t30, t31 := fp2Conjugate(a010, a011) + let t40, t41 := fp2Conjugate(a110, a111) + let t50, t51 := fp2Conjugate(a020, a021) + let t60, t61 := fp2Conjugate(a120, a121) + + t20, t21 := mulByGamma31(t20, t21) + t30, t31 := mulByGamma32(t30, t31) + t40, t41 := mulByGamma33(t40, t41) + t50, t51 := mulByGamma34(t50, t51) + t60, t61 := mulByGamma35(t60, t61) + + c00 := t10 + c01 := t11 + c10 := t30 + c11 := t31 + c20 := t50 + c21 := t51 + c30 := t20 + c31 := t21 + c40 := t40 + c41 := t41 + c50 := t60 + c51 := t61 + } + + // GAMMA_1_i + /// @notice Computes the multiplication between a fp2 element by the constants g_1,i. + /// @dev Where g_1,i = u^(i(p-1)/6) + /// @dev This value was precomputed using Python. Already in montgomery form. + /// @dev See https://eprint.iacr.org/2010/354.pdf for further details. + /// @params a00, a01 The coefficients of the Fp2 element A. + /// @return c00, c01 The coefficients of the element C = A*g_1,i. + + function mulByGamma11(a00, a01) -> c00, c01 { + let g00 := 1334504125441109323775816677333762124980877086439557453392802825656291576071 + let g01 := 7532670101108748540749979597679923402841328813027773483599019704565791010162 + c00, c01 := fp2Mul(a00, a01, g00, g01) + } + + function mulByGamma12(a00, a01) -> c00, c01 { + let g00 := 11461073415658098971834280704587444395456423268720245247603935854280982113072 + let g01 := 17373957475705492831721812124331982823197004514106338927670775596783233550167 + c00, c01 := fp2Mul(a00, a01, g00, g01) + } + + function mulByGamma13(a00, a01) -> c00, c01 { + let g00 := 16829996427371746075450799880956928810557034522864196246648550205375670302249 + let g01 := 20140510615310063345578764457068708762835443761990824243702724480509675468743 + c00, c01 := fp2Mul(a00, a01, g00, g01) + } + + function mulByGamma14(a00, a01) -> c00, c01 { + let g00 := 9893659366031634526915473325149983243417508801286144596494093251884139331218 + let g01 := 16514792769865828027011044701859348114858257981779976519405133026725453154633 + c00, c01 := fp2Mul(a00, a01, g00, g01) + } + + function mulByGamma15(a00, a01) -> c00, c01 { + let g00 := 8443299194457421137480282511969901974227997168695360756777672575877693116391 + let g01 := 21318636632361225103955470331868462398471880609949088574192481281746934874025 + c00, c01 := fp2Mul(a00, a01, g00, g01) + } + + // GAMMA_2_i + /// @notice Computes the multiplication between a fp2 element by the constants g_2,i. + /// @dev Where g_2,i = g_1,i * g'_1,i + /// @dev This value was precomputed using Python. Already in montgomery form. + /// @dev See https://eprint.iacr.org/2010/354.pdf for further details. + /// @params a00, a01 The coefficients of the Fp2 element A. + /// @return c00, c01 The coefficients of the element C = A*g_2,i. + + function mulByGamma21(a00, a01) -> c00, c01 { + let g0 := 1881798392815877688876180778159931906057091683336018750908411925848733129714 + c00, c01 := fp2ScalarMul(a00, a01, g0) + } + + function mulByGamma22(a00, a01) -> c00, c01 { + let g0 := 17419166386535333598783630241015674584964973961482396687585055285806960741276 + c00, c01 := fp2ScalarMul(a00, a01, g0) + } + + function mulByGamma23(a00, a01) -> c00, c01 { + let g0 := 15537367993719455909907449462855742678907882278146377936676643359958227611562 + c00, c01 := fp2ScalarMul(a00, a01, g0) + } + + function mulByGamma24(a00, a01) -> c00, c01 { + let g0 := 20006444479023397533370224967097343182639219473961804911780625968796493078869 + c00, c01 := fp2ScalarMul(a00, a01, g0) + } + + function mulByGamma25(a00, a01) -> c00, c01 { + let g0 := 4469076485303941623462775504241600503731337195815426975103982608838265467307 + c00, c01 := fp2ScalarMul(a00, a01, g0) + } + + // GAMMA_3_i + /// @notice Computes the multiplication between a fp2 element by the constants g_3,i. + /// @dev Where g_3,i = g_1,i * g_2,i + /// @dev This value was precomputed using Python. Already in montgomery form. + /// @dev See https://eprint.iacr.org/2010/354.pdf for further details. + /// @params a00, a01 The coefficients of the Fp2 element A. + /// @return c00, c01 The coefficients of the element C = A*g_3,i. + + function mulByGamma31(a00, a01) -> c00, c01 { + let g00 := 3649295186494431467217240962842301358951278585756714214031945394966344685949 + let g01 := 17372117152826387298350653207345606612066102743297871578090761045572893546809 + c00, c01 := fp2Mul(a00, a01, g00, g01) + } + + function mulByGamma32(a00, a01) -> c00, c01 { + let g00 := 14543349330631744552586812320441124107441202078168618766450326117520897829805 + let g01 := 4646831431411403714092965637071058625728899792817054432901795759277546050476 + c00, c01 := fp2Mul(a00, a01, g00, g01) + } + + function mulByGamma33(a00, a01) -> c00, c01 { + let g00 := 5058246444467529146795605864300346278139276634433627416040487689269555906334 + let g01 := 1747732256529211876667641288188566325860867395306999418986313414135550739840 + c00, c01 := fp2Mul(a00, a01, g00, g01) + } + + function mulByGamma34(a00, a01) -> c00, c01 { + let g00 := 3025265262868802913511075437173590487338001780554453930995247874855578067679 + let g01 := 10425289180741305073643362413949631488281652900778689227251281048515799234257 + c00, c01 := fp2Mul(a00, a01, g00, g01) + } + + function mulByGamma35(a00, a01) -> c00, c01 { + let g00 := 9862576063628467829192720579684130652367741026604221989510773554027227469215 + let g01 := 16681752610922605480353377694363181135019829138759259603037557916788351015335 + c00, c01 := fp2Mul(a00, a01, g00, g01) + } + + // PAIRING FUNCTIONS + + /// @notice Computes the double of a G2 point and its tangent line. + /// @dev The point is in projective coordinates. + /// @dev See https://eprint.iacr.org/2013/722.pdf for further details. + /// @params xq0, xq1 The coefficients of the Fp2 X coordinate of the Q point. + /// @params yq0, yq1 The coefficients of the Fp2 X coordinate of the Q point. + /// @params zq0, zq1 The coefficients of the Fp2 X coordinate of the Q point. + /// @return xt0, xt1 The coefficients of the Fp2 X coordinate of T = 2Q. + /// @return yt0, yt1 The coefficients of the Fp2 X coordinate of T = 2Q. + /// @return zt0, zt1 The coefficients of the Fp2 X coordinate of T = 2Q. + /// @return l00, l01, l10, l11, l20, l21, l30, l31, l40, l41, l50, l51 The coefficients of the tangent line to Q. + function doubleStep(xq0, xq1, yq0, yq1, zq0, zq1) -> l00, l01, l10, l11, l20, l21, l30, l31, l40, l41, l50, l51, xt0, xt1, yt0, yt1, zt0, zt1 { + let zero := 0 + let twoInv := MONTGOMERY_TWO_INV() + let t00, t01 := fp2Mul(xq0, xq1, yq0, yq1) + let t10, t11 := fp2ScalarMul(t00, t01, twoInv) + let t20, t21 := fp2Mul(yq0, yq1, yq0, yq1) + let t30, t31 := fp2Mul(zq0, zq1, zq0, zq1) + let t40, t41 := fp2Add(t30, t31, t30, t31) + t40, t41 := fp2Add(t40, t41, t30, t31) + let t50, t51 := MONTGOMERY_TWISTED_CURVE_COEFFS() + t50, t51 := fp2Mul(t40, t41, t50, t51) + let t60, t61 :=fp2Add(t50, t51, t50, t51) + t60, t61 := fp2Add(t60, t61, t50, t51) + let t70, t71 := fp2Add(t20, t21, t60, t61) + t70, t71 := fp2ScalarMul(t70, t71, twoInv) + let t80, t81 := fp2Add(yq0, yq1, zq0, zq1) + t80, t81 := fp2Mul(t80, t81, t80, t81) + let t90, t91 := fp2Add(t30, t31, t20, t21) + t80, t81 := fp2Sub(t80, t81, t90, t91) + let t100, t101 := fp2Sub(t50, t51, t20, t21) + let t110, t111 := fp2Mul(xq0, xq1, xq0, xq1) + let t120, t121 := fp2Mul(t50, t51, t50, t51) + let t130, t131 := fp2Add(t120, t121, t120, t121) + t130, t131 := fp2Add(t130, t131, t120, t121) + + // l0 + l00 := t80 + l01 := t81 + l10 := zero + l11 := zero + l20 := zero + l21 := zero + + // l1 + l30, l31 := fp2Add(t110, t111, t110, t111) + l30, l31 := fp2Add(l30, l31, t110, t111) + + // l2 + l40 := t100 + l41 := t101 + + l50 := zero + l51 := zero + + // Tx + xt0, xt1 := fp2Sub(t20, t21, t60, t61) + xt0, xt1 := fp2Mul(xt0, xt1, t10, t11) + + // Ty + yt0, yt1 := fp2Mul(t70, t71, t70, t71) + yt0, yt1 := fp2Sub(yt0, yt1, t130, t131) + + // Tz + zt0, zt1 := fp2Mul(t20, t21, t80, t81) + } + + /// @notice Computes the addition of two G2 points and the line through them. + /// @dev It's called mixed addition because Q is in affine coordinates and T in projective coordinates. + /// @dev The two points must be different, in this Q, which is G2 group generator of an order of 21888242871839275222246405745257275088548364400416034343698204186575808495617, is doubled and added. So will never reach Q. + /// @dev See https://eprint.iacr.org/2013/722.pdf for further details. + /// @dev Disclaimer: The algorithm described in the paper is has a typo, the (`l00`,`l01`) coefficients should not be negated. + /// @params xq0, xq1 The coefficients of the Fp2 X coordinate of the Q point. + /// @params yq0, yq1 The coefficients of the Fp2 Y coordinate of the Q point. + /// @params xt0, xt1 The coefficients of the Fp2 X coordinate of the T point. + /// @params yt0, yt1 The coefficients of the Fp2 Y coordinate of the T point. + /// @params zt0, zt1 The coefficients of the Fp2 Z coordinate of the T point. + /// @return xc0, xc1 The coefficients of the Fp2 X coordinate of C = Q + T. + /// @return yc0, yc1 The coefficients of the Fp2 X coordinate of C = Q + T. + /// @return zc0, zc1 The coefficients of the Fp2 X coordinate of C = Q + T. + /// @return l00, l01, l10, l11, l20, l21, l30, l31, l40, l41, l50, l51 The coefficients of the line through T and Q. + function mixedAdditionStep(xq0, xq1, yq0, yq1, xt0, xt1, yt0, yt1, zt0, zt1) -> l00, l01, l10, l11, l20, l21, l30, l31, l40, l41, l50, l51, xc0, xc1, yc0, yc1, zc0, zc1 { + let zero := 0 + let t00, t01 := fp2Mul(yq0,yq1,zt0,zt1) + let t10, t11 := fp2Sub(yt0, yt1, t00, t01) + t00, t01 := fp2Mul(xq0, xq1, zt0, zt1) + let t20, t21 := fp2Sub(xt0, xt1, t00, t01) + let t30, t31 := fp2Mul(t10, t11, t10, t11) + let t40, t41 := fp2Mul(t20, t21, t20, t21) + let t50, t51 := fp2Mul(t20, t21, t40, t41) + let t60, t61 := fp2Mul(zt0, zt1, t30, t31) + let t70, t71 := fp2Mul(xt0, xt1, t40, t41) + t00, t01 := fp2Add(t70, t71, t70, t71) + let t80, t81 := fp2Add(t50, t51, t60, t61) + t80, t81 := fp2Sub(t80, t81, t00, t01) + t00, t01 := fp2Mul(yt0, yt1, t50, t51) + + // Xc0 + xc0, xc1 := fp2Mul(t20, t21, t80, t81) + + // Yc0 + yc0, yc1 := fp2Sub(t70, t71, t80, t81) + yc0, yc1 := fp2Mul(yc0, yc1, t10, t11) + yc0, yc1 := fp2Sub(yc0, yc1, t00, t01) + + // Zc0 + zc0, zc1 := fp2Mul(t50, t51, zt0, zt1) + t00, t01 := fp2Mul(t20, t21, yq0, yq1) + let t90, t91 := fp2Mul(xq0, xq1, t10, t11) + t90, t91 := fp2Sub(t90, t91, t00, t01) + + // l0 + l00 := t20 + l01 := t21 + l10 := zero + l11 := zero + l20 := zero + l21 := zero + + // l1 + l30 := t10 + l31 := t11 + + // l2 + l40 := t90 + l41 := t91 + l50 := zero + l51 := zero + } + + /// @notice Computes the line through two G2 points. + /// @dev Like in the mixedAdditionStep, Q is in affine coordinates and T in projective coordinates. + /// @dev The two points must be different, in this Q, which is G2 group generator of an order of 21888242871839275222246405745257275088548364400416034343698204186575808495617, is doubled and added. So will never reach Q. + /// @params xq0, xq1 The coefficients of the Fp2 X coordinate of the Q point. + /// @params yq0, yq1 The coefficients of the Fp2 Y coordinate of the Q point. + /// @params xt0, xt1 The coefficients of the Fp2 X coordinate of the T point. + /// @params yt0, yt1 The coefficients of the Fp2 Y coordinate of the T point. + /// @params zt0, zt1 The coefficients of the Fp2 Z coordinate of the T point. + /// @return l00, l01, l10, l11, l20, l21, l30, l31, l40, l41, l50, l51 The coefficients of the line through T and Q. + function computeLine(xq0, xq1, yq0, yq1, xt0, xt1, yt0, yt1, zt0, zt1) -> l00, l01, l10, l11, l20, l21, l30, l31, l40, l41, l50, l51 { + let zero := 0 + let t00, t01 := fp2Mul(yq0,yq1,zt0,zt1) + let t10, t11 := fp2Sub(yt0, yt1, t00, t01) + t00, t01 := fp2Mul(xq0, xq1, zt0, zt1) + let t20, t21 := fp2Sub(xt0, xt1, t00, t01) + let t30, t31 := fp2Mul(t20, t21, yq0, yq1) + let t40, t41 := fp2Mul(xq0, xq1, t10, t11) + t40, t41 := fp2Sub(t40, t41, t30, t31) + + // l0 + l00 := t20 + l01 := t21 + l10 := zero + l11 := zero + l20 := zero + l21 := zero + + // l1 + l30, l31 := fp2Neg(t10, t11) + + // l2 + l40 := t40 + l41 := t41 + l50 := zero + l51 := zero + } + + /// @notice Computes the final exponentiation to the result given by the Millers Loop. + /// @dev It computes the exponentiation of a Fp12 elemento to e, with e = (p^12 -1)/r + /// @dev We can split this exponentitation in three parts: e = (p^6 - 1)(p^2 + 1)((p^4 - p^2 + 1)/r) + /// @dev The first 2 parts are easy to compute using the Frobenius operator. + /// @dev To calculate this we use the first 5 lines of Algorithm 31 in: https://eprint.iacr.org/2010/354.pdf + /// @dev For the hard part we use the Fuentes et al. method. Algorithm 6 in: https://eprint.iacr.org/2015/192.pdf + /// @params a000, a001, a010, a011, a020, a021, a100, a101, a110, a111, a120, a121 The coefficients of the Fp12 element A. + /// @return f000, f001, f010, f011, f020, f021, f100, f101, f110, f111, f120, f121 The coefficients of A^(s*((p^12 -1)/r)) where s is not divisible by r. + function finalExponentiation(a000, a001, a010, a011, a020, a021, a100, a101, a110, a111, a120, a121) -> f000, f001, f010, f011, f020, f021, f100, f101, f110, f111, f120, f121 { + f000 := a000 + f001 := a001 + f010 := a010 + f011 := a011 + f020 := a020 + f021 := a021 + f100 := a100 + f101 := a101 + f110 := a110 + f111 := a111 + f120 := a120 + f121 := a121 + + // Easy Part + let t0000, t0001, t0010, t0011, t0020, t0021, t0100, t0101, t0110, t0111, t0120, t0121 := fp12Conjugate(f000, f001, f010, f011, f020, f021, f100, f101, f110, f111, f120, f121) + f000, f001, f010, f011, f020, f021, f100, f101, f110, f111, f120, f121 := fp12Inv(f000, f001, f010, f011, f020, f021, f100, f101, f110, f111, f120, f121) + t0000, t0001, t0010, t0011, t0020, t0021, t0100, t0101, t0110, t0111, t0120, t0121 := fp12Mul(t0000, t0001, t0010, t0011, t0020, t0021, t0100, t0101, t0110, t0111, t0120, t0121, f000, f001, f010, f011, f020, f021, f100, f101, f110, f111, f120, f121) + let t1000, t1001, t1010, t1011, t1020, t1021, t1100, t1101, t1110, t1111, t1120, t1121 := frobeniusSquare(t0000, t0001, t0010, t0011, t0020, t0021, t0100, t0101, t0110, t0111, t0120, t0121) + f000, f001, f010, f011, f020, f021, f100, f101, f110, f111, f120, f121 := fp12Mul(t1000, t1001, t1010, t1011, t1020, t1021, t1100, t1101, t1110, t1111, t1120, t1121, t0000, t0001, t0010, t0011, t0020, t0021, t0100, t0101, t0110, t0111, t0120, t0121) + + // Hard Part + t0000, t0001, t0010, t0011, t0020, t0021, t0100, t0101, t0110, t0111, t0120, t0121 := fp12Expt(f000, f001, f010, f011, f020, f021, f100, f101, f110, f111, f120, f121) + t0000, t0001, t0010, t0011, t0020, t0021, t0100, t0101, t0110, t0111, t0120, t0121 := fp12Conjugate(t0000, t0001, t0010, t0011, t0020, t0021, t0100, t0101, t0110, t0111, t0120, t0121) + t0000, t0001, t0010, t0011, t0020, t0021, t0100, t0101, t0110, t0111, t0120, t0121 := fp12CyclotomicSquare(t0000, t0001, t0010, t0011, t0020, t0021, t0100, t0101, t0110, t0111, t0120, t0121) + t1000, t1001, t1010, t1011, t1020, t1021, t1100, t1101, t1110, t1111, t1120, t1121 := fp12CyclotomicSquare(t0000, t0001, t0010, t0011, t0020, t0021, t0100, t0101, t0110, t0111, t0120, t0121) + t1000, t1001, t1010, t1011, t1020, t1021, t1100, t1101, t1110, t1111, t1120, t1121 := fp12Mul(t0000, t0001, t0010, t0011, t0020, t0021, t0100, t0101, t0110, t0111, t0120, t0121, t1000, t1001, t1010, t1011, t1020, t1021, t1100, t1101, t1110, t1111, t1120, t1121) + let t2000, t2001, t2010, t2011, t2020, t2021, t2100, t2101, t2110, t2111, t2120, t2121 := fp12Expt(t1000, t1001, t1010, t1011, t1020, t1021, t1100, t1101, t1110, t1111, t1120, t1121) + t2000, t2001, t2010, t2011, t2020, t2021, t2100, t2101, t2110, t2111, t2120, t2121 := fp12Conjugate(t2000, t2001, t2010, t2011, t2020, t2021, t2100, t2101, t2110, t2111, t2120, t2121) + let t3000, t3001, t3010, t3011, t3020, t3021, t3100, t3101, t3110, t3111, t3120, t3121 := fp12Conjugate(t1000, t1001, t1010, t1011, t1020, t1021, t1100, t1101, t1110, t1111, t1120, t1121) + t1000, t1001, t1010, t1011, t1020, t1021, t1100, t1101, t1110, t1111, t1120, t1121 := fp12Mul(t2000, t2001, t2010, t2011, t2020, t2021, t2100, t2101, t2110, t2111, t2120, t2121, t3000, t3001, t3010, t3011, t3020, t3021, t3100, t3101, t3110, t3111, t3120, t3121) + t3000, t3001, t3010, t3011, t3020, t3021, t3100, t3101, t3110, t3111, t3120, t3121 := fp12CyclotomicSquare(t2000, t2001, t2010, t2011, t2020, t2021, t2100, t2101, t2110, t2111, t2120, t2121) + let t4000, t4001, t4010, t4011, t4020, t4021, t4100, t4101, t4110, t4111, t4120, t4121 := fp12Expt(t3000, t3001, t3010, t3011, t3020, t3021, t3100, t3101, t3110, t3111, t3120, t3121) + t4000, t4001, t4010, t4011, t4020, t4021, t4100, t4101, t4110, t4111, t4120, t4121 := fp12Mul(t4000, t4001, t4010, t4011, t4020, t4021, t4100, t4101, t4110, t4111, t4120, t4121, t1000, t1001, t1010, t1011, t1020, t1021, t1100, t1101, t1110, t1111, t1120, t1121) + t3000, t3001, t3010, t3011, t3020, t3021, t3100, t3101, t3110, t3111, t3120, t3121 := fp12Mul(t4000, t4001, t4010, t4011, t4020, t4021, t4100, t4101, t4110, t4111, t4120, t4121, t0000, t0001, t0010, t0011, t0020, t0021, t0100, t0101, t0110, t0111, t0120, t0121) + t0000, t0001, t0010, t0011, t0020, t0021, t0100, t0101, t0110, t0111, t0120, t0121 := fp12Mul(t2000, t2001, t2010, t2011, t2020, t2021, t2100, t2101, t2110, t2111, t2120, t2121, t4000, t4001, t4010, t4011, t4020, t4021, t4100, t4101, t4110, t4111, t4120, t4121) + t0000, t0001, t0010, t0011, t0020, t0021, t0100, t0101, t0110, t0111, t0120, t0121 := fp12Mul(t0000, t0001, t0010, t0011, t0020, t0021, t0100, t0101, t0110, t0111, t0120, t0121, f000, f001, f010, f011, f020, f021, f100, f101, f110, f111, f120, f121) + t2000, t2001, t2010, t2011, t2020, t2021, t2100, t2101, t2110, t2111, t2120, t2121 := frobenius(t3000, t3001, t3010, t3011, t3020, t3021, t3100, t3101, t3110, t3111, t3120, t3121) + t0000, t0001, t0010, t0011, t0020, t0021, t0100, t0101, t0110, t0111, t0120, t0121 := fp12Mul(t2000, t2001, t2010, t2011, t2020, t2021, t2100, t2101, t2110, t2111, t2120, t2121, t0000, t0001, t0010, t0011, t0020, t0021, t0100, t0101, t0110, t0111, t0120, t0121) + t2000, t2001, t2010, t2011, t2020, t2021, t2100, t2101, t2110, t2111, t2120, t2121 := frobeniusSquare(t4000, t4001, t4010, t4011, t4020, t4021, t4100, t4101, t4110, t4111, t4120, t4121) + t0000, t0001, t0010, t0011, t0020, t0021, t0100, t0101, t0110, t0111, t0120, t0121 := fp12Mul(t2000, t2001, t2010, t2011, t2020, t2021, t2100, t2101, t2110, t2111, t2120, t2121, t0000, t0001, t0010, t0011, t0020, t0021, t0100, t0101, t0110, t0111, t0120, t0121) + t2000, t2001, t2010, t2011, t2020, t2021, t2100, t2101, t2110, t2111, t2120, t2121 := fp12Conjugate(f000, f001, f010, f011, f020, f021, f100, f101, f110, f111, f120, f121) + t2000, t2001, t2010, t2011, t2020, t2021, t2100, t2101, t2110, t2111, t2120, t2121 := fp12Mul(t2000, t2001, t2010, t2011, t2020, t2021, t2100, t2101, t2110, t2111, t2120, t2121, t3000, t3001, t3010, t3011, t3020, t3021, t3100, t3101, t3110, t3111, t3120, t3121) + t2000, t2001, t2010, t2011, t2020, t2021, t2100, t2101, t2110, t2111, t2120, t2121 := frobeniusCube(t2000, t2001, t2010, t2011, t2020, t2021, t2100, t2101, t2110, t2111, t2120, t2121) + f000, f001, f010, f011, f020, f021, f100, f101, f110, f111, f120, f121 := fp12Mul(t2000, t2001, t2010, t2011, t2020, t2021, t2100, t2101, t2110, t2111, t2120, t2121, t0000, t0001, t0010, t0011, t0020, t0021, t0100, t0101, t0110, t0111, t0120, t0121) + } + + /// @notice Computes the Millers Loop for the optimal ate pairing. + /// @dev Algorithm 1 in: https://eprint.iacr.org/2010/354.pdf + /// @dev It takes two points: P that belongs to the curve G1, in affine coordinates (Fp elements) + /// @dev Point Q belongs to the twisted G2 curve, in affine coordinates (Fp2 elements) + /// @params xp, yp The coordinates of the point P. + /// @params xq0, xq1 The coefficients of the X coordinate of point Q. + /// @params yq0, yq1 The coefficients of the Y coordinate of point Q. + /// @return f000, f001, f010, f011, f020, f021, f100, f101, f110, f111, f120, f121 The Fp12 element result of the Miller Loop + function millerLoop(xq0, xq1, yq0, yq1, xp, yp) -> f000, f001, f010, f011, f020, f021, f100, f101, f110, f111, f120, f121 { + let t00, t01, t10, t11, t20, t21 := g2ProjectiveFromAffine(xq0, xq1, yq0, yq1) + let mq00, mq01, mq10, mq11 := g2AffineNeg(xq0, xq1, yq0, yq1) + f000, f001, f010, f011, f020, f021, f100, f101, f110, f111, f120, f121 := FP12_ONE() + let naf := NAF_REPRESENTATIVE() + let n_iter := 63 + let l00, l01, l10, l11, l20, l21, l30, l31, l40, l41, l50, l51 + let myp := montgomerySub(0, yp) + let mxp := montgomerySub(0, xp) + + // Computes the first iteration of Millers loop outside to avoid unecesariy square + // NAF[64] == 0 + l00, l01, l10, l11, l20, l21, l30, l31, l40, l41, l50, l51, t00, t01, t10, t11, t20, t21 := doubleStep(t00, t01, t10, t11, t20, t21) + l00, l01 := fp2ScalarMul(l00, l01, myp) + l30, l31 := fp2ScalarMul(l30, l31, xp) + f000, f001, f010, f011, f020, f021, f100, f101, f110, f111, f120, f121 := fp12Mul(f000, f001, f010, f011, f020, f021, f100, f101, f110, f111, f120, f121, l00, l01, l10, l11, l20, l21, l30, l31, l40, l41, l50, l51) + + // Computes the second iteration of Millers loop outside + // NAF[63] == -1. + // Here T = 2Q, so doing a double step and a mixed addition step with -Q looks like: (2(2Q)-Q) = 3Q. + // This is equivalent to a mixed addition step with Q: (2Q + Q) = 3Q + f000, f001, f010, f011, f020, f021, f100, f101, f110, f111, f120, f121 := fp12Mul(f000, f001, f010, f011, f020, f021, f100, f101, f110, f111, f120, f121,f000, f001, f010, f011, f020, f021, f100, f101, f110, f111, f120, f121) + l00, l01, l10, l11, l20, l21, l30, l31, l40, l41, l50, l51 := computeLine(mq00, mq01, mq10, mq11, t00, t01, t10, t11, t20, t21) + l00, l01 := fp2ScalarMul(l00, l01, yp) + l30, l31 := fp2ScalarMul(l30, l31, xp) + f000, f001, f010, f011, f020, f021, f100, f101, f110, f111, f120, f121 := fp12Mul(f000, f001, f010, f011, f020, f021, f100, f101, f110, f111, f120, f121, l00, l01, l10, l11, l20, l21, l30, l31, l40, l41, l50, l51) + l00, l01, l10, l11, l20, l21, l30, l31, l40, l41, l50, l51, t00, t01, t10, t11, t20, t21 := mixedAdditionStep(xq0, xq1, yq0, yq1, t00, t01, t10, t11, t20, t21) + l00, l01 := fp2ScalarMul(l00, l01, yp) + l30, l31 := fp2ScalarMul(l30, l31, mxp) + f000, f001, f010, f011, f020, f021, f100, f101, f110, f111, f120, f121 := fp12Mul(f000, f001, f010, f011, f020, f021, f100, f101, f110, f111, f120, f121, l00, l01, l10, l11, l20, l21, l30, l31, l40, l41, l50, l51) + + for {let i := 0} lt(i, n_iter) { i := add(i, 1) } { + f000, f001, f010, f011, f020, f021, f100, f101, f110, f111, f120, f121 := fp12Square(f000, f001, f010, f011, f020, f021, f100, f101, f110, f111, f120, f121) + + l00, l01, l10, l11, l20, l21, l30, l31, l40, l41, l50, l51, t00, t01, t10, t11, t20, t21 := doubleStep(t00, t01, t10, t11, t20, t21) + l00, l01 := fp2ScalarMul(l00, l01, myp) + l30, l31 := fp2ScalarMul(l30, l31, xp) + f000, f001, f010, f011, f020, f021, f100, f101, f110, f111, f120, f121 := fp12Mul(f000, f001, f010, f011, f020, f021, f100, f101, f110, f111, f120, f121, l00, l01, l10, l11, l20, l21, l30, l31, l40, l41, l50, l51) + + // naf digit = 1 + if and(naf, 1) { + l00, l01, l10, l11, l20, l21, l30, l31, l40, l41, l50, l51, t00, t01, t10, t11, t20, t21 := mixedAdditionStep(xq0, xq1, yq0, yq1, t00, t01, t10, t11, t20, t21) + l00, l01 := fp2ScalarMul(l00, l01, yp) + l30, l31 := fp2ScalarMul(l30, l31, mxp) + f000, f001, f010, f011, f020, f021, f100, f101, f110, f111, f120, f121 := fp12Mul(f000, f001, f010, f011, f020, f021, f100, f101, f110, f111, f120, f121, l00, l01, l10, l11, l20, l21, l30, l31, l40, l41, l50, l51) + } + + // naf digit = -1 + if and(naf, 2) { + l00, l01, l10, l11, l20, l21, l30, l31, l40, l41, l50, l51, t00, t01, t10, t11, t20, t21 := mixedAdditionStep(mq00, mq01, mq10, mq11, t00, t01, t10, t11, t20, t21) + l00, l01 := fp2ScalarMul(l00, l01, yp) + l30, l31 := fp2ScalarMul(l30, l31, mxp) + f000, f001, f010, f011, f020, f021, f100, f101, f110, f111, f120, f121 := fp12Mul(f000, f001, f010, f011, f020, f021, f100, f101, f110, f111, f120, f121, l00, l01, l10, l11, l20, l21, l30, l31, l40, l41, l50, l51) + } + + naf := shr(2, naf) + } + + let r00, r01 := fp2Conjugate(xq0, xq1) + let r10, r11 := fp2Conjugate(yq0, yq1) + r00, r01 := mulByGamma12(r00, r01) + r10, r11 := mulByGamma13(r10, r11) + + let r20, r21 := mulByGamma22(xq0, xq1) + let r30, r31 := mulByGamma23(yq0, yq1) + r30, r31 := fp2Neg(r30, r31) + + l00, l01, l10, l11, l20, l21, l30, l31, l40, l41, l50, l51, t00, t01, t10, t11, t20, t21 := mixedAdditionStep(r00, r01, r10, r11, t00, t01, t10, t11, t20, t21) + l00, l01 := fp2ScalarMul(l00, l01, yp) + l30, l31 := fp2ScalarMul(l30, l31, mxp) + f000, f001, f010, f011, f020, f021, f100, f101, f110, f111, f120, f121 := fp12Mul(f000, f001, f010, f011, f020, f021, f100, f101, f110, f111, f120, f121, l00, l01, l10, l11, l20, l21, l30, l31, l40, l41, l50, l51) + + l00, l01, l10, l11, l20, l21, l30, l31, l40, l41, l50, l51 := computeLine(r20, r21, r30, r31, t00, t01, t10, t11, t20, t21) + l00, l01 := fp2ScalarMul(l00, l01, yp) + l30, l31 := fp2ScalarMul(l30, l31, xp) + f000, f001, f010, f011, f020, f021, f100, f101, f110, f111, f120, f121 := fp12Mul(f000, f001, f010, f011, f020, f021, f100, f101, f110, f111, f120, f121, l00, l01, l10, l11, l20, l21, l30, l31, l40, l41, l50, l51) + } + + // FALLBACK + + let inputSize := calldatasize() + + // Empty input is valid and results in returning one. + if eq(inputSize, 0) { + mstore(0, 1) + return(0, 32) + } + + // If the input length is not a multiple of 192, the call fails. + if mod(inputSize, PAIR_LENGTH()) { + // Bad pairing input + burnGas() + } + + let r000, r001, r010, r011, r020, r021, r100, r101, r110, r111, r120, r121 := FP12_ONE() + + // Calldata "parsing" + for { let i := 0 } lt(i, inputSize) { i := add(i, PAIR_LENGTH()) } { + /* G1 */ + calldatacopy(i, i, 32) // x + calldatacopy(add(i, 32), add(i, 32), 32) // y + + let g1_x := mload(i) + let g1_y := mload(add(i, 32)) + + if iszero(and(coordinateIsOnFieldOrder(g1_x), coordinateIsOnFieldOrder(g1_y))) { + burnGas() + } + + g1_x := intoMontgomeryForm(g1_x) + g1_y := intoMontgomeryForm(g1_y) + + let g1IsInfinity := g1AffinePointIsInfinity(g1_x, g1_y) + + if and(iszero(g1IsInfinity), iszero(g1AffinePointIsOnCurve(g1_x, g1_y))) { + burnGas() + } + + /* G2 */ + let g2_x1_offset := add(i, 64) + let g2_x0_offset := add(i, 96) + let g2_y1_offset := add(i, 128) + let g2_y0_offset := add(i, 160) + + calldatacopy(g2_x1_offset, g2_x1_offset, 32) + calldatacopy(g2_x0_offset, g2_x0_offset, 32) + calldatacopy(g2_y1_offset, g2_y1_offset, 32) + calldatacopy(g2_y0_offset, g2_y0_offset, 32) + + let g2_x1 := mload(g2_x1_offset) + let g2_x0 := mload(g2_x0_offset) + let g2_y1 := mload(g2_y1_offset) + let g2_y0 := mload(g2_y0_offset) + + if iszero(and(coordinateIsOnFieldOrder(g2_x0), coordinateIsOnFieldOrder(g2_x1))) { + burnGas() + } + + if iszero(and(coordinateIsOnFieldOrder(g2_y0), coordinateIsOnFieldOrder(g2_y1))) { + burnGas() + } + + if g2AffinePointIsInfinity(g2_x0, g2_x1, g2_y0, g2_y1) { + continue + } + + g2_x0 := intoMontgomeryForm(g2_x0) + g2_x1 := intoMontgomeryForm(g2_x1) + g2_y0 := intoMontgomeryForm(g2_y0) + g2_y1 := intoMontgomeryForm(g2_y1) + + if iszero(g2IsInSubGroup(g2_x0,g2_x1, g2_y0, g2_y1, MONTGOMERY_ONE(), 0)) { + burnGas() + } + + if iszero(g2AffinePointIsOnCurve(g2_x0, g2_x1, g2_y0, g2_y1)) { + burnGas() + } + + // We must continue if g1 is the point at infinity after validating both g1 and g2 + // That's why although knowing this before parsing and validating g2 we check it later. + if g1IsInfinity { + continue + } + + + let f000, f001, f010, f011, f020, f021, f100, f101, f110, f111, f120, f121 := millerLoop(g2_x0, g2_x1, g2_y0, g2_y1, g1_x, g1_y) + + r000, r001, r010, r011, r020, r021, r100, r101, r110, r111, r120, r121 := fp12Mul(r000, r001, r010, r011, r020, r021, r100, r101, r110, r111, r120, r121, f000, f001, f010, f011, f020, f021, f100, f101, f110, f111, f120, f121) + } + + r000, r001, r010, r011, r020, r021, r100, r101, r110, r111, r120, r121 := finalExponentiation(r000, r001, r010, r011, r020, r021, r100, r101, r110, r111, r120, r121) + + // Pair check + if and(eq(r000, MONTGOMERY_ONE()), iszero(or(r001, or(r010, r011)))) { + if iszero(or(or(r020, r021), or(r100, r101))) { + if iszero(or(or(r110, r111), or(r120, r121))) { + mstore(0, 1) + return(0, 32) + } + } + } + + mstore(0, 0) + return(0, 32) + } + } +} diff --git a/contracts/system-contracts/precompiles/Ecrecover.yul b/contracts/system-contracts/precompiles/Ecrecover.yul new file mode 100644 index 0000000..cbb8fcc --- /dev/null +++ b/contracts/system-contracts/precompiles/Ecrecover.yul @@ -0,0 +1,100 @@ +/** + * @author Matter Labs + * @custom:security-contact security@matterlabs.dev + * @notice The contract used to emulate EVM's ecrecover precompile. + * @dev It uses `precompileCall` to call the zkEVM built-in precompiles. + */ +object "Ecrecover" { + code { + return(0, 0) + } + object "Ecrecover_deployed" { + code { + //////////////////////////////////////////////////////////////// + // CONSTANTS + //////////////////////////////////////////////////////////////// + + // Group order of secp256k1, see https://en.bitcoin.it/wiki/Secp256k1 + function SECP256K1_GROUP_SIZE() -> ret { + ret := 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 + } + + /// @dev The gas cost of processing ecrecover circuit precompile. + function ECRECOVER_GAS_COST() -> ret { + ret := 7000 + } + + //////////////////////////////////////////////////////////////// + // HELPER FUNCTIONS + //////////////////////////////////////////////////////////////// + + // @dev Packs precompile parameters into one word. + // Note: functions expect to work with 32/64 bits unsigned integers. + // Caller should ensure the type matching before! + function unsafePackPrecompileParams( + uint32_inputOffsetInWords, + uint32_inputLengthInWords, + uint32_outputOffsetInWords, + uint32_outputLengthInWords, + uint64_perPrecompileInterpreted + ) -> rawParams { + rawParams := uint32_inputOffsetInWords + rawParams := or(rawParams, shl(32, uint32_inputLengthInWords)) + rawParams := or(rawParams, shl(64, uint32_outputOffsetInWords)) + rawParams := or(rawParams, shl(96, uint32_outputLengthInWords)) + rawParams := or(rawParams, shl(192, uint64_perPrecompileInterpreted)) + } + + /// @dev Executes the `precompileCall` opcode. + function precompileCall(precompileParams, gasToBurn) -> ret { + // Compiler simulation for calling `precompileCall` opcode + ret := verbatim_2i_1o("precompile", precompileParams, gasToBurn) + } + + //////////////////////////////////////////////////////////////// + // FALLBACK + //////////////////////////////////////////////////////////////// + + let digest := calldataload(0) + let v := calldataload(32) + let r := calldataload(64) + let s := calldataload(96) + + // Validate the input by the yellow paper rules (Appendix E. Precompiled contracts) + let vIsInvalid := iszero(or(eq(v, 27), eq(v, 28))) + let sIsInvalid := or(eq(s, 0), gt(s, sub(SECP256K1_GROUP_SIZE(), 1))) + let rIsInvalid := or(eq(r, 0), gt(r, sub(SECP256K1_GROUP_SIZE(), 1))) + + if or(vIsInvalid, or(sIsInvalid, rIsInvalid)) { + return(0, 0) + } + + // Store the data in memory, so the ecrecover circuit will read it + mstore(0, digest) + mstore(32, sub(v, 27)) + mstore(64, r) + mstore(96, s) + + let precompileParams := unsafePackPrecompileParams( + 0, // input offset in words + 4, // input length in words (the signed digest, v, r, s) + 0, // output offset in words + 2, // output length in words (success, signer) + 0 // No special meaning, ecrecover circuit doesn't check this value + ) + let gasToPay := ECRECOVER_GAS_COST() + + // Check whether the call is successfully handled by the ecrecover circuit + let success := precompileCall(precompileParams, gasToPay) + let internalSuccess := mload(0) + + switch and(success, internalSuccess) + case 0 { + return(0, 0) + } + default { + return(32, 32) + } + } + } +} diff --git a/contracts/system-contracts/precompiles/Keccak256.yul b/contracts/system-contracts/precompiles/Keccak256.yul new file mode 100644 index 0000000..8eaa536 --- /dev/null +++ b/contracts/system-contracts/precompiles/Keccak256.yul @@ -0,0 +1,111 @@ +/** + * @author Matter Labs + * @custom:security-contact security@matterlabs.dev + * @notice The contract used to emulate EVM's keccak256 opcode. + * @dev It accepts the data to be hashed in the calldata, propagates it to the zkEVM built-in circuit precompile via `precompileCall`, and burns the gas. + */ +object "Keccak256" { + code { + return(0, 0) + } + object "Keccak256_deployed" { + code { + //////////////////////////////////////////////////////////////// + // CONSTANTS + //////////////////////////////////////////////////////////////// + + /// @dev Returns the block size used by the keccak256 hashing function. + /// The value 136 bytes corresponds to the size of the input data block that the keccak256 + /// algorithm processes in each round, as defined in the keccak256 specification. This is derived + /// from the formula (1600 - 2 * bit length of the digest) / 8, where the bit length for keccak256 + /// is 256 bits. For more details, refer to the Keccak specification at + /// https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.202.pdf#page=30 + function BLOCK_SIZE() -> ret { + ret := 136 + } + + /// @dev The gas cost of processing one keccak256 round. + /// @dev This constant is made equal to the corresponding constant in + /// https://github.com/matter-labs/era-zkevm_opcode_defs/blob/v1.4.1/src/circuit_prices.rs, + /// which was automatically generated depending on the capacity of rounds for a + /// single Keccak256 circuit. + function KECCAK_ROUND_GAS_COST() -> ret { + ret := 40 + } + + /// @dev Returns a 32-bit mask value + function UINT32_BIT_MASK() -> ret { + ret := 0xffffffff + } + + //////////////////////////////////////////////////////////////// + // HELPER FUNCTIONS + //////////////////////////////////////////////////////////////// + + /// @dev Load raw calldata fat pointer + function getCalldataPtr() -> calldataPtr { + calldataPtr := verbatim_0i_1o("get_global::ptr_calldata") + } + + /// @dev Packs precompile parameters into one word. + /// Note: functions expect to work with 32/64 bits unsigned integers. + /// Caller should ensure the type matching before! + function unsafePackPrecompileParams( + uint32_inputOffsetInBytes, + uint32_inputLengthInBytes, + uint32_outputOffsetInWords, + uint32_outputLengthInWords, + uint32_memoryPageToRead, + uint32_memoryPageToWrite, + uint64_perPrecompileInterpreted + ) -> rawParams { + rawParams := uint32_inputOffsetInBytes + rawParams := or(rawParams, shl(32, uint32_inputLengthInBytes)) + rawParams := or(rawParams, shl(64, uint32_outputOffsetInWords)) + rawParams := or(rawParams, shl(96, uint32_outputLengthInWords)) + rawParams := or(rawParams, shl(128, uint32_memoryPageToRead)) + rawParams := or(rawParams, shl(160, uint32_memoryPageToWrite)) + rawParams := or(rawParams, shl(192, uint64_perPrecompileInterpreted)) + } + + /// @dev Executes the `precompileCall` opcode. + function precompileCall(precompileParams, gasToBurn) -> ret { + // Compiler simulation for calling `precompileCall` opcode + ret := verbatim_2i_1o("precompile", precompileParams, gasToBurn) + } + + //////////////////////////////////////////////////////////////// + // FALLBACK + //////////////////////////////////////////////////////////////// + + // 1. Load raw calldata fat pointer + let calldataFatPtr := getCalldataPtr() + + // 2. Parse calldata fat pointer + let ptrMemoryPage := and(shr(32, calldataFatPtr), UINT32_BIT_MASK()) + let ptrStart := and(shr(64, calldataFatPtr), UINT32_BIT_MASK()) + let ptrLength := and(shr(96, calldataFatPtr), UINT32_BIT_MASK()) + + // 3. Pack precompile parameters + let precompileParams := unsafePackPrecompileParams( + ptrStart, // input offset in bytes + ptrLength, // input length in bytes (safe to pass, never exceed `type(uint32).max`) + 0, // output offset in words + 1, // output length in words (NOTE: VM doesn't check this value for now, but this could change in future) + ptrMemoryPage, // memory page to read from + 0, // memory page to write to (0 means write to heap) + 0 // per precompile interpreted value (0 since circuit doesn't react on this value anyway) + ) + // 4. Calculate number of required hash rounds per calldata + let numRounds := add(div(ptrLength, BLOCK_SIZE()), 1) + let gasToPay := mul(KECCAK_ROUND_GAS_COST(), numRounds) + + // 5. Call precompile + let success := precompileCall(precompileParams, gasToPay) + if iszero(success) { + revert(0, 0) + } + return(0, 32) + } + } +} diff --git a/contracts/system-contracts/precompiles/P256Verify.yul b/contracts/system-contracts/precompiles/P256Verify.yul new file mode 100644 index 0000000..8cd14be --- /dev/null +++ b/contracts/system-contracts/precompiles/P256Verify.yul @@ -0,0 +1,91 @@ +/** + * @author Matter Labs + * @custom:security-contact security@matterlabs.dev + * @notice The contract that emulates RIP-7212's P256VERIFY precompile. + * @dev It uses `precompileCall` to call the zkEVM built-in precompiles. + */ +object "P256Verify" { + code { + return(0, 0) + } + object "P256Verify_deployed" { + code { + //////////////////////////////////////////////////////////////// + // CONSTANTS + //////////////////////////////////////////////////////////////// + + /// @dev The gas cost of processing V circuit precompile. + function P256_VERIFY_GAS_COST() -> ret { + ret := 12000 + } + + //////////////////////////////////////////////////////////////// + // HELPER FUNCTIONS + //////////////////////////////////////////////////////////////// + + /// @dev Packs precompile parameters into one word. + /// Note: functions expect to work with 32/64 bits unsigned integers. + /// Caller should ensure the type matching before! + function unsafePackPrecompileParams( + uint32_inputOffsetInWords, + uint32_inputLengthInWords, + uint32_outputOffsetInWords, + uint32_outputLengthInWords, + uint64_perPrecompileInterpreted + ) -> rawParams { + rawParams := uint32_inputOffsetInWords + rawParams := or(rawParams, shl(32, uint32_inputLengthInWords)) + rawParams := or(rawParams, shl(64, uint32_outputOffsetInWords)) + rawParams := or(rawParams, shl(96, uint32_outputLengthInWords)) + rawParams := or(rawParams, shl(192, uint64_perPrecompileInterpreted)) + } + + /// @dev Executes the `precompileCall` opcode. + function precompileCall(precompileParams, gasToBurn) -> ret { + // Compiler simulation for calling `precompileCall` opcode + ret := verbatim_2i_1o("precompile", precompileParams, gasToBurn) + } + + //////////////////////////////////////////////////////////////// + // FALLBACK + //////////////////////////////////////////////////////////////// + + if iszero(eq(calldatasize(), 160)) { + return(0, 0) + } + + // Copy first 5 32-bytes words (the signed digest, r, s, x, y) from the calldata + // to memory, from where secp256r1 circuit will read it. + // The validity of the input as it is done in the internal precompile implementation. + calldatacopy(0, 0, 160) + + let precompileParams := unsafePackPrecompileParams( + 0, // input offset in words + 5, // input length in words (the signed digest, r, s, x, y) + 0, // output offset in words + 2, // output length in words (internalSuccess, isValid) + 0 // No special meaning, secp256r1 circuit doesn't check this value + ) + let gasToPay := P256_VERIFY_GAS_COST() + + // Check whether the call is successfully handled by the secp256r1 circuit + let success := precompileCall(precompileParams, gasToPay) + let internalSuccess := mload(0) + + switch and(success, internalSuccess) + case 0 { + return(0, 0) + } + default { + // The circuits might write `0` to the memory, while providing `internalSuccess` as `1`, so + // we double check here. + let isValid := mload(32) + if eq(isValid, 0) { + return(0, 0) + } + + return(32, 32) + } + } + } +} diff --git a/contracts/system-contracts/precompiles/SHA256.yul b/contracts/system-contracts/precompiles/SHA256.yul new file mode 100644 index 0000000..ff52632 --- /dev/null +++ b/contracts/system-contracts/precompiles/SHA256.yul @@ -0,0 +1,103 @@ +/** + * @author Matter Labs + * @custom:security-contact security@matterlabs.dev + * @notice The contract used to emulate EVM's sha256 precompile. + * @dev It accepts the data to be hashed, pad it by the specification + * and uses `precompileCall` to call the zkEVM built-in precompiles. + * @dev Thus sha256 precompile circuit operates over padded data to perform efficient sponge round computation. + */ +object "SHA256" { + code { + return(0, 0) + } + object "SHA256_deployed" { + code { + //////////////////////////////////////////////////////////////// + // CONSTANTS + //////////////////////////////////////////////////////////////// + + /// @dev The size of the processing sha256 block in bytes. + function BLOCK_SIZE() -> ret { + ret := 64 + } + + /// @dev The gas cost of processing one sha256 round. + function SHA256_ROUND_GAS_COST() -> ret { + ret := 7 + } + + //////////////////////////////////////////////////////////////// + // HELPER FUNCTIONS + //////////////////////////////////////////////////////////////// + + // @dev Packs precompile parameters into one word. + // Note: functions expect to work with 32/64 bits unsigned integers. + // Caller should ensure the type matching before! + function unsafePackPrecompileParams( + uint32_inputOffsetInWords, + uint32_inputLengthInWords, + uint32_outputOffsetInWords, + uint32_outputLengthInWords, + uint64_perPrecompileInterpreted + ) -> rawParams { + rawParams := uint32_inputOffsetInWords + rawParams := or(rawParams, shl(32, uint32_inputLengthInWords)) + rawParams := or(rawParams, shl(64, uint32_outputOffsetInWords)) + rawParams := or(rawParams, shl(96, uint32_outputLengthInWords)) + rawParams := or(rawParams, shl(192, uint64_perPrecompileInterpreted)) + } + + /// @dev Executes the `precompileCall` opcode. + function precompileCall(precompileParams, gasToBurn) -> ret { + // Compiler simulation for calling `precompileCall` opcode + ret := verbatim_2i_1o("precompile", precompileParams, gasToBurn) + } + + //////////////////////////////////////////////////////////////// + // FALLBACK + //////////////////////////////////////////////////////////////// + + // Copy calldata to memory for pad it + let bytesSize := calldatasize() + calldatacopy(0, 0, bytesSize) + + // The sha256 padding includes additional 8 bytes of the total message's length in bits, + // so calculate the "full" message length with it. + let extendBytesLen := add(bytesSize, 8) + + let padLen := sub(BLOCK_SIZE(), mod(extendBytesLen, BLOCK_SIZE())) + let paddedBytesSize := add(extendBytesLen, padLen) + + // The original message size in bits + let binSize := mul(bytesSize, 8) + // Same bin size, but shifted to the left, needed for the padding + let leftShiftedBinSize := shl(sub(256, 64), binSize) + + // Write 0x80000... as padding according the sha256 specification + mstore(bytesSize, 0x8000000000000000000000000000000000000000000000000000000000000000) + // then will be some zeroes and BE encoded bit length + mstore(sub(paddedBytesSize, 8), leftShiftedBinSize) + + let numRounds := div(paddedBytesSize, BLOCK_SIZE()) + let precompileParams := unsafePackPrecompileParams( + 0, // input offset in words + // Always divisible by 32, since `BLOCK_SIZE()` is 64 bytes + div(paddedBytesSize, 32), // input length in words (safe to pass, never exceed `type(uint32).max`) + 0, // output offset in words + 1, // output length in words + numRounds // number of rounds (safe to pass, never exceed `type(uint64).max`) + ) + let gasToPay := mul(SHA256_ROUND_GAS_COST(), numRounds) + + let success := precompileCall(precompileParams, gasToPay) + + switch success + case 0 { + revert(0, 0) + } + default { + return(0, 32) + } + } + } +} diff --git a/contracts/system-contracts/precompiles/test-contracts/Keccak256Mock.yul b/contracts/system-contracts/precompiles/test-contracts/Keccak256Mock.yul new file mode 100644 index 0000000..b37eb69 --- /dev/null +++ b/contracts/system-contracts/precompiles/test-contracts/Keccak256Mock.yul @@ -0,0 +1,99 @@ +/** + * @author Matter Labs + * @notice The contract used to emulate EVM's keccak256 opcode. + * @dev It accepts the data to be hashed in the calldata, propagate it to the zkEVM built-in circuit precompile via `precompileCall` and burn . + */ + object "Keccak256" { + code { } + object "Keccak256_deployed" { + code { + //////////////////////////////////////////////////////////////// + // CONSTANTS + //////////////////////////////////////////////////////////////// + + /// @dev The size of the processing keccak256 block in bytes. + function BLOCK_SIZE() -> ret { + ret := 136 + } + + /// @dev The gas cost of processing one keccak256 round. + function KECCAK_ROUND_GAS_COST() -> ret { + ret := 40 + } + + /// @dev Returns a 32-bit mask value + function UINT32_BIT_MASK() -> ret { + ret := 0xffffffff + } + + //////////////////////////////////////////////////////////////// + // HELPER FUNCTIONS + //////////////////////////////////////////////////////////////// + + /// @dev Load raw calldata fat pointer + function getCalldataPtr() -> calldataPtr { + calldataPtr := verbatim_0i_1o("get_global::ptr_calldata") + } + + /// @dev Packs precompile parameters into one word. + /// Note: functions expect to work with 32/64 bits unsigned integers. + /// Caller should ensure the type matching before! + function unsafePackPrecompileParams( + uint32_inputOffsetInBytes, + uint32_inputLengthInBytes, + uint32_outputOffsetInWords, + uint32_outputLengthInWords, + uint32_memoryPageToRead, + uint32_memoryPageToWrite, + uint64_perPrecompileInterpreted + ) -> rawParams { + rawParams := uint32_inputOffsetInBytes + rawParams := or(rawParams, shl(32, uint32_inputLengthInBytes)) + rawParams := or(rawParams, shl(64, uint32_outputOffsetInWords)) + rawParams := or(rawParams, shl(96, uint32_outputLengthInWords)) + rawParams := or(rawParams, shl(128, uint32_memoryPageToRead)) + rawParams := or(rawParams, shl(160, uint32_memoryPageToWrite)) + rawParams := or(rawParams, shl(192, uint64_perPrecompileInterpreted)) + } + + /// @dev Executes the `precompileCall` opcode. + function precompileCall(precompileParams, gasToBurn) -> ret { + // Compiler simulation for calling `precompileCall` opcode + ret := verbatim_2i_1o("precompile", precompileParams, gasToBurn) + } + + //////////////////////////////////////////////////////////////// + // FALLBACK + //////////////////////////////////////////////////////////////// + + // 1. Load raw calldata fat pointer + let calldataFatPtr := getCalldataPtr() + + // 2. Parse calldata fat pointer + let ptrMemoryPage := and(shr(32, calldataFatPtr), UINT32_BIT_MASK()) + let ptrStart := and(shr(64, calldataFatPtr), UINT32_BIT_MASK()) + let ptrLength := and(shr(96, calldataFatPtr), UINT32_BIT_MASK()) + + // 3. Pack precompile parameters + let precompileParams := unsafePackPrecompileParams( + ptrStart, // input offset in bytes + ptrLength, // input length in bytes (safe to pass, never exceed `type(uint32).max`) + 0, // output offset in words + 1, // output length in words (NOTE: VM doesn't check this value for now, but this could change in future) + ptrMemoryPage, // memory page to read from + 0, // memory page to write to (0 means write to heap) + 0 // per precompile interpreted value (0 since circuit doesn't react on this value anyway) + ) + // 4. Calculate number of required hash rounds per calldata + let numRounds := div(add(ptrLength, sub(BLOCK_SIZE(), 1)), BLOCK_SIZE()) + let gasToPay := 0 + + // 5. Call precompile + let success := precompileCall(precompileParams, gasToPay) + if iszero(success) { + revert(0, 0) + } + return(0, 32) + } + } +} diff --git a/contracts/system-contracts/test-contracts/AlwaysRevert.sol b/contracts/system-contracts/test-contracts/AlwaysRevert.sol new file mode 100644 index 0000000..3c9d469 --- /dev/null +++ b/contracts/system-contracts/test-contracts/AlwaysRevert.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +contract AlwaysRevert { + fallback() external { + revert(""); + } +} diff --git a/contracts/system-contracts/test-contracts/CodeOracleTest.sol b/contracts/system-contracts/test-contracts/CodeOracleTest.sol new file mode 100644 index 0000000..f022fb7 --- /dev/null +++ b/contracts/system-contracts/test-contracts/CodeOracleTest.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.20; + +address constant REAL_CODE_ORACLE_ADDR = 0x0000000000000000000000000000000000008011; + +contract CodeOracleTest { + function callCodeOracle( + bytes32 _versionedHash, + bytes32 _expectedBytecodeHash + ) external view returns (uint256 gasCost) { + uint256 gasBefore = gasleft(); + + // Call the code oracle + (bool success, bytes memory returnedBytecode) = REAL_CODE_ORACLE_ADDR.staticcall( + abi.encodePacked(_versionedHash) + ); + + gasCost = gasBefore - gasleft(); + + // Check the result + require(success, "CodeOracle call failed"); + + // Check the returned bytecode + require( + keccak256(returnedBytecode) == _expectedBytecodeHash, + "Returned bytecode does not match the expected hash" + ); + } + + function codeOracleTest(bytes32 _versionedHash, bytes32 _expectedBytecodeHash) external view { + // Here we call code oracle twice in order to ensure that the memory page is preserved and the gas cost is lower the second time. + // Note, that we use external calls in order to remove any possibility of inlining by the compiler. + uint256 firstCallCost = this.callCodeOracle(_versionedHash, _expectedBytecodeHash); + uint256 secondCallCost = this.callCodeOracle(_versionedHash, _expectedBytecodeHash); + uint256 thirdCallCost = this.callCodeOracle(_versionedHash, _expectedBytecodeHash); + + require(secondCallCost < firstCallCost, "The second call should have cost less gas"); + require(thirdCallCost == secondCallCost, "The third call should have the same gas cost as the second call"); + } +} diff --git a/contracts/system-contracts/test-contracts/DelegateCaller.sol b/contracts/system-contracts/test-contracts/DelegateCaller.sol new file mode 100644 index 0000000..a28cc01 --- /dev/null +++ b/contracts/system-contracts/test-contracts/DelegateCaller.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +contract DelegateCaller { + function delegateCall(address _to) external payable { + assembly { + calldatacopy(0, 0, calldatasize()) + let result := delegatecall(gas(), _to, 0, calldatasize(), 0, 0) + returndatacopy(0, 0, returndatasize()) + switch result + case 0 { + revert(0, returndatasize()) + } + default { + return(0, returndatasize()) + } + } + } +} diff --git a/contracts/system-contracts/test-contracts/Deployable.sol b/contracts/system-contracts/test-contracts/Deployable.sol new file mode 100644 index 0000000..8178ead --- /dev/null +++ b/contracts/system-contracts/test-contracts/Deployable.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +contract Deployable { + event Deployed(uint256 value, bytes data); + + constructor() payable { + uint256 len; + assembly { + len := codesize() + } + bytes memory data = new bytes(len); + assembly { + codecopy(add(data, 0x20), 0, len) + } + emit Deployed(msg.value, data); + } +} diff --git a/contracts/system-contracts/test-contracts/ExtraAbiCaller.zasm b/contracts/system-contracts/test-contracts/ExtraAbiCaller.zasm new file mode 100644 index 0000000..b95b2a0 --- /dev/null +++ b/contracts/system-contracts/test-contracts/ExtraAbiCaller.zasm @@ -0,0 +1,60 @@ +.text + .file "main" + .globl __entry +__entry: +.func_begin0: + sub.s! 0, r2, r0 + jump.eq @.RUNTIME_CODE + ; deployment code + add 32, r0, r1 + st.1 r0, r1 + st.1 r1, r0 + add @CPI0_1[0], r0, r1 + ret.ok.to_label r1, @DEFAULT_FAR_RETURN +.RUNTIME_CODE: + ; ABI: + ; 0-32 address(in the lowest 20 bytes) + ; 32-64 msg.value + ; 64-384 extra data + ; 384+ calldata + ; + ; load address into r2 + ld.inc r1, r2, r1 + ; set msg.value + ld.inc r1, r3, r1 + context.set_context_u128 r3 + ; load extra abi data into r3-r12 + ld.inc r1, r3, r1 + ld.inc r1, r4, r1 + ld.inc r1, r5, r1 + ld.inc r1, r6, r1 + ld.inc r1, r7, r1 + ld.inc r1, r8, r1 + ld.inc r1, r9, r1 + ld.inc r1, r10, r1 + ld.inc r1, r11, r1 + ld.inc r1, r12, r1 + ptr.pack.s @CPI0_0[0], r1, r1 + far_call r1, r2, @.CALL_REVERT + ptr.pack.s @CPI0_2[0], r1, r1 + ret.ok r1 +.CALL_REVERT: + ptr.pack.s @CPI0_2[0], r1, r1 + ret.revert r1 +.func_end0: + .note.GNU-stack + .rodata +; far call abi: +; gas amount = 0xFFFFFFFF(max) +; forwarding mode = fat ptr +; shard id = 0 +; constructor flag = false +; system call flag = true +; 01 00 00 01 FFFFFFFF 0000000000000000 00000000000000000000000000000000 +CPI0_0: + .cell 452312902503159716397502014137536550255307801666780882257920705274096648192 +CPI0_1: + .cell 5070602400912917605986812821504 +; 01 00000000 0000000000000000 00000000000000000000000000000000 +CPI0_2: + .cell 26959946667150639794667015087019630673637144422540572481103610249216 diff --git a/contracts/system-contracts/test-contracts/KeccakTest.sol b/contracts/system-contracts/test-contracts/KeccakTest.sol new file mode 100644 index 0000000..79581af --- /dev/null +++ b/contracts/system-contracts/test-contracts/KeccakTest.sol @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; +pragma abicoder v2; + +import {LOAD_LATEST_RETURNDATA_INTO_ACTIVE_PTR_CALL_ADDRESS, PTR_PACK_INTO_ACTIVE_CALL_ADDRESS, SystemContractsCaller, CalldataForwardingMode, RAW_FAR_CALL_BY_REF_CALL_ADDRESS} from "../libraries/SystemContractsCaller.sol"; +import {EfficientCall, KECCAK256_SYSTEM_CONTRACT} from "../libraries/EfficientCall.sol"; + +// In this test it is important to actually change the real Keccak256's contract's bytecode, +// which requires changes in the real AccountCodeStorage contract +address constant REAL_DEPLOYER_SYSTEM_CONTRACT = address(0x8006); +address constant REAL_FORCE_DEPLOYER_ADDRESS = address(0x8007); + +contract KeccakTest { + bytes32 constant EMPTY_STRING_KECCAK = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470; + + // Just some computation-heavy function, it will be used to test out of gas + function infiniteFunction(uint256 n) public pure returns (uint256 sumOfSquares) { + for (uint256 i = 0; i < n; i++) { + sumOfSquares += i * i; + } + } + + function _loadFarCallABIIntoActivePtr(uint256 _gas) private view { + uint256 farCallAbi = SystemContractsCaller.getFarCallABIWithEmptyFatPointer({ + gasPassed: uint32(_gas), + // Only rollup is supported for now + shardId: 0, + forwardingMode: CalldataForwardingMode.ForwardFatPointer, + isConstructorCall: false, + isSystemCall: false + }); + _ptrPackIntoActivePtr(farCallAbi); + } + + function _loadReturnDataIntoActivePtr() internal { + address callAddr = LOAD_LATEST_RETURNDATA_INTO_ACTIVE_PTR_CALL_ADDRESS; + assembly { + pop(staticcall(0, callAddr, 0, 0xFFFF, 0, 0)) + } + } + + function _ptrPackIntoActivePtr(uint256 _farCallAbi) internal view { + address callAddr = PTR_PACK_INTO_ACTIVE_CALL_ADDRESS; + assembly { + pop(staticcall(_farCallAbi, callAddr, 0, 0xFFFF, 0, 0)) + } + } + + function rawCallByRef(address _address) internal returns (bool success) { + address callAddr = RAW_FAR_CALL_BY_REF_CALL_ADDRESS; + assembly { + success := call(_address, callAddr, 0, 0, 0xFFFF, 0, 0) + } + } + + function zeroPointerTest() external { + try this.infiniteFunction{gas: 1000000}(1000000) returns (uint256) { + revert("The transaction should have failed"); + } catch {} + + _loadReturnDataIntoActivePtr(); + _loadFarCallABIIntoActivePtr(1000000); + bool success = rawCallByRef(KECCAK256_SYSTEM_CONTRACT); + require(success, "The call to keccak should have succeeded"); + + uint256 returndataSize = 0; + assembly { + returndataSize := returndatasize() + } + require(returndataSize == 32, "The return data size should be 32 bytes"); + + bytes32 result; + assembly { + returndatacopy(0, 0, 32) + result := mload(0) + } + + require(result == EMPTY_STRING_KECCAK, "The result is not correct"); + } + + function keccakUpgradeTest( + bytes calldata eraseCallData, + bytes calldata upgradeCalldata + ) external returns (bytes32 hash) { + // Firstly, we reset keccak256 bytecode to be some random bytecode + EfficientCall.mimicCall({ + _gas: gasleft(), + _address: address(REAL_DEPLOYER_SYSTEM_CONTRACT), + _data: eraseCallData, + _whoToMimic: REAL_FORCE_DEPLOYER_ADDRESS, + _isConstructor: false, + _isSystem: false + }); + + // Since the keccak contract has been erased, it should not work anymore + try this.callKeccak(msg.data[0:0]) returns (bytes32) { + revert("The keccak should not work anymore"); + } catch {} + + // Upgrading it back to the correct version: + EfficientCall.mimicCall({ + _gas: gasleft(), + _address: address(REAL_DEPLOYER_SYSTEM_CONTRACT), + _data: upgradeCalldata, + _whoToMimic: REAL_FORCE_DEPLOYER_ADDRESS, + _isConstructor: false, + _isSystem: false + }); + + // Now it should work again + hash = this.callKeccak(msg.data[0:0]); + require(hash == EMPTY_STRING_KECCAK, "Keccak should start working again"); + } + + function keccakPerformUpgrade(bytes calldata upgradeCalldata) external { + EfficientCall.mimicCall({ + _gas: gasleft(), + _address: address(REAL_DEPLOYER_SYSTEM_CONTRACT), + _data: upgradeCalldata, + _whoToMimic: REAL_FORCE_DEPLOYER_ADDRESS, + _isConstructor: false, + _isSystem: false + }); + } + + function callKeccak(bytes calldata _data) external pure returns (bytes32 hash) { + hash = keccak256(_data); + } + + function keccakValidationTest( + bytes calldata upgradeCalldata, + bytes calldata resetCalldata, + bytes[] calldata testInputs, + bytes32[] calldata expectedOutputs + ) external { + require(testInputs.length == expectedOutputs.length, "mismatch between number of inputs and outputs"); + + // Firstly, we upgrade keccak256 bytecode to the correct version. + EfficientCall.mimicCall({ + _gas: gasleft(), + _address: address(REAL_DEPLOYER_SYSTEM_CONTRACT), + _data: upgradeCalldata, + _whoToMimic: REAL_FORCE_DEPLOYER_ADDRESS, + _isConstructor: false, + _isSystem: false + }); + + bytes32[] memory result = new bytes32[](testInputs.length); + + for (uint256 i = 0; i < testInputs.length; i++) { + bytes32 res = this.callKeccak(testInputs[i]); + result[i] = res; + } + + for (uint256 i = 0; i < result.length; i++) { + require(result[i] == expectedOutputs[i], "hash was not calculated correctly"); + } + + // Upgrading it back to the original version: + EfficientCall.mimicCall({ + _gas: gasleft(), + _address: address(REAL_DEPLOYER_SYSTEM_CONTRACT), + _data: resetCalldata, + _whoToMimic: REAL_FORCE_DEPLOYER_ADDRESS, + _isConstructor: false, + _isSystem: false + }); + } +} diff --git a/contracts/system-contracts/test-contracts/MockContract.sol b/contracts/system-contracts/test-contracts/MockContract.sol new file mode 100644 index 0000000..b7d9bcb --- /dev/null +++ b/contracts/system-contracts/test-contracts/MockContract.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +contract MockContract { + event Called(uint256 value, bytes data); + + struct CallResult { + bytes input; + bool failure; + bytes returnData; + } + + CallResult[] private results; + + constructor() { + // Clean results if mock was redeployed. + delete results; + } + + // This function call will not pass to fallback, but this is fine for the tests. + function setResult(CallResult calldata result) external { + bytes32 inputKeccak = keccak256(result.input); + for (uint256 i = 0; i < results.length; i++) { + if (keccak256(results[i].input) == inputKeccak) { + results[i] = result; + return; + } + } + results.push(result); + } + + fallback() external payable { + bytes memory data = msg.data; + bytes32 inputKeccak = keccak256(data); + + // empty return data with successful result by default. + bool failure; + bytes memory returnData; + + for (uint256 i = 0; i < results.length; i++) { + if (keccak256(results[i].input) == inputKeccak) { + failure = results[i].failure; + returnData = results[i].returnData; + break; + } + } + + // Emitting event only if empty successful result expected. + // Can fail if call context is static, but usually it's not a case, + // because view/pure call without return data doesn't make sense. + // Useful, because for such calls we can check for this event, + // to be sure that the needed call was made. + if (!failure && returnData.length == 0) { + emit Called(msg.value, data); + } + + assembly { + switch failure + case 0 { + return(add(returnData, 0x20), mload(returnData)) + } + default { + revert(add(returnData, 0x20), mload(returnData)) + } + } + } +} diff --git a/contracts/system-contracts/test-contracts/SystemCaller.sol b/contracts/system-contracts/test-contracts/SystemCaller.sol new file mode 100644 index 0000000..b51caec --- /dev/null +++ b/contracts/system-contracts/test-contracts/SystemCaller.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {SYSTEM_CALL_CALL_ADDRESS, MSG_VALUE_SIMULATOR_IS_SYSTEM_BIT, SystemContractsCaller, CalldataForwardingMode} from "../libraries/SystemContractsCaller.sol"; +import {Utils} from "../libraries/Utils.sol"; + +address constant REAL_MSG_VALUE_SYSTEM_CONTRACT = address(0x8009); + +// Proxy that sets system call, does the same thing as `ExtraAbiCaller.zasm`, but can be called with callee abi, which is more convenient. +// Also updates the real balance of the callee. +contract SystemCaller { + address immutable to; + + constructor(address _to) { + to = _to; + } + + // The library method will not work, because it uses the MsgValueSimulator test address. + fallback() external payable { + address callAddr = SYSTEM_CALL_CALL_ADDRESS; + + address _to = to; + bytes memory data = msg.data; + uint32 dataStart; + assembly { + dataStart := add(data, 0x20) + } + uint32 dataLength = uint32(Utils.safeCastToU32(data.length)); + + uint256 farCallAbi = SystemContractsCaller.getFarCallABI({ + dataOffset: 0, + memoryPage: 0, + dataStart: dataStart, + dataLength: dataLength, + gasPassed: Utils.safeCastToU32(gasleft()), + // Only rollup is supported for now + shardId: 0, + forwardingMode: CalldataForwardingMode.UseHeap, + isConstructorCall: false, + isSystemCall: true + }); + + bool success; + if (msg.value == 0) { + // Doing the system call directly + assembly { + success := call(_to, callAddr, 0, 0, farCallAbi, 0, 0) + } + } else { + address msgValueSimulator = REAL_MSG_VALUE_SYSTEM_CONTRACT; + // We need to supply the mask to the MsgValueSimulator to denote + // that the call should be a system one. + uint256 forwardMask = MSG_VALUE_SIMULATOR_IS_SYSTEM_BIT; + + assembly { + success := call(msgValueSimulator, callAddr, callvalue(), _to, farCallAbi, forwardMask, 0) + } + } + uint256 returnDataSize; + assembly { + returnDataSize := returndatasize() + } + bytes memory returnData = new bytes(returnDataSize); + assembly { + returndatacopy(add(returnData, 0x20), 0, returnDataSize) + switch success + case 0 { + revert(add(returnData, 0x20), returnDataSize) + } + default { + return(add(returnData, 0x20), returnDataSize) + } + } + } +} diff --git a/contracts/system-contracts/test-contracts/TransferTest.sol b/contracts/system-contracts/test-contracts/TransferTest.sol new file mode 100644 index 0000000..1b9e9bd --- /dev/null +++ b/contracts/system-contracts/test-contracts/TransferTest.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.20; + +contract TransferTest { + function transfer(address payable to, uint256 amount, bool warmUpRecipient) public payable { + if (warmUpRecipient) { + // This will warm up both the `X` variable and the balance of the recipient + TransferTestReentrantRecipient(to).setX{value: msg.value}(); + } + + to.transfer(amount); + } + + function send(address payable to, uint256 amount, bool warmUpRecipient) public payable { + if (warmUpRecipient) { + // This will warm up both the `X` variable and the balance of the recipient + TransferTestReentrantRecipient(to).setX{value: msg.value}(); + } + + bool success = to.send(amount); + + require(success, "Transaction failed"); + } + + receive() external payable {} +} + +contract TransferTestRecipient { + event Received(address indexed sender, uint256 amount); + + receive() external payable { + require(gasleft() >= 2100, "Not enough gas"); + require(gasleft() <= 2300, "Too much gas"); + emit Received(msg.sender, msg.value); + } +} + +contract TransferTestReentrantRecipient { + uint256 x; + + event Received(uint256 tmp, uint256 amount); + + function setX() external payable { + x = 1; + } + + receive() external payable { + x = 1; + } +}