diff --git a/l1-contracts/contracts/bridge/L1AssetRouter.sol b/l1-contracts/contracts/bridge/L1AssetRouter.sol deleted file mode 100644 index 9a4a16bce..000000000 --- a/l1-contracts/contracts/bridge/L1AssetRouter.sol +++ /dev/null @@ -1,1030 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -// solhint-disable reason-string, gas-custom-errors - -import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable-v4/access/Ownable2StepUpgradeable.sol"; -import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable-v4/security/PausableUpgradeable.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 {IL1AssetRouter} from "./interfaces/IL1AssetRouter.sol"; -import {IL2Bridge} from "./interfaces/IL2Bridge.sol"; -import {IL2BridgeLegacy} from "./interfaces/IL2BridgeLegacy.sol"; -import {IL1AssetHandler} from "./interfaces/IL1AssetHandler.sol"; -import {IL1NativeTokenVault} from "./interfaces/IL1NativeTokenVault.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 {DataEncoding} from "../common/libraries/DataEncoding.sol"; -import {AddressAliasHelper} from "../vendor/AddressAliasHelper.sol"; -import {TWO_BRIDGES_MAGIC_VALUE, ETH_TOKEN_ADDRESS} from "../common/Config.sol"; -import {L2_NATIVE_TOKEN_VAULT_ADDRESS} from "../common/L2ContractAddresses.sol"; - -import {IBridgehub, L2TransactionRequestTwoBridgesInner, L2TransactionRequestDirect} from "../bridgehub/IBridgehub.sol"; -import {L2_BASE_TOKEN_SYSTEM_CONTRACT_ADDR, L2_ASSET_ROUTER_ADDR} from "../common/L2ContractAddresses.sol"; - -import {BridgeHelper} from "./BridgeHelper.sol"; - -/// @author Matter Labs -/// @custom:security-contact security@matterlabs.dev -/// @dev Bridges assets between L1 and ZK chain, supporting both ETH and ERC20 tokens. -/// @dev Designed for use with a proxy for upgradability. -contract L1AssetRouter is IL1AssetRouter, 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 __DEPRECATED_l2BridgeAddress; - - /// @dev A mapping chainId => L2 deposit transaction hash => dataHash - // keccak256(abi.encode(account, tokenAddress, amount)) for legacy transfers - // keccak256(abi.encode(_prevMsgSender, assetId, transferData)) for new transfers - /// @dev Tracks deposit transactions to 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; - - /// @notice Deprecated. Kept for backwards compatibility. - /// @dev Indicates whether the hyperbridging is enabled for a given chain. - // slither-disable-next-line uninitialized-state - mapping(uint256 chainId => bool enabled) public hyperbridgingEnabled; - - /// @dev Maps token balances for each chain to prevent unauthorized spending across ZK chain. - /// 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; - - /// @dev Maps asset ID to address of corresponding asset handler. - /// @dev Tracks the address of Asset Handler contracts, where bridged funds are locked for each asset. - /// @dev P.S. this liquidity was locked directly in SharedBridge before. - mapping(bytes32 assetId => address assetHandlerAddress) public assetHandlerAddress; - - /// @dev Maps asset ID to the asset deployment tracker address. - /// @dev Tracks the address of Deployment Tracker contract on L1, which sets Asset Handlers on L2s (ZK chain). - /// @dev For the asset and stores respective addresses. - mapping(bytes32 assetId => address assetDeploymentTracker) public assetDeploymentTracker; - - /// @dev Address of native token vault. - IL1NativeTokenVault public nativeTokenVault; - - /// @notice Checks that the message sender is the bridgehub. - modifier onlyBridgehub() { - require(msg.sender == address(BRIDGE_HUB), "L1AR: not BH"); - _; - } - - /// @notice Checks that the message sender is the bridgehub or zkSync Era Diamond Proxy. - modifier onlyBridgehubOrEra(uint256 _chainId) { - require( - msg.sender == address(BRIDGE_HUB) || (_chainId == ERA_CHAIN_ID && msg.sender == ERA_DIAMOND_PROXY), - "L1AR: msg.sender not equal to bridgehub or era chain" - ); - _; - } - - /// @notice Checks that the message sender is the legacy bridge. - modifier onlyLegacyBridge() { - require(msg.sender == address(legacyBridge), "L1AR: not legacy bridge"); - _; - } - - /// @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. - /// @dev Used for testing purposes only, as the contract has been initialized on mainnet. - /// @param _owner The 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. - /// @param _eraPostDiamondUpgradeFirstBatch The first batch number on the zkSync Era Diamond Proxy that was settled after diamond proxy upgrade. - /// @param _eraPostLegacyBridgeUpgradeFirstBatch The first batch number on the zkSync Era Diamond Proxy that was settled after legacy bridge upgrade. - /// @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 initialize( - address _owner, - uint256 _eraPostDiamondUpgradeFirstBatch, - uint256 _eraPostLegacyBridgeUpgradeFirstBatch, - uint256 _eraLegacyBridgeLastDepositBatch, - uint256 _eraLegacyBridgeLastDepositTxNumber - ) external reentrancyGuardInitializer initializer { - require(_owner != address(0), "L1AR: owner 0"); - _transferOwnership(_owner); - if (eraPostDiamondUpgradeFirstBatch == 0) { - eraPostDiamondUpgradeFirstBatch = _eraPostDiamondUpgradeFirstBatch; - eraPostLegacyBridgeUpgradeFirstBatch = _eraPostLegacyBridgeUpgradeFirstBatch; - eraLegacyBridgeLastDepositBatch = _eraLegacyBridgeLastDepositBatch; - eraLegacyBridgeLastDepositTxNumber = _eraLegacyBridgeLastDepositTxNumber; - } - } - - /// @notice Transfers tokens from shared bridge to native token vault. - /// @dev This function is part of the upgrade process used to transfer liquidity. - /// @param _token The address of the token to be transferred to NTV. - function transferTokenToNTV(address _token) external { - address ntvAddress = address(nativeTokenVault); - require(msg.sender == ntvAddress, "L1AR: not NTV"); - if (ETH_TOKEN_ADDRESS == _token) { - uint256 amount = address(this).balance; - bool callSuccess; - // Low-level assembly call, to avoid any memory copying (save gas) - assembly { - callSuccess := call(gas(), ntvAddress, amount, 0, 0, 0, 0) - } - require(callSuccess, "L1AR: eth transfer failed"); - } else { - IERC20(_token).safeTransfer(ntvAddress, IERC20(_token).balanceOf(address(this))); - } - } - - /// @notice Clears chain balance for specific token. - /// @dev This function is part of the upgrade process used to nullify chain balances once they are credited to NTV. - /// @param _chainId The ID of the ZK chain. - /// @param _token The address of the token which was previously deposit to shared bridge. - function nullifyChainBalanceByNTV(uint256 _chainId, address _token) external { - require(msg.sender == address(nativeTokenVault), "L1AR: not NTV"); - chainBalance[_chainId][_token] = 0; - } - - /// @notice Sets the L1ERC20Bridge contract address. - /// @dev Should be called only once by the owner. - /// @param _legacyBridge The address of the legacy bridge. - function setL1Erc20Bridge(address _legacyBridge) external onlyOwner { - require(address(legacyBridge) == address(0), "L1AR: legacy bridge already set"); - require(_legacyBridge != address(0), "L1AR: legacy bridge 0"); - legacyBridge = IL1ERC20Bridge(_legacyBridge); - } - - /// @notice Sets the nativeTokenVault contract address. - /// @dev Should be called only once by the owner. - /// @param _nativeTokenVault The address of the native token vault. - function setNativeTokenVault(IL1NativeTokenVault _nativeTokenVault) external onlyOwner { - require(address(nativeTokenVault) == address(0), "L1AR: native token vault already set"); - require(address(_nativeTokenVault) != address(0), "L1AR: native token vault 0"); - nativeTokenVault = _nativeTokenVault; - } - - /// @notice Used to set the assed deployment tracker address for given asset data. - /// @param _assetRegistrationData The asset data which may include the asset address and any additional required data or encodings. - /// @param _assetDeploymentTracker The whitelisted address of asset deployment tracker for provided asset. - function setAssetDeploymentTracker( - bytes32 _assetRegistrationData, - address _assetDeploymentTracker - ) external onlyOwner { - bytes32 assetId = keccak256( - abi.encode(uint256(block.chainid), _assetDeploymentTracker, _assetRegistrationData) - ); - assetDeploymentTracker[assetId] = _assetDeploymentTracker; - emit AssetDeploymentTrackerSet(assetId, _assetDeploymentTracker, _assetRegistrationData); - } - - /// @notice Sets the asset handler address for a specified asset ID on the chain of the asset deployment tracker. - /// @dev The caller of this function is encoded within the `assetId`, therefore, it should be invoked by the asset deployment tracker contract. - /// @dev Typically, for most tokens, ADT is the native token vault. However, custom tokens may have their own specific asset deployment trackers. - /// @dev `setAssetHandlerAddressOnCounterPart` should be called on L1 to set asset handlers on L2 chains for a specific asset ID. - /// @param _assetRegistrationData The asset data which may include the asset address and any additional required data or encodings. - /// @param _assetHandlerAddress The address of the asset handler to be set for the provided asset. - function setAssetHandlerAddressInitial(bytes32 _assetRegistrationData, address _assetHandlerAddress) external { - bool senderIsNTV = msg.sender == address(nativeTokenVault); - address sender = senderIsNTV ? L2_NATIVE_TOKEN_VAULT_ADDRESS : msg.sender; - bytes32 assetId = DataEncoding.encodeAssetId(block.chainid, _assetRegistrationData, sender); - require(senderIsNTV || msg.sender == assetDeploymentTracker[assetId], "ShB: not NTV or ADT"); - assetHandlerAddress[assetId] = _assetHandlerAddress; - if (senderIsNTV) { - assetDeploymentTracker[assetId] = msg.sender; - } - emit AssetHandlerRegisteredInitial(assetId, _assetHandlerAddress, _assetRegistrationData, sender); - } - - /// @notice Used to set the asset handler address for a given asset ID on a remote ZK chain - /// @dev No access control on the caller, as msg.sender is encoded in the assetId. - /// @param _chainId The ZK chain ID. - /// @param _mintValue The value withdrawn by base token bridge to cover for l2 gas and l2 msg.value costs. - /// @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. - /// @param _assetId The encoding of asset ID. - /// @param _assetHandlerAddressOnCounterPart The address of the asset handler, which will hold the token of interest. - /// @return txHash The L2 transaction hash of setting asset handler on remote chain. - function setAssetHandlerAddressOnCounterPart( - uint256 _chainId, - uint256 _mintValue, - uint256 _l2TxGasLimit, - uint256 _l2TxGasPerPubdataByte, - address _refundRecipient, - bytes32 _assetId, - address _assetHandlerAddressOnCounterPart - ) external payable returns (bytes32 txHash) { - require(msg.sender == assetDeploymentTracker[_assetId] || msg.sender == owner(), "L1AR: only ADT or owner"); - - bytes memory l2Calldata = abi.encodeCall( - IL2Bridge.setAssetHandlerAddress, - (_assetId, _assetHandlerAddressOnCounterPart) - ); - - L2TransactionRequestDirect memory request = L2TransactionRequestDirect({ - chainId: _chainId, - l2Contract: L2_ASSET_ROUTER_ADDR, - mintValue: _mintValue, // l2 gas + l2 msg.value the bridgehub will withdraw the mintValue from the base token bridge for gas - l2Value: 0, // For base token deposits, there is no msg.value during the call, as the base token is minted to the recipient address - l2Calldata: l2Calldata, - l2GasLimit: _l2TxGasLimit, - l2GasPerPubdataByteLimit: _l2TxGasPerPubdataByte, - factoryDeps: new bytes[](0), - refundRecipient: _refundRecipient - }); - txHash = BRIDGE_HUB.requestL2TransactionDirect{value: msg.value}(request); - } - - /// @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 ZK chain to which deposit. - /// @param _assetId The deposited asset ID. - /// @param _prevMsgSender The `msg.sender` address from the external call that initiated current one. - /// @param _amount The total amount of tokens to be bridged. - function bridgehubDepositBaseToken( - uint256 _chainId, - bytes32 _assetId, - address _prevMsgSender, - uint256 _amount - ) external payable onlyBridgehubOrEra(_chainId) whenNotPaused { - address l1AssetHandler = assetHandlerAddress[_assetId]; - require(l1AssetHandler != address(0), "ShB: asset handler not set"); - - _transferAllowanceToNTV(_assetId, _amount, _prevMsgSender); - // slither-disable-next-line unused-return - IL1AssetHandler(l1AssetHandler).bridgeBurn{value: msg.value}({ - _chainId: _chainId, - _l2Value: 0, - _assetId: _assetId, - _prevMsgSender: _prevMsgSender, - _data: abi.encode(_amount, address(0)) - }); - - // 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, _assetId, _amount); - } - - /// @notice Initiates a deposit transaction within Bridgehub, used by `requestL2TransactionTwoBridges`. - /// @param _chainId The chain ID of the ZK chain 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. - /// @return request The data used by the bridgehub to create L2 transaction request to specific ZK chain. - function bridgehubDeposit( - uint256 _chainId, - address _prevMsgSender, - uint256 _l2Value, - bytes calldata _data - ) - external - payable - override - onlyBridgehub - whenNotPaused - returns (L2TransactionRequestTwoBridgesInner memory request) - { - bytes32 assetId; - bytes memory transferData; - bool legacyDeposit = false; - bytes1 encodingVersion = _data[0]; - - // The new encoding ensures that the calldata is collision-resistant with respect to the legacy format. - // In the legacy calldata, the first input was the address, meaning the most significant byte was always `0x00`. - if (encodingVersion == 0x01) { - (assetId, transferData) = abi.decode(_data[1:], (bytes32, bytes)); - require( - assetHandlerAddress[assetId] != address(nativeTokenVault), - "ShB: new encoding format not yet supported for NTV" - ); - } else { - (assetId, transferData) = _handleLegacyData(_data, _prevMsgSender); - legacyDeposit = true; - } - - require(BRIDGE_HUB.baseTokenAssetId(_chainId) != assetId, "L1AR: baseToken deposit not supported"); - - bytes memory bridgeMintCalldata = _burn({ - _chainId: _chainId, - _l2Value: _l2Value, - _assetId: assetId, - _prevMsgSender: _prevMsgSender, - _transferData: transferData, - _passValue: true - }); - bytes32 txDataHash = this.encodeTxDataHash(legacyDeposit, _prevMsgSender, assetId, transferData); - - request = _requestToBridge({ - _prevMsgSender: _prevMsgSender, - _assetId: assetId, - _bridgeMintCalldata: bridgeMintCalldata, - _txDataHash: txDataHash - }); - - emit BridgehubDepositInitiated({ - chainId: _chainId, - txDataHash: txDataHash, - from: _prevMsgSender, - assetId: assetId, - bridgeMintCalldata: bridgeMintCalldata - }); - } - - /// @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 ZK chain to which confirm the deposit. - /// @param _txDataHash The keccak256 hash of 0x01 || abi.encode(bytes32, bytes) to identify deposits. - /// @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 { - require(depositHappened[_chainId][_txHash] == 0x00, "L1AR: tx hap"); - depositHappened[_chainId][_txHash] = _txDataHash; - emit BridgehubDepositFinalized(_chainId, _txDataHash, _txHash); - } - - /// @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 { - _finalizeWithdrawal({ - _chainId: _chainId, - _l2BatchNumber: _l2BatchNumber, - _l2MessageIndex: _l2MessageIndex, - _l2TxNumberInBatch: _l2TxNumberInBatch, - _message: _message, - _merkleProof: _merkleProof - }); - } - - /// @dev Calls the internal `_encodeTxDataHash`. Used as a wrapped for try / catch case. - /// @param _isLegacyEncoding Boolean flag indicating whether to use the legacy encoding standard (true) or the latest encoding standard (false). - /// @param _prevMsgSender The address of the entity that initiated the deposit. - /// @param _assetId The unique identifier of the deposited L1 token. - /// @param _transferData The encoded transfer data, which includes both the deposit amount and the address of the L2 receiver. - /// @return txDataHash The resulting encoded transaction data hash. - function encodeTxDataHash( - bool _isLegacyEncoding, - address _prevMsgSender, - bytes32 _assetId, - bytes calldata _transferData - ) external view returns (bytes32 txDataHash) { - return _encodeTxDataHash(_isLegacyEncoding, _prevMsgSender, _assetId, _transferData); - } - - /// @dev Withdraw funds from the initiated deposit, that failed when finalizing on L2. - /// @param _chainId The ZK chain id to which deposit was initiated. - /// @param _depositSender The address of the entity that initiated the deposit. - /// @param _assetId The unique identifier of the deposited L1 token. - /// @param _assetData The encoded transfer data, which includes both the deposit amount and the address of the L2 receiver. Might include extra information. - /// @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. - /// @dev Processes claims of failed deposit, whether they originated from the legacy bridge or the current system. - function bridgeRecoverFailedTransfer( - uint256 _chainId, - address _depositSender, - bytes32 _assetId, - bytes memory _assetData, - bytes32 _l2TxHash, - uint256 _l2BatchNumber, - uint256 _l2MessageIndex, - uint16 _l2TxNumberInBatch, - bytes32[] calldata _merkleProof - ) public nonReentrant whenNotPaused { - { - bool proofValid = BRIDGE_HUB.proveL1ToL2TransactionStatus({ - _chainId: _chainId, - _l2TxHash: _l2TxHash, - _l2BatchNumber: _l2BatchNumber, - _l2MessageIndex: _l2MessageIndex, - _l2TxNumberInBatch: _l2TxNumberInBatch, - _merkleProof: _merkleProof, - _status: TxStatus.Failure - }); - require(proofValid, "yn"); - } - - require(!_isEraLegacyDeposit(_chainId, _l2BatchNumber, _l2TxNumberInBatch), "L1AR: legacy cFD"); - { - bytes32 dataHash = depositHappened[_chainId][_l2TxHash]; - // Determine if the given dataHash matches the calculated legacy transaction hash. - bool isLegacyTxDataHash = _isLegacyTxDataHash(_depositSender, _assetId, _assetData, dataHash); - // If the dataHash matches the legacy transaction hash, skip the next step. - // Otherwise, perform the check using the new transaction data hash encoding. - if (!isLegacyTxDataHash) { - bytes32 txDataHash = _encodeTxDataHash(false, _depositSender, _assetId, _assetData); - require(dataHash == txDataHash, "L1AR: d.it not hap"); - } - } - delete depositHappened[_chainId][_l2TxHash]; - - IL1AssetHandler(assetHandlerAddress[_assetId]).bridgeRecoverFailedTransfer( - _chainId, - _assetId, - _depositSender, - _assetData - ); - - emit ClaimedFailedDepositSharedBridge(_chainId, _depositSender, _assetId, _assetData); - } - - /// @dev Receives and parses (name, symbol, decimals) from the token contract - function getERC20Getters(address _token) public view returns (bytes memory) { - return BridgeHelper.getERC20Getters(_token, ETH_TOKEN_ADDRESS); - } - - /// @dev send the burn message to the asset - /// @notice Forwards the burn request for specific asset to respective asset handler - /// @param _chainId The chain ID of the ZK chain to which deposit. - /// @param _l2Value The L2 `msg.value` from the L1 -> L2 deposit transaction. - /// @param _assetId The deposited asset ID. - /// @param _prevMsgSender The `msg.sender` address from the external call that initiated current one. - /// @param _transferData The encoded data, which is used by the asset handler to determine L2 recipient and amount. Might include extra information. - /// @param _passValue Boolean indicating whether to pass msg.value in the call. - /// @return bridgeMintCalldata The calldata used by remote asset handler to mint tokens for recipient. - function _burn( - uint256 _chainId, - uint256 _l2Value, - bytes32 _assetId, - address _prevMsgSender, - bytes memory _transferData, - bool _passValue - ) internal returns (bytes memory bridgeMintCalldata) { - address l1AssetHandler = assetHandlerAddress[_assetId]; - require(l1AssetHandler != address(0), "ShB: asset handler does not exist for assetId"); - - uint256 msgValue = _passValue ? msg.value : 0; - bridgeMintCalldata = IL1AssetHandler(l1AssetHandler).bridgeBurn{value: msgValue}({ - _chainId: _chainId, - _l2Value: _l2Value, - _assetId: _assetId, - _prevMsgSender: _prevMsgSender, - _data: _transferData - }); - } - - struct MessageParams { - uint256 l2BatchNumber; - uint256 l2MessageIndex; - uint16 l2TxNumberInBatch; - } - - /// @notice Internal function that handles the logic for finalizing withdrawals, supporting both the current bridge system and the legacy ERC20 bridge. - /// @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. - /// @return l1Receiver The address to receive bridged assets. - /// @return assetId The bridged asset ID. - /// @return amount The amount of asset bridged. - function _finalizeWithdrawal( - uint256 _chainId, - uint256 _l2BatchNumber, - uint256 _l2MessageIndex, - uint16 _l2TxNumberInBatch, - bytes calldata _message, - bytes32[] calldata _merkleProof - ) internal nonReentrant whenNotPaused returns (address l1Receiver, bytes32 assetId, uint256 amount) { - require( - !isWithdrawalFinalized[_chainId][_l2BatchNumber][_l2MessageIndex], - "L1AR: Withdrawal is already finalized" - ); - isWithdrawalFinalized[_chainId][_l2BatchNumber][_l2MessageIndex] = true; - - // Handling special case for withdrawal from ZKsync Era initiated before Shared Bridge. - require(!_isEraLegacyEthWithdrawal(_chainId, _l2BatchNumber), "L1AR: legacy eth withdrawal"); - require(!_isEraLegacyTokenWithdrawal(_chainId, _l2BatchNumber), "L1AR: legacy token withdrawal"); - - bytes memory transferData; - { - MessageParams memory messageParams = MessageParams({ - l2BatchNumber: _l2BatchNumber, - l2MessageIndex: _l2MessageIndex, - l2TxNumberInBatch: _l2TxNumberInBatch - }); - (assetId, transferData) = _checkWithdrawal(_chainId, messageParams, _message, _merkleProof); - } - address l1AssetHandler = assetHandlerAddress[assetId]; - // slither-disable-next-line unused-return - IL1AssetHandler(l1AssetHandler).bridgeMint(_chainId, assetId, transferData); - (amount, l1Receiver) = abi.decode(transferData, (uint256, address)); - - emit WithdrawalFinalizedSharedBridge(_chainId, l1Receiver, assetId, amount); - } - - /// @notice Decodes the transfer input for legacy data and transfers allowance to NTV - /// @dev Is not applicable for custom asset handlers - /// @param _data encoded transfer data (address _l1Token, uint256 _depositAmount, address _l2Receiver) - /// @param _prevMsgSender address of the deposit initiator - function _handleLegacyData(bytes calldata _data, address _prevMsgSender) internal returns (bytes32, bytes memory) { - (address _l1Token, uint256 _depositAmount, address _l2Receiver) = abi.decode( - _data, - (address, uint256, address) - ); - bytes32 assetId = _ensureTokenRegisteredWithNTV(_l1Token); - _transferAllowanceToNTV(assetId, _depositAmount, _prevMsgSender); - return (assetId, abi.encode(_depositAmount, _l2Receiver)); - } - - function _ensureTokenRegisteredWithNTV(address _l1Token) internal returns (bytes32 assetId) { - assetId = DataEncoding.encodeNTVAssetId(block.chainid, _l1Token); - if (nativeTokenVault.tokenAddress(assetId) == address(0)) { - nativeTokenVault.registerToken(_l1Token); - } - } - - /// @notice Transfers allowance to Native Token Vault, if the asset is registered with it. Does nothing for ETH or non-registered tokens. - /// @dev assetId is not the padded address, but the correct encoded id (NTV stores respective format for IDs) - function _transferAllowanceToNTV(bytes32 _assetId, uint256 _amount, address _prevMsgSender) internal { - address l1TokenAddress = nativeTokenVault.tokenAddress(_assetId); - if (l1TokenAddress == address(0) || l1TokenAddress == ETH_TOKEN_ADDRESS) { - return; - } - IERC20 l1Token = IERC20(l1TokenAddress); - - // Do the transfer if allowance to Shared bridge is bigger than amount - // And if there is not enough allowance for the NTV - if ( - l1Token.allowance(_prevMsgSender, address(this)) >= _amount && - l1Token.allowance(_prevMsgSender, address(nativeTokenVault)) < _amount - ) { - // slither-disable-next-line arbitrary-send-erc20 - l1Token.safeTransferFrom(_prevMsgSender, address(this), _amount); - l1Token.forceApprove(address(nativeTokenVault), _amount); - } - } - - /// @dev The request data that is passed to the bridgehub - function _requestToBridge( - address _prevMsgSender, - bytes32 _assetId, - bytes memory _bridgeMintCalldata, - bytes32 _txDataHash - ) internal view returns (L2TransactionRequestTwoBridgesInner memory request) { - // Request the finalization of the deposit on the L2 side - bytes memory l2TxCalldata = _getDepositL2Calldata(_prevMsgSender, _assetId, _bridgeMintCalldata); - - request = L2TransactionRequestTwoBridgesInner({ - magicValue: TWO_BRIDGES_MAGIC_VALUE, - l2Contract: L2_ASSET_ROUTER_ADDR, - l2Calldata: l2TxCalldata, - factoryDeps: new bytes[](0), - txDataHash: _txDataHash - }); - } - - /// @dev Generate a calldata for calling the deposit finalization on the L2 bridge contract - function _getDepositL2Calldata( - address _l1Sender, - bytes32 _assetId, - bytes memory _assetData - ) internal view returns (bytes memory) { - // First branch covers the case when asset is not registered with NTV (custom asset handler) - // Second branch handles tokens registered with NTV and uses legacy calldata encoding - if (nativeTokenVault.tokenAddress(_assetId) == address(0)) { - return abi.encodeCall(IL2Bridge.finalizeDeposit, (_assetId, _assetData)); - } else { - // slither-disable-next-line unused-return - (, address _l2Receiver, address _parsedL1Token, uint256 _amount, bytes memory _gettersData) = DataEncoding - .decodeBridgeMintData(_assetData); - return - abi.encodeCall( - IL2BridgeLegacy.finalizeDeposit, - (_l1Sender, _l2Receiver, _parsedL1Token, _amount, _gettersData) - ); - } - } - - /// @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) { - require((_chainId != ERA_CHAIN_ID) || eraPostDiamondUpgradeFirstBatch != 0, "L1AR: diamondUFB not set for Era"); - 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) { - require( - (_chainId != ERA_CHAIN_ID) || eraPostLegacyBridgeUpgradeFirstBatch != 0, - "L1AR: LegacyUFB not set for Era" - ); - return (_chainId == ERA_CHAIN_ID) && (_l2BatchNumber < eraPostLegacyBridgeUpgradeFirstBatch); - } - - /// @dev Determines if the provided data for a failed deposit corresponds to a legacy failed deposit. - /// @param _prevMsgSender The address of the entity that initiated the deposit. - /// @param _assetId The unique identifier of the deposited L1 token. - /// @param _transferData The encoded transfer data, which includes both the deposit amount and the address of the L2 receiver. - /// @param _expectedTxDataHash The nullifier data hash stored for the failed deposit. - /// @return isLegacyTxDataHash True if the transaction is legacy, false otherwise. - function _isLegacyTxDataHash( - address _prevMsgSender, - bytes32 _assetId, - bytes memory _transferData, - bytes32 _expectedTxDataHash - ) internal view returns (bool isLegacyTxDataHash) { - try this.encodeTxDataHash(true, _prevMsgSender, _assetId, _transferData) returns (bytes32 txDataHash) { - return txDataHash == _expectedTxDataHash; - } catch { - return false; - } - } - - /// @dev Encodes the transaction data hash using either the latest encoding standard or the legacy standard. - /// @param _isLegacyEncoding Boolean flag indicating whether to use the legacy encoding standard (true) or the latest encoding standard (false). - /// @param _prevMsgSender The address of the entity that initiated the deposit. - /// @param _assetId The unique identifier of the deposited L1 token. - /// @param _transferData The encoded transfer data, which includes both the deposit amount and the address of the L2 receiver. - /// @return txDataHash The resulting encoded transaction data hash. - function _encodeTxDataHash( - bool _isLegacyEncoding, - address _prevMsgSender, - bytes32 _assetId, - bytes memory _transferData - ) internal view returns (bytes32 txDataHash) { - if (_isLegacyEncoding) { - (uint256 depositAmount, ) = abi.decode(_transferData, (uint256, address)); - txDataHash = keccak256(abi.encode(_prevMsgSender, nativeTokenVault.tokenAddress(_assetId), depositAmount)); - } else { - // Similarly to calldata, the txDataHash is collision-resistant. - // In the legacy data hash, the first encoded variable was the address, which is padded with zeros during `abi.encode`. - txDataHash = keccak256(bytes.concat(bytes1(0x01), abi.encode(_prevMsgSender, _assetId, _transferData))); - } - } - - /// @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) { - require( - (_chainId != ERA_CHAIN_ID) || (eraLegacyBridgeLastDepositBatch != 0), - "L1AR: last deposit time not set for Era" - ); - return - (_chainId == ERA_CHAIN_ID) && - (_l2BatchNumber < eraLegacyBridgeLastDepositBatch || - (_l2TxNumberInBatch <= eraLegacyBridgeLastDepositTxNumber && - _l2BatchNumber == eraLegacyBridgeLastDepositBatch)); - } - - /// @notice Verifies the validity of a withdrawal message from L2 and returns withdrawal details. - /// @param _chainId The chain ID of the transaction to check. - /// @param _messageParams The message params, which include batch number, message index, and L2 tx number in batch. - /// @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 assetId The ID of the bridged asset. - /// @return transferData The transfer data used to finalize withdawal. - function _checkWithdrawal( - uint256 _chainId, - MessageParams memory _messageParams, - bytes calldata _message, - bytes32[] calldata _merkleProof - ) internal view returns (bytes32 assetId, bytes memory transferData) { - (assetId, transferData) = _parseL2WithdrawalMessage(_chainId, _message); - L2Message memory l2ToL1Message; - { - bool baseTokenWithdrawal = (assetId == BRIDGE_HUB.baseTokenAssetId(_chainId)); - address l2Sender = baseTokenWithdrawal ? L2_BASE_TOKEN_SYSTEM_CONTRACT_ADDR : L2_ASSET_ROUTER_ADDR; - - 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 - }); - require(success, "L1AR: withd w proof"); // withdrawal wrong proof - } - - /// @notice Parses the withdrawal message and returns withdrawal details. - /// @dev Currently, 3 different encoding versions are supported: legacy mailbox withdrawal, ERC20 bridge withdrawal, - /// @dev and the latest version supported by shared bridge. Selectors are used for versioning. - /// @param _chainId The ZK chain ID. - /// @param _l2ToL1message The encoded L2 -> L1 message. - /// @return assetId The ID of the bridged asset. - /// @return transferData The transfer data used to finalize withdawal. - function _parseL2WithdrawalMessage( - uint256 _chainId, - bytes memory _l2ToL1message - ) internal view returns (bytes32 assetId, bytes memory transferData) { - // We check that the message is long enough to read the data. - // Please note that there are three versions of the message: - // 1. The message that is sent by `withdraw(address _l1Receiver)` or `withdrawWithMessage`. In the second case, this function ignores the extra data - // It should be equal to the length of the bytes4 function signature + address l1Receiver + uint256 amount = 4 + 20 + 32 = 56 (bytes). - // 2. The legacy `getL1WithdrawMessage`, the length of the data is known. - // 3. The message that is encoded by `getL1WithdrawMessage(bytes32 _assetId, bytes memory _bridgeMintData)` - // No length is assumed. The assetId is decoded and the mintData is passed to respective assetHandler - - (uint32 functionSignature, uint256 offset) = UnsafeBytes.readUint32(_l2ToL1message, 0); - if (bytes4(functionSignature) == IMailbox.finalizeEthWithdrawal.selector) { - uint256 amount; - address l1Receiver; - - // The data is expected to be at least 56 bytes long. - require(_l2ToL1message.length >= 56, "L1AR: wrong msg len"); // wrong message length - // this message is a base token withdrawal - (l1Receiver, offset) = UnsafeBytes.readAddress(_l2ToL1message, offset); - // slither-disable-next-line unused-return - (amount, ) = UnsafeBytes.readUint256(_l2ToL1message, offset); - assetId = BRIDGE_HUB.baseTokenAssetId(_chainId); - transferData = abi.encode(amount, l1Receiver); - } else if (bytes4(functionSignature) == IL1ERC20Bridge.finalizeWithdrawal.selector) { - address l1Token; - uint256 amount; - address l1Receiver; - // 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). - require(_l2ToL1message.length == 76, "L1AR: wrong msg len 2"); - (l1Receiver, offset) = UnsafeBytes.readAddress(_l2ToL1message, offset); - (l1Token, offset) = UnsafeBytes.readAddress(_l2ToL1message, offset); - // slither-disable-next-line unused-return - (amount, ) = UnsafeBytes.readUint256(_l2ToL1message, offset); - - assetId = DataEncoding.encodeNTVAssetId(block.chainid, l1Token); - transferData = abi.encode(amount, l1Receiver); - } else if (bytes4(functionSignature) == this.finalizeWithdrawal.selector) { - // The data is expected to be at least 36 bytes long to contain assetId. - require(_l2ToL1message.length >= 36, "L1AR: wrong msg len"); // wrong message length - (assetId, offset) = UnsafeBytes.readBytes32(_l2ToL1message, offset); - transferData = UnsafeBytes.readRemainingBytes(_l2ToL1message, offset); - } else { - revert("L1AR: Incorrect message function selector"); - } - } - - /*////////////////////////////////////////////////////////////// - SHARED BRIDGE TOKEN BRIDGING LEGACY FUNCTIONS - //////////////////////////////////////////////////////////////*/ - - /// @notice Withdraw funds from the initiated deposit, that failed when finalizing on L2. - /// @dev Cannot be used to claim deposits made with new encoding. - /// @param _chainId The ZK chain id to which deposit was initiated. - /// @param _depositSender The address of the deposit initiator. - /// @param _l1Asset 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 _l1Asset, - uint256 _amount, - bytes32 _l2TxHash, - uint256 _l2BatchNumber, - uint256 _l2MessageIndex, - uint16 _l2TxNumberInBatch, - bytes32[] calldata _merkleProof - ) external override { - bytes32 assetId = DataEncoding.encodeNTVAssetId(block.chainid, _l1Asset); - // For legacy deposits, the l2 receiver is not required to check tx data hash - bytes memory transferData = abi.encode(_amount, address(0)); - bridgeRecoverFailedTransfer({ - _chainId: _chainId, - _depositSender: _depositSender, - _assetId: assetId, - _assetData: transferData, - _l2TxHash: _l2TxHash, - _l2BatchNumber: _l2BatchNumber, - _l2MessageIndex: _l2MessageIndex, - _l2TxNumberInBatch: _l2TxNumberInBatch, - _merkleProof: _merkleProof - }); - } - - /*////////////////////////////////////////////////////////////// - ERA ERC20 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 txHash 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 txHash) { - require(_l1Token != L1_WETH_TOKEN, "L1AR: WETH deposit not supported 2"); - - bytes32 _assetId; - bytes memory bridgeMintCalldata; - - { - // Inner call to encode data to decrease local var numbers - _assetId = _ensureTokenRegisteredWithNTV(_l1Token); - IERC20(_l1Token).forceApprove(address(nativeTokenVault), _amount); - } - - { - bridgeMintCalldata = _burn({ - _chainId: ERA_CHAIN_ID, - _l2Value: 0, - _assetId: _assetId, - _prevMsgSender: _prevMsgSender, - _transferData: abi.encode(_amount, _l2Receiver), - _passValue: false - }); - } - - { - bytes memory l2TxCalldata = _getDepositL2Calldata(_prevMsgSender, _assetId, bridgeMintCalldata); - - // 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: L2_ASSET_ROUTER_ADDR, - mintValue: msg.value, // l2 gas + l2 msg.Value the bridgehub will withdraw the mintValue from the shared bridge (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 - }); - txHash = BRIDGE_HUB.requestL2TransactionDirect{value: msg.value}(request); - } - - // Save the deposited amount to claim funds on L1 if the deposit failed on L2 - depositHappened[ERA_CHAIN_ID][txHash] = keccak256(abi.encode(_prevMsgSender, _l1Token, _amount)); - - emit LegacyDepositInitiated({ - chainId: ERA_CHAIN_ID, - l2DepositTxHash: txHash, - from: _prevMsgSender, - to: _l2Receiver, - l1Asset: _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 l1Asset 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 l1Asset, uint256 amount) { - bytes32 assetId; - (l1Receiver, assetId, amount) = _finalizeWithdrawal({ - _chainId: ERA_CHAIN_ID, - _l2BatchNumber: _l2BatchNumber, - _l2MessageIndex: _l2MessageIndex, - _l2TxNumberInBatch: _l2TxNumberInBatch, - _message: _message, - _merkleProof: _merkleProof - }); - l1Asset = nativeTokenVault.tokenAddress(assetId); - } - - /*////////////////////////////////////////////////////////////// - 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/l1-contracts/contracts/bridge/L1NativeTokenVault.sol b/l1-contracts/contracts/bridge/L1NativeTokenVault.sol deleted file mode 100644 index fba532597..000000000 --- a/l1-contracts/contracts/bridge/L1NativeTokenVault.sol +++ /dev/null @@ -1,259 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -// solhint-disable reason-string, gas-custom-errors - -import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable-v4/access/Ownable2StepUpgradeable.sol"; -import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable-v4/security/PausableUpgradeable.sol"; - -import {IERC20} from "@openzeppelin/contracts-v4/token/ERC20/IERC20.sol"; -import {SafeERC20} from "@openzeppelin/contracts-v4/token/ERC20/utils/SafeERC20.sol"; - -import {IL1NativeTokenVault} from "./interfaces/IL1NativeTokenVault.sol"; -import {IL1AssetHandler} from "./interfaces/IL1AssetHandler.sol"; - -import {IL1AssetRouter} from "./interfaces/IL1AssetRouter.sol"; -import {ETH_TOKEN_ADDRESS} from "../common/Config.sol"; -import {DataEncoding} from "../common/libraries/DataEncoding.sol"; - -import {BridgeHelper} from "./BridgeHelper.sol"; - -/// @author Matter Labs -/// @custom:security-contact security@matterlabs.dev -/// @dev Vault holding L1 native ETH and ERC20 tokens bridged into the ZK chains. -/// @dev Designed for use with a proxy for upgradability. -contract L1NativeTokenVault is IL1NativeTokenVault, IL1AssetHandler, Ownable2StepUpgradeable, PausableUpgradeable { - using SafeERC20 for IERC20; - - /// @dev The address of the WETH token on L1. - address public immutable override L1_WETH_TOKEN; - - /// @dev L1 Shared Bridge smart contract that handles communication with its counterparts on L2s - IL1AssetRouter public immutable override L1_SHARED_BRIDGE; - - /// @dev Maps token balances for each chain to prevent unauthorized spending across ZK chains. - /// 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; - - /// @dev A mapping assetId => tokenAddress - mapping(bytes32 assetId => address tokenAddress) public tokenAddress; - - /// @notice Checks that the message sender is the bridge. - modifier onlyBridge() { - require(msg.sender == address(L1_SHARED_BRIDGE), "NTV not ShB"); - _; - } - - /// @dev Contract is expected to be used as proxy implementation. - /// @dev Initialize the implementation to prevent Parity hack. - constructor(address _l1WethAddress, IL1AssetRouter _l1SharedBridge) { - _disableInitializers(); - L1_WETH_TOKEN = _l1WethAddress; - L1_SHARED_BRIDGE = _l1SharedBridge; - } - - /// @dev Accepts ether only from the Shared Bridge. - receive() external payable { - require(address(L1_SHARED_BRIDGE) == msg.sender, "NTV: ETH only accepted from Shared Bridge"); - } - - /// @dev Initializes a contract for later use. Expected to be used in the proxy - /// @param _owner Address which can change pause / unpause the NTV - /// 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 initializer { - require(_owner != address(0), "NTV owner 0"); - _transferOwnership(_owner); - } - - /// @notice Transfers tokens from shared bridge as part of the migration process. - /// @dev Both ETH and ERC20 tokens can be transferred. Exhausts balance of shared bridge after the first call. - /// @dev Calling second time for the same token will revert. - /// @param _token The address of token to be transferred (address(1) for ether and contract address for ERC20). - function transferFundsFromSharedBridge(address _token) external { - if (_token == ETH_TOKEN_ADDRESS) { - uint256 balanceBefore = address(this).balance; - L1_SHARED_BRIDGE.transferTokenToNTV(_token); - uint256 balanceAfter = address(this).balance; - require(balanceAfter > balanceBefore, "NTV: 0 eth transferred"); - } else { - uint256 balanceBefore = IERC20(_token).balanceOf(address(this)); - uint256 sharedBridgeChainBalance = IERC20(_token).balanceOf(address(L1_SHARED_BRIDGE)); - require(sharedBridgeChainBalance > 0, "NTV: 0 amount to transfer"); - L1_SHARED_BRIDGE.transferTokenToNTV(_token); - uint256 balanceAfter = IERC20(_token).balanceOf(address(this)); - require(balanceAfter - balanceBefore >= sharedBridgeChainBalance, "NTV: wrong amount transferred"); - } - } - - /// @notice Updates chain token balance within NTV to account for tokens transferred from the shared bridge (part of the migration process). - /// @dev Clears chain balance on the shared bridge after the first call. Subsequent calls will not affect the state. - /// @param _token The address of token to be transferred (address(1) for ether and contract address for ERC20). - /// @param _targetChainId The chain ID of the corresponding ZK chain. - function updateChainBalancesFromSharedBridge(address _token, uint256 _targetChainId) external { - uint256 sharedBridgeChainBalance = L1_SHARED_BRIDGE.chainBalance(_targetChainId, _token); - chainBalance[_targetChainId][_token] = chainBalance[_targetChainId][_token] + sharedBridgeChainBalance; - L1_SHARED_BRIDGE.nullifyChainBalanceByNTV(_targetChainId, _token); - } - - /// @notice Registers tokens within the NTV. - /// @dev The goal was to allow bridging L1 native tokens automatically, by registering them on the fly. - /// @notice Allows the bridge to register a token address for the vault. - /// @notice No access control is ok, since the bridging of tokens should be permissionless. This requires permissionless registration. - function registerToken(address _l1Token) external { - require(_l1Token != L1_WETH_TOKEN, "NTV: WETH deposit not supported"); - require(_l1Token == ETH_TOKEN_ADDRESS || _l1Token.code.length > 0, "NTV: empty token"); - bytes32 assetId = DataEncoding.encodeNTVAssetId(block.chainid, _l1Token); - L1_SHARED_BRIDGE.setAssetHandlerAddressInitial(bytes32(uint256(uint160(_l1Token))), address(this)); - tokenAddress[assetId] = _l1Token; - } - - /// @inheritdoc IL1AssetHandler - function bridgeMint( - uint256 _chainId, - bytes32 _assetId, - bytes calldata _data - ) external payable override onlyBridge whenNotPaused returns (address l1Receiver) { - // here we are minting the tokens after the bridgeBurn has happened on an L2, so we can assume the l1Token is not zero - address l1Token = tokenAddress[_assetId]; - uint256 amount; - (amount, l1Receiver) = abi.decode(_data, (uint256, address)); - // Check that the chain has sufficient balance - require(chainBalance[_chainId][l1Token] >= amount, "NTV: not enough funds"); // not enough funds - 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) - } - require(callSuccess, "NTV: withdrawal failed, no funds or cannot transfer to receiver"); - } else { - // Withdraw funds - IERC20(l1Token).safeTransfer(l1Receiver, amount); - } - emit BridgeMint(_chainId, _assetId, l1Receiver, amount); - } - - /// @inheritdoc IL1AssetHandler - /// @notice Allows bridgehub to acquire mintValue for L1->L2 transactions. - /// @dev In case of native token vault _data is the tuple of _depositAmount and _l2Receiver. - function bridgeBurn( - uint256 _chainId, - uint256, - bytes32 _assetId, - address _prevMsgSender, - bytes calldata _data - ) external payable override onlyBridge whenNotPaused returns (bytes memory _bridgeMintData) { - (uint256 _depositAmount, address _l2Receiver) = abi.decode(_data, (uint256, address)); - - uint256 amount; - address l1Token = tokenAddress[_assetId]; - if (l1Token == ETH_TOKEN_ADDRESS) { - amount = msg.value; - - // In the old SDK/contracts the user had to always provide `0` as the deposit amount for ETH token, while - // ultimately the provided `msg.value` was used as the deposit amount. This check is needed for backwards compatibility. - if (_depositAmount == 0) { - _depositAmount = amount; - } - - require(_depositAmount == amount, "L1NTV: msg.value not equal to amount"); - } else { - // The Bridgehub also checks this, but we want to be sure - require(msg.value == 0, "NTV m.v > 0 b d.it"); - amount = _depositAmount; - - uint256 expectedDepositAmount = _depositFunds(_prevMsgSender, IERC20(l1Token), _depositAmount); // note if _prevMsgSender is this contract, this will return 0. This does not happen. - require(expectedDepositAmount == _depositAmount, "5T"); // The token has non-standard transfer logic - } - require(amount != 0, "6T"); // empty deposit amount - - chainBalance[_chainId][l1Token] += amount; - - _bridgeMintData = DataEncoding.encodeBridgeMintData({ - _prevMsgSender: _prevMsgSender, - _l2Receiver: _l2Receiver, - _l1Token: l1Token, - _amount: amount, - _erc20Metadata: getERC20Getters(l1Token) - }); - - emit BridgeBurn({ - chainId: _chainId, - assetId: _assetId, - l1Sender: _prevMsgSender, - l2receiver: _l2Receiver, - amount: amount - }); - } - - /// @inheritdoc IL1AssetHandler - function bridgeRecoverFailedTransfer( - uint256 _chainId, - bytes32 _assetId, - address _depositSender, - bytes calldata _data - ) external payable override onlyBridge whenNotPaused { - (uint256 _amount, ) = abi.decode(_data, (uint256, address)); - address l1Token = tokenAddress[_assetId]; - require(_amount > 0, "y1"); - - // check that the chain has sufficient balance - require(chainBalance[_chainId][l1Token] >= _amount, "NTV: not enough funds 2"); - 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(), _depositSender, _amount, 0, 0, 0, 0) - } - require(callSuccess, "NTV: claimFailedDeposit failed, no funds or cannot transfer to receiver"); - } 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. - } - } - - /// @dev Receives and parses (name, symbol, decimals) from the token contract - function getERC20Getters(address _token) public view returns (bytes memory) { - return BridgeHelper.getERC20Getters(_token, ETH_TOKEN_ADDRESS); - } - - /// @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)); - address from = _from; - // in the legacy scenario the SharedBridge was granting the allowance, we have to transfer from them instead of the user - if ( - _token.allowance(address(L1_SHARED_BRIDGE), address(this)) >= _amount && - _token.allowance(_from, address(this)) < _amount - ) { - from = address(L1_SHARED_BRIDGE); - } - // slither-disable-next-line arbitrary-send-erc20 - _token.safeTransferFrom(from, address(this), _amount); - uint256 balanceAfter = _token.balanceOf(address(this)); - - return balanceAfter - balanceBefore; - } - - /*////////////////////////////////////////////////////////////// - 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/l1-contracts/contracts/state-transition/libraries/Merkle.sol b/l1-contracts/contracts/state-transition/libraries/Merkle.sol deleted file mode 100644 index 57701f338..000000000 --- a/l1-contracts/contracts/state-transition/libraries/Merkle.sol +++ /dev/null @@ -1,54 +0,0 @@ -// 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/l1-contracts/test/foundry/unit/concrete/state-transition/StateTransitionManager/FreezeChain.t.sol b/l1-contracts/test/foundry/unit/concrete/state-transition/StateTransitionManager/FreezeChain.t.sol deleted file mode 100644 index 20dd04e92..000000000 --- a/l1-contracts/test/foundry/unit/concrete/state-transition/StateTransitionManager/FreezeChain.t.sol +++ /dev/null @@ -1,26 +0,0 @@ -// // SPDX-License-Identifier: MIT -pragma solidity 0.8.24; - -import {StateTransitionManagerTest} from "./_StateTransitionManager_Shared.t.sol"; -import {GettersFacet} from "contracts/state-transition/chain-deps/facets/Getters.sol"; -import {IAdmin} from "contracts/state-transition/chain-interfaces/IAdmin.sol"; -import {FacetIsFrozen} from "contracts/common/L1ContractErrors.sol"; - -contract freezeChainTest is StateTransitionManagerTest { - // function test_FreezingChain() public { - // createNewChain(getDiamondCutData(diamondInit)); - // address newChainAddress = chainContractAddress.getHyperchain(chainId); - // GettersFacet gettersFacet = GettersFacet(newChainAddress); - // bool isChainFrozen = gettersFacet.isDiamondStorageFrozen(); - // assertEq(isChainFrozen, false); - // vm.stopPrank(); - // vm.startPrank(governor); - // chainContractAddress.freezeChain(block.chainid); - // // Repeated call should revert - // vm.expectRevert(bytes.concat("q1")); // storage frozen - // chainContractAddress.freezeChain(block.chainid); - // // Call fails as storage is frozen - // vm.expectRevert(bytes.concat("q1")); - // isChainFrozen = gettersFacet.isDiamondStorageFrozen(); - // } -} diff --git a/l1-contracts/test/foundry/unit/concrete/state-transition/chain-deps/facets/Getters/PriorityQueueFrontOperation.t.sol b/l1-contracts/test/foundry/unit/concrete/state-transition/chain-deps/facets/Getters/PriorityQueueFrontOperation.t.sol deleted file mode 100644 index ac8ccfeaa..000000000 --- a/l1-contracts/test/foundry/unit/concrete/state-transition/chain-deps/facets/Getters/PriorityQueueFrontOperation.t.sol +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import {GettersFacetTest} from "./_Getters_Shared.t.sol"; -import {PriorityOperation} from "contracts/state-transition/libraries/PriorityQueue.sol"; -import {QueueIsEmpty} from "contracts/common/L1ContractErrors.sol"; - -contract GetPriorityQueueFrontOperationTest is GettersFacetTest { - function test_revertWhen_queueIsEmpty() public { - vm.expectRevert(QueueIsEmpty.selector); - gettersFacet.priorityQueueFrontOperation(); - } - - function test() public { - PriorityOperation memory expected = PriorityOperation({ - canonicalTxHash: bytes32(uint256(1)), - expirationTimestamp: uint64(2), - layer2Tip: uint192(3) - }); - - gettersFacetWrapper.util_setPriorityQueueFrontOperation(expected); - - PriorityOperation memory received = gettersFacet.priorityQueueFrontOperation(); - - bytes32 expectedHash = keccak256(abi.encode(expected)); - bytes32 receivedHash = keccak256(abi.encode(received)); - assertEq(expectedHash, receivedHash, "Priority queue front operation is incorrect"); - } -} diff --git a/l1-contracts/test/foundry/unit/concrete/state-transition/libraries/Merkle/Merkle.t.sol b/l1-contracts/test/foundry/unit/concrete/state-transition/libraries/Merkle/Merkle.t.sol deleted file mode 100644 index 89514fc99..000000000 --- a/l1-contracts/test/foundry/unit/concrete/state-transition/libraries/Merkle/Merkle.t.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.24; - -import {Test} from "forge-std/Test.sol"; -import {MerkleTest} from "contracts/dev-contracts/test/MerkleTest.sol"; -import {MerkleTreeNoSort} from "./MerkleTreeNoSort.sol"; -import {MerklePathEmpty, MerkleIndexOutOfBounds, MerklePathOutOfBounds} from "contracts/common/L1ContractErrors.sol"; - -contract MerkleTestTest is Test { - MerkleTreeNoSort merkleTree; - MerkleTest merkleTest; - bytes32[] elements; - bytes32 root; - - function setUp() public { - merkleTree = new MerkleTreeNoSort(); - merkleTest = new MerkleTest(); - - for (uint256 i = 0; i < 65; i++) { - elements.push(keccak256(abi.encodePacked(i))); - } - - root = merkleTree.getRoot(elements); - } - - function testElements(uint256 i) public { - vm.assume(i < elements.length); - bytes32 leaf = elements[i]; - bytes32[] memory proof = merkleTree.getProof(elements, i); - - bytes32 rootFromContract = merkleTest.calculateRoot(proof, i, leaf); - - assertEq(rootFromContract, root); - } - - function testFirstElement() public { - testElements(0); - } - - function testLastElement() public { - testElements(elements.length - 1); - } - - function testEmptyProof_shouldRevert() public { - bytes32 leaf = elements[0]; - bytes32[] memory proof; - - vm.expectRevert(MerklePathEmpty.selector); - merkleTest.calculateRoot(proof, 0, leaf); - } - - function testLeafIndexTooBig_shouldRevert() public { - bytes32 leaf = elements[0]; - bytes32[] memory proof = merkleTree.getProof(elements, 0); - - vm.expectRevert(MerkleIndexOutOfBounds.selector); - merkleTest.calculateRoot(proof, 2 ** 255, leaf); - } - - function testProofLengthTooLarge_shouldRevert() public { - bytes32 leaf = elements[0]; - bytes32[] memory proof = new bytes32[](256); - - vm.expectRevert(MerklePathOutOfBounds.selector); - merkleTest.calculateRoot(proof, 0, leaf); - } -} diff --git a/l2-contracts/contracts/bridge/L2AssetRouter.sol b/l2-contracts/contracts/bridge/L2AssetRouter.sol deleted file mode 100644 index d143517b1..000000000 --- a/l2-contracts/contracts/bridge/L2AssetRouter.sol +++ /dev/null @@ -1,198 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.20; - -import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; -import {UpgradeableBeacon} from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; - -import {IL2AssetRouter} from "./interfaces/IL2AssetRouter.sol"; -import {IL1AssetRouter} from "./interfaces/IL1AssetRouter.sol"; -import {ILegacyL2SharedBridge} from "./interfaces/ILegacyL2SharedBridge.sol"; -import {IL2AssetHandler} from "./interfaces/IL2AssetHandler.sol"; -import {IL2StandardToken} from "./interfaces/IL2StandardToken.sol"; -import {IL2NativeTokenVault} from "./interfaces/IL2NativeTokenVault.sol"; - -import {AddressAliasHelper} from "../vendor/AddressAliasHelper.sol"; -import {L2ContractHelper, L2_NATIVE_TOKEN_VAULT} from "../L2ContractHelper.sol"; -import {DataEncoding} from "../common/libraries/DataEncoding.sol"; - -import {EmptyAddress, InvalidCaller} 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 L2AssetRouter is IL2AssetRouter, ILegacyL2SharedBridge, Initializable { - /// @dev Chain ID of Era for legacy reasons - uint256 public immutable ERA_CHAIN_ID; - - /// @dev Chain ID of L1 for bridging reasons - uint256 public immutable L1_CHAIN_ID; - - /// @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 DEPRECATED_l2TokenBeacon; - - /// @dev Bytecode hash of the proxy for tokens deployed by the bridge. - bytes32 internal DEPRECATED_l2TokenProxyBytecodeHash; - - /// @notice Deprecated. Kept for backwards compatibility. - /// @dev A mapping l2 token address => l1 token address - mapping(address l2Token => address l1Token) public override l1TokenAddress; - - /// @notice Obsolete, as all calls are performed via L1 Shared Bridge. Kept for backwards compatibility. - /// @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 The contract responsible for handling tokens native to a single chain. - IL2NativeTokenVault public nativeTokenVault; - - /// @dev A mapping of asset ID to asset handler address - mapping(bytes32 assetId => address assetHandlerAddress) public override assetHandlerAddress; - - /// @notice Checks that the message sender is the legacy bridge. - modifier onlyL1Bridge() { - // Only the L1 bridge counterpart can initiate and finalize the deposit. - if ( - AddressAliasHelper.undoL1ToL2Alias(msg.sender) != l1Bridge && - AddressAliasHelper.undoL1ToL2Alias(msg.sender) != l1SharedBridge - ) { - revert InvalidCaller(msg.sender); - } - _; - } - - /// @dev Contract is expected to be used as proxy implementation. - /// @dev Disable the initialization to prevent Parity hack. - /// @param _l1SharedBridge The address of the L1 Bridge contract. - /// @param _l1Bridge The address of the legacy L1 Bridge contract. - constructor(uint256 _eraChainId, uint256 _l1ChainId, address _l1SharedBridge, address _l1Bridge) { - ERA_CHAIN_ID = _eraChainId; - L1_CHAIN_ID = _l1ChainId; - if (_l1SharedBridge == address(0)) { - revert EmptyAddress(); - } - - l1SharedBridge = _l1SharedBridge; - if (block.chainid == ERA_CHAIN_ID) { - if (_l1Bridge == address(0)) { - revert EmptyAddress(); - } - if (l1Bridge == address(0)) { - l1Bridge = _l1Bridge; - } - } - _disableInitializers(); - } - - /// @dev Used to set the assedAddress for a given assetId. - /// @dev Will be used by ZK Gateway - function setAssetHandlerAddress(bytes32 _assetId, address _assetAddress) external onlyL1Bridge { - assetHandlerAddress[_assetId] = _assetAddress; - emit AssetHandlerRegistered(_assetId, _assetAddress); - } - - /// @notice Finalize the deposit and mint funds - /// @param _assetId The encoding of the asset on L2 - /// @param _transferData The encoded data required for deposit (address _l1Sender, uint256 _amount, address _l2Receiver, bytes memory erc20Data, address originToken) - function finalizeDeposit(bytes32 _assetId, bytes memory _transferData) public override onlyL1Bridge { - address assetHandler = assetHandlerAddress[_assetId]; - if (assetHandler != address(0)) { - IL2AssetHandler(assetHandler).bridgeMint(L1_CHAIN_ID, _assetId, _transferData); - } else { - L2_NATIVE_TOKEN_VAULT.bridgeMint(L1_CHAIN_ID, _assetId, _transferData); - assetHandlerAddress[_assetId] = address(L2_NATIVE_TOKEN_VAULT); - } - - emit FinalizeDepositSharedBridge(L1_CHAIN_ID, _assetId, _transferData); - } - - /// @notice Initiates a withdrawal by burning funds on the contract and sending the message to L1 - /// where tokens would be unlocked - /// @param _assetId The asset id of the withdrawn asset - /// @param _assetData The data that is passed to the asset handler contract - function withdraw(bytes32 _assetId, bytes memory _assetData) public override { - address assetHandler = assetHandlerAddress[_assetId]; - bytes memory _l1bridgeMintData = IL2AssetHandler(assetHandler).bridgeBurn({ - _chainId: L1_CHAIN_ID, - _mintValue: 0, - _assetId: _assetId, - _prevMsgSender: msg.sender, - _data: _assetData - }); - - bytes memory message = _getL1WithdrawMessage(_assetId, _l1bridgeMintData); - L2ContractHelper.sendMessageToL1(message); - - emit WithdrawalInitiatedSharedBridge(L1_CHAIN_ID, msg.sender, _assetId, _assetData); - } - - /// @notice Encodes the message for l2ToL1log sent during withdraw initialization. - /// @param _assetId The encoding of the asset on L2 which is withdrawn. - /// @param _l1bridgeMintData The calldata used by l1 asset handler to unlock tokens for recipient. - function _getL1WithdrawMessage( - bytes32 _assetId, - bytes memory _l1bridgeMintData - ) internal pure returns (bytes memory) { - // note we use the IL1SharedBridge.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 - // solhint-disable-next-line func-named-parameters - return abi.encodePacked(IL1AssetRouter.finalizeWithdrawal.selector, _assetId, _l1bridgeMintData); - } - - /*////////////////////////////////////////////////////////////// - LEGACY FUNCTIONS - //////////////////////////////////////////////////////////////*/ - - /// @notice Legacy finalizeDeposit. - /// @dev Finalizes the deposit and mint funds. - /// @param _l1Sender The address of token sender on L1. - /// @param _l2Receiver The address of token receiver on L2. - /// @param _l1Token The address of the token transferred. - /// @param _amount The amount of the token transferred. - /// @param _data The metadata of the token transferred. - function finalizeDeposit( - address _l1Sender, - address _l2Receiver, - address _l1Token, - uint256 _amount, - bytes calldata _data - ) external override { - bytes32 assetId = DataEncoding.encodeNTVAssetId(L1_CHAIN_ID, _l1Token); - // solhint-disable-next-line func-named-parameters - bytes memory data = DataEncoding.encodeBridgeMintData(_l1Sender, _l2Receiver, _l1Token, _amount, _data); - finalizeDeposit(assetId, data); - } - - /// @notice Initiates a withdrawal by burning funds on the contract and sending the message to L1 - /// where tokens would be unlocked. - /// @param _l1Receiver The address of token receiver on L1. - /// @param _l2Token The address of the token transferred. - /// @param _amount The amount of the token transferred. - function withdraw(address _l1Receiver, address _l2Token, uint256 _amount) external { - bytes32 assetId = DataEncoding.encodeNTVAssetId(L1_CHAIN_ID, getL1TokenAddress(_l2Token)); - bytes memory data = abi.encode(_amount, _l1Receiver); - withdraw(assetId, data); - } - - /// @notice Legacy getL1TokenAddress. - /// @param _l2Token The address of token on L2. - /// @return The address of token on L1. - function getL1TokenAddress(address _l2Token) public view returns (address) { - return IL2StandardToken(_l2Token).l1Address(); - } - - /// @notice Legacy function used for backward compatibility to return L2 wrapped token - /// @notice address corresponding to provided L1 token address and deployed through NTV. - /// @dev However, the shared bridge can use custom asset handlers such that L2 addresses differ, - /// @dev or an L1 token may not have an L2 counterpart. - /// @param _l1Token The address of token on L1. - /// @return Address of an L2 token counterpart - function l2TokenAddress(address _l1Token) public view returns (address) { - return L2_NATIVE_TOKEN_VAULT.l2TokenAddress(_l1Token); - } -} diff --git a/l2-contracts/contracts/bridge/L2NativeTokenVault.sol b/l2-contracts/contracts/bridge/L2NativeTokenVault.sol deleted file mode 100644 index 56c50d9a8..000000000 --- a/l2-contracts/contracts/bridge/L2NativeTokenVault.sol +++ /dev/null @@ -1,234 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.20; - -import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; -import {BeaconProxy} from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; -import {UpgradeableBeacon} from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; - -import {IL2StandardToken} from "./interfaces/IL2StandardToken.sol"; -import {IL2NativeTokenVault} from "./interfaces/IL2NativeTokenVault.sol"; - -import {L2StandardERC20} from "./L2StandardERC20.sol"; -import {L2ContractHelper, DEPLOYER_SYSTEM_CONTRACT, L2_ASSET_ROUTER, IContractDeployer} from "../L2ContractHelper.sol"; -import {SystemContractsCaller} from "../SystemContractsCaller.sol"; -import {DataEncoding} from "../common/libraries/DataEncoding.sol"; - -import {EmptyAddress, EmptyBytes32, AddressMismatch, AssetIdMismatch, DeployFailed, AmountMustBeGreaterThanZero, InvalidCaller} 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 L2NativeTokenVault is IL2NativeTokenVault, Ownable2StepUpgradeable { - /// @dev Chain ID of L1 for bridging reasons. - uint256 public immutable L1_CHAIN_ID; - - bytes32 internal l2TokenProxyBytecodeHash; - - /// @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; - - mapping(bytes32 assetId => address tokenAddress) public override tokenAddress; - - /// @dev Bytecode hash of the proxy for tokens deployed by the bridge. - - modifier onlyBridge() { - if (msg.sender != address(L2_ASSET_ROUTER)) { - revert InvalidCaller(msg.sender); - // Only L2 bridge can call this method - } - _; - } - - /// @notice Initializes the bridge contract for later use. Expected to be used in the proxy. - /// @param _l1ChainId The L1 chain id differs between mainnet and testnets. - /// @param _l2TokenProxyBytecodeHash The bytecode hash of the proxy for tokens deployed by the bridge. - /// @param _aliasedOwner The address of the governor contract. - constructor(uint256 _l1ChainId, bytes32 _l2TokenProxyBytecodeHash, address _aliasedOwner) { - L1_CHAIN_ID = _l1ChainId; - - _disableInitializers(); - if (_l2TokenProxyBytecodeHash == bytes32(0)) { - revert EmptyBytes32(); - } - if (_aliasedOwner == address(0)) { - revert EmptyAddress(); - } - - l2TokenProxyBytecodeHash = _l2TokenProxyBytecodeHash; - _transferOwnership(_aliasedOwner); - } - - /// @notice Sets L2 token beacon used by wrapped ERC20 tokens deployed by NTV. - /// @dev Sets the l2TokenBeacon, called after initialize. - /// @param _l2TokenBeacon The address of L2 token beacon implementation. - /// @param _l2TokenProxyBytecodeHash The bytecode hash of the L2 token proxy that will be deployed for wrapped tokens. - function setL2TokenBeacon(address _l2TokenBeacon, bytes32 _l2TokenProxyBytecodeHash) external onlyOwner { - l2TokenBeacon = UpgradeableBeacon(_l2TokenBeacon); - l2TokenProxyBytecodeHash = _l2TokenProxyBytecodeHash; - emit L2TokenBeaconUpdated(_l2TokenBeacon, _l2TokenProxyBytecodeHash); - } - - /// @notice Configure L2 token beacon used by wrapped ERC20 tokens deployed by NTV. - /// @dev we don't call this in the constructor, as we need to provide factory deps. - /// @param _contractsDeployedAlready Ensures beacon proxy for standard ERC20 has not been deployed. - function configureL2TokenBeacon(bool _contractsDeployedAlready, address _l2TokenBeacon) external { - if (address(l2TokenBeacon) != address(0)) { - revert AddressMismatch(address(l2TokenBeacon), address(0)); - } - if (_contractsDeployedAlready) { - if (_l2TokenBeacon == address(0)) { - revert EmptyAddress(); - } - l2TokenBeacon = UpgradeableBeacon(_l2TokenBeacon); - } else { - address l2StandardToken = address(new L2StandardERC20{salt: bytes32(0)}()); - l2TokenBeacon = new UpgradeableBeacon{salt: bytes32(0)}(l2StandardToken); - l2TokenBeacon.transferOwnership(owner()); - } - } - - /// @notice Used when the chain receives a transfer from L1 Shared Bridge and correspondingly mints the asset. - /// @param _chainId The chainId that the message is from. - /// @param _assetId The assetId of the asset being bridged. - /// @param _data The abi.encoded transfer data. - function bridgeMint(uint256 _chainId, bytes32 _assetId, bytes calldata _data) external payable override onlyBridge { - address token = tokenAddress[_assetId]; - ( - address _l1Sender, - address _l2Receiver, - address originToken, - uint256 _amount, - bytes memory erc20Data - ) = DataEncoding.decodeBridgeMintData(_data); - - if (token == address(0)) { - address expectedToken = _calculateCreate2TokenAddress(originToken); - bytes32 expectedAssetId = DataEncoding.encodeNTVAssetId(L1_CHAIN_ID, originToken); - if (_assetId != expectedAssetId) { - // Make sure that a NativeTokenVault sent the message - revert AssetIdMismatch(expectedAssetId, _assetId); - } - address deployedToken = _deployL2Token(originToken, erc20Data); - if (deployedToken != expectedToken) { - revert AddressMismatch(expectedToken, deployedToken); - } - tokenAddress[_assetId] = expectedToken; - token = expectedToken; - } - - IL2StandardToken(token).bridgeMint(_l2Receiver, _amount); - /// backwards compatible event - emit FinalizeDeposit(_l1Sender, _l2Receiver, token, _amount); - emit BridgeMint({ - chainId: _chainId, - assetId: _assetId, - sender: _l1Sender, - l2Receiver: _l2Receiver, - amount: _amount - }); - } - - /// @notice Burns wrapped tokens and returns the calldata for L2 -> L1 message. - /// @dev In case of native token vault _data is the tuple of _depositAmount and _l2Receiver. - /// @param _chainId The chainId that the message will be sent to. - /// @param _mintValue The L1 base token value bridged. - /// @param _assetId The L2 assetId of the asset being bridged. - /// @param _prevMsgSender The original caller of the shared bridge. - /// @param _data The abi.encoded transfer data. - /// @return l1BridgeMintData The calldata used by l1 asset handler to unlock tokens for recipient. - function bridgeBurn( - uint256 _chainId, - uint256 _mintValue, - bytes32 _assetId, - address _prevMsgSender, - bytes calldata _data - ) external payable override onlyBridge returns (bytes memory l1BridgeMintData) { - (uint256 _amount, address _l1Receiver) = abi.decode(_data, (uint256, address)); - if (_amount == 0) { - // "Amount cannot be zero"); - revert AmountMustBeGreaterThanZero(); - } - - address l2Token = tokenAddress[_assetId]; - IL2StandardToken(l2Token).bridgeBurn(_prevMsgSender, _amount); - - /// backwards compatible event - emit WithdrawalInitiated(_prevMsgSender, _l1Receiver, l2Token, _amount); - emit BridgeBurn({ - chainId: _chainId, - assetId: _assetId, - l2Sender: _prevMsgSender, - receiver: _l1Receiver, - mintValue: _mintValue, - amount: _amount - }); - l1BridgeMintData = _data; - } - - /// @notice Calculates L2 wrapped token address corresponding to L1 token counterpart. - /// @param _l1Token The address of token on L1. - /// @return expectedToken The address of token on L2. - function l2TokenAddress(address _l1Token) public view override returns (address expectedToken) { - bytes32 expectedAssetId = DataEncoding.encodeNTVAssetId(L1_CHAIN_ID, _l1Token); - expectedToken = tokenAddress[expectedAssetId]; - if (expectedToken == address(0)) { - expectedToken = _calculateCreate2TokenAddress(_l1Token); - } - } - - /// @notice Deploys and initializes the L2 token for the L1 counterpart. - /// @param _l1Token The address of token on L1. - /// @param _erc20Data The ERC20 metadata of the token deployed. - /// @return The address of the beacon proxy (L2 wrapped / bridged token). - function _deployL2Token(address _l1Token, bytes memory _erc20Data) internal returns (address) { - bytes32 salt = _getCreate2Salt(_l1Token); - - BeaconProxy l2Token = _deployBeaconProxy(salt); - L2StandardERC20(address(l2Token)).bridgeInitialize(_l1Token, _erc20Data); - - return address(l2Token); - } - - /// @notice Deploys 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. - /// @param salt The salt used for beacon proxy deployment of L2 wrapped token. - /// @return proxy The beacon proxy, i.e. L2 wrapped / bridged token. - 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))); - } - - /// @notice Calculates L2 wrapped token address given the currently stored beacon proxy bytecode hash and beacon address. - /// @param _l1Token The address of token on L1. - /// @return Address of an L2 token counterpart. - function _calculateCreate2TokenAddress(address _l1Token) internal view returns (address) { - bytes32 constructorInputHash = keccak256(abi.encode(address(l2TokenBeacon), "")); - bytes32 salt = _getCreate2Salt(_l1Token); - return - L2ContractHelper.computeCreate2Address(address(this), salt, l2TokenProxyBytecodeHash, constructorInputHash); - } - - /// @notice Converts the L1 token address to the create2 salt of deployed L2 token. - /// @param _l1Token The address of token on L1. - /// @return salt The salt used to compute address of wrapped token on L2 and for beacon proxy deployment. - function _getCreate2Salt(address _l1Token) internal pure returns (bytes32 salt) { - salt = bytes32(uint256(uint160(_l1Token))); - } -} diff --git a/l2-contracts/contracts/dev-contracts/DevL2SharedBridge.sol b/l2-contracts/contracts/dev-contracts/DevL2SharedBridge.sol deleted file mode 100644 index e93d5c987..000000000 --- a/l2-contracts/contracts/dev-contracts/DevL2SharedBridge.sol +++ /dev/null @@ -1,33 +0,0 @@ -// 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/l2-contracts/src/deploy-shared-bridge-on-l2-through-l1.ts b/l2-contracts/src/deploy-shared-bridge-on-l2-through-l1.ts deleted file mode 100644 index 433365064..000000000 --- a/l2-contracts/src/deploy-shared-bridge-on-l2-through-l1.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { Command } from "commander"; -import type { BigNumberish } from "ethers"; -import { Wallet, ethers } from "ethers"; -import { formatUnits, parseUnits } from "ethers/lib/utils"; -import { provider, publishBytecodeFromL1, priorityTxMaxGasLimit } from "./utils"; - -import { ethTestConfig } from "./deploy-utils"; - -import { Deployer } from "../../l1-contracts/src.ts/deploy"; -import { GAS_MULTIPLIER } from "../../l1-contracts/scripts/utils"; -import * as hre from "hardhat"; -import { - ADDRESS_ONE, - L2_ASSET_ROUTER_ADDRESS, - L2_BRIDGEHUB_ADDRESS, - L2_MESSAGE_ROOT_ADDRESS, - L2_NATIVE_TOKEN_VAULT_ADDRESS, -} from "../../l1-contracts/src.ts/utils"; - -import { L2NativeTokenVaultFactory } from "../typechain"; -import { BridgehubFactory } from "../../l1-contracts/typechain"; - -export const L2_SHARED_BRIDGE_ABI = hre.artifacts.readArtifactSync("L2SharedBridge").abi; -export const L2_STANDARD_TOKEN_PROXY_BYTECODE = hre.artifacts.readArtifactSync("BeaconProxy").bytecode; - -export async function publishL2NativeTokenVaultDependencyBytecodesOnL2( - deployer: Deployer, - chainId: string, - gasPrice: BigNumberish -) { - if (deployer.verbose) { - console.log("Providing necessary L2 bytecodes"); - } - - const L2_STANDARD_ERC20_PROXY_FACTORY_BYTECODE = hre.artifacts.readArtifactSync("UpgradeableBeacon").bytecode; - const L2_STANDARD_ERC20_IMPLEMENTATION_BYTECODE = hre.artifacts.readArtifactSync("L2StandardERC20").bytecode; - - const receipt = await ( - await publishBytecodeFromL1( - chainId, - deployer.deployWallet, - [ - L2_STANDARD_ERC20_PROXY_FACTORY_BYTECODE, - L2_STANDARD_ERC20_IMPLEMENTATION_BYTECODE, - L2_STANDARD_TOKEN_PROXY_BYTECODE, - ], - gasPrice - ) - ).wait(); - - if (deployer.verbose) { - console.log("Bytecodes published on L2, hash: ", receipt.transactionHash); - } -} - -async function setL2TokenBeacon(deployer: Deployer, chainId: string, gasPrice: BigNumberish) { - if (deployer.verbose) { - console.log("Setting L2 token beacon"); - } - const l2NTV = L2NativeTokenVaultFactory.connect(L2_NATIVE_TOKEN_VAULT_ADDRESS, deployer.deployWallet); - - const receipt = await deployer.executeUpgradeOnL2( - chainId, - L2_NATIVE_TOKEN_VAULT_ADDRESS, - gasPrice, - l2NTV.interface.encodeFunctionData("setL2TokenBeacon", [false, ethers.constants.AddressZero]), - priorityTxMaxGasLimit - ); - if (deployer.verbose) { - console.log("Set L2Token Beacon, upgrade hash", receipt.transactionHash); - } - const bridgehub = BridgehubFactory.connect(L2_BRIDGEHUB_ADDRESS, deployer.deployWallet); - const receipt2 = await deployer.executeUpgradeOnL2( - chainId, - L2_BRIDGEHUB_ADDRESS, - gasPrice, - bridgehub.interface.encodeFunctionData("setAddresses", [ - L2_ASSET_ROUTER_ADDRESS, - ADDRESS_ONE, - L2_MESSAGE_ROOT_ADDRESS, - ]), - priorityTxMaxGasLimit - ); - if (deployer.verbose) { - console.log("Set addresses in BH, upgrade hash", receipt2.transactionHash); - } -} - -export async function deploySharedBridgeOnL2ThroughL1(deployer: Deployer, chainId: string, gasPrice: BigNumberish) { - await publishL2NativeTokenVaultDependencyBytecodesOnL2(deployer, chainId, gasPrice); - await setL2TokenBeacon(deployer, chainId, gasPrice); - if (deployer.verbose) { - console.log(`CONTRACTS_L2_NATIVE_TOKEN_VAULT_IMPL_ADDR=${L2_NATIVE_TOKEN_VAULT_ADDRESS}`); - console.log(`CONTRACTS_L2_NATIVE_TOKEN_VAULT_PROXY_ADDR=${L2_NATIVE_TOKEN_VAULT_ADDRESS}`); - console.log(`CONTRACTS_L2_SHARED_BRIDGE_IMPL_ADDR=${L2_ASSET_ROUTER_ADDRESS}`); - console.log(`CONTRACTS_L2_SHARED_BRIDGE_ADDR=${L2_ASSET_ROUTER_ADDRESS}`); - } -} - -async function main() { - const program = new Command(); - - program.version("0.1.0").name("deploy-shared-bridge-on-l2-through-l1"); - - program - .option("--private-key ") - .option("--chain-id ") - .option("--local-legacy-bridge-testing") - .option("--gas-price ") - .option("--nonce ") - .option("--erc20-bridge ") - .option("--skip-initialize-chain-governance ") - .action(async (cmd) => { - const chainId: string = cmd.chainId ? cmd.chainId : process.env.CHAIN_ETH_ZKSYNC_NETWORK_ID; - const deployWallet = cmd.privateKey - ? new Wallet(cmd.privateKey, provider) - : Wallet.fromMnemonic( - process.env.MNEMONIC ? process.env.MNEMONIC : ethTestConfig.mnemonic, - "m/44'/60'/0'/0/1" - ).connect(provider); - console.log(`Using deployer wallet: ${deployWallet.address}`); - - const deployer = new Deployer({ - deployWallet, - ownerAddress: deployWallet.address, - verbose: true, - }); - - const nonce = cmd.nonce ? parseInt(cmd.nonce) : await deployer.deployWallet.getTransactionCount(); - console.log(`Using nonce: ${nonce}`); - - const gasPrice = cmd.gasPrice - ? parseUnits(cmd.gasPrice, "gwei") - : (await provider.getGasPrice()).mul(GAS_MULTIPLIER); - console.log(`Using gas price: ${formatUnits(gasPrice, "gwei")} gwei`); - - const skipInitializeChainGovernance = - !!cmd.skipInitializeChainGovernance && cmd.skipInitializeChainGovernance === "true"; - if (skipInitializeChainGovernance) { - console.log("Initialization of the chain governance will be skipped"); - } - - await deploySharedBridgeOnL2ThroughL1(deployer, chainId, gasPrice); - }); - - await program.parseAsync(process.argv); -} - -main() - .then(() => process.exit(0)) - .catch((err) => { - console.error("Error:", err); - process.exit(1); - }); diff --git a/l2-contracts/test/erc20.test.ts b/l2-contracts/test/erc20.test.ts deleted file mode 100644 index ec531f7aa..000000000 --- a/l2-contracts/test/erc20.test.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { Deployer } from "@matterlabs/hardhat-zksync-deploy"; -import { expect } from "chai"; -import { ethers } from "ethers"; -import * as hre from "hardhat"; -import { Provider, Wallet } from "zksync-ethers"; -import { hashBytecode } from "zksync-ethers/build/utils"; -import { unapplyL1ToL2Alias, setCode } from "./test-utils"; -import type { L2AssetRouter, L2NativeTokenVault, L2StandardERC20 } from "../typechain"; -import { L2AssetRouterFactory, L2NativeTokenVaultFactory, L2StandardERC20Factory } from "../typechain"; - -const richAccount = [ - { - address: "0x36615Cf349d7F6344891B1e7CA7C72883F5dc049", - privateKey: "0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110", - }, - { - address: "0xa61464658AfeAf65CccaaFD3a512b69A83B77618", - privateKey: "0xac1e735be8536c6534bb4f17f06f6afc73b2b5ba84ac2cfb12f7461b20c0bbe3", - }, - { - address: "0x0D43eB5B8a47bA8900d84AA36656c92024e9772e", - privateKey: "0xd293c684d884d56f8d6abd64fc76757d3664904e309a0645baf8522ab6366d9e", - }, - { - address: "0xA13c10C0D5bd6f79041B9835c63f91de35A15883", - privateKey: "0x850683b40d4a740aa6e745f889a6fdc8327be76e122f5aba645a5b02d0248db8", - }, -]; - -describe("ERC20Bridge", function () { - const provider = new Provider(hre.config.networks.localhost.url); - const deployerWallet = new Wallet(richAccount[0].privateKey, provider); - const governorWallet = new Wallet(richAccount[1].privateKey, provider); - const proxyAdminWallet = new Wallet(richAccount[3].privateKey, provider); - - // We need to emulate a L1->L2 transaction from the L1 bridge to L2 counterpart. - // It is a bit easier to use EOA and it is sufficient for the tests. - const l1BridgeWallet = new Wallet(richAccount[2].privateKey, provider); - - // We won't actually deploy an L1 token in these tests, but we need some address for it. - const L1_TOKEN_ADDRESS = "0x1111000000000000000000000000000000001111"; - const L2_ASSET_ROUTER_ADDRESS = "0x0000000000000000000000000000000000010003"; - const L2_NATIVE_TOKEN_VAULT_ADDRESS = "0x0000000000000000000000000000000000010004"; - - const testChainId = 9; - - let erc20Bridge: L2AssetRouter; - let erc20NativeTokenVault: L2NativeTokenVault; - let erc20Token: L2StandardERC20; - const contractsDeployedAlready: boolean = false; - - before("Deploy token and bridge", async function () { - const deployer = new Deployer(hre, deployerWallet); - - // While we formally don't need to deploy the token and the beacon proxy, it is a neat way to have the bytecode published - const l2TokenImplAddress = await deployer.deploy(await deployer.loadArtifact("L2StandardERC20")); - const l2Erc20TokenBeacon = await deployer.deploy(await deployer.loadArtifact("UpgradeableBeacon"), [ - l2TokenImplAddress.address, - ]); - await deployer.deploy(await deployer.loadArtifact("BeaconProxy"), [l2Erc20TokenBeacon.address, "0x"]); - const beaconProxyBytecodeHash = hashBytecode((await deployer.loadArtifact("BeaconProxy")).bytecode); - let constructorArgs = ethers.utils.defaultAbiCoder.encode( - ["uint256", "uint256", "address", "address"], - /// note in real deployment we have to transfer ownership of standard deployer here - [testChainId, 1, unapplyL1ToL2Alias(l1BridgeWallet.address), unapplyL1ToL2Alias(l1BridgeWallet.address)] - ); - await setCode( - deployerWallet, - L2_ASSET_ROUTER_ADDRESS, - (await deployer.loadArtifact("L2AssetRouter")).bytecode, - true, - constructorArgs - ); - - erc20Bridge = L2AssetRouterFactory.connect(L2_ASSET_ROUTER_ADDRESS, deployerWallet); - const l2NativeTokenVaultArtifact = await deployer.loadArtifact("L2NativeTokenVault"); - constructorArgs = ethers.utils.defaultAbiCoder.encode( - ["uint256", "bytes32", "address", "bool"], - /// note in real deployment we have to transfer ownership of standard deployer here - [1, beaconProxyBytecodeHash, governorWallet.address, contractsDeployedAlready] - ); - await setCode( - deployerWallet, - L2_NATIVE_TOKEN_VAULT_ADDRESS, - l2NativeTokenVaultArtifact.bytecode, - true, - constructorArgs - ); - - erc20NativeTokenVault = L2NativeTokenVaultFactory.connect(L2_NATIVE_TOKEN_VAULT_ADDRESS, l1BridgeWallet); - const governorNTV = L2NativeTokenVaultFactory.connect(L2_NATIVE_TOKEN_VAULT_ADDRESS, governorWallet); - await governorNTV.configureL2TokenBeacon(false, ethers.constants.AddressZero); - }); - - it("Should finalize deposit ERC20 deposit", async function () { - const erc20BridgeWithL1BridgeWallet = L2AssetRouterFactory.connect(erc20Bridge.address, proxyAdminWallet); - const l1Depositor = ethers.Wallet.createRandom(); - const l2Receiver = ethers.Wallet.createRandom(); - const l1Bridge = await hre.ethers.getImpersonatedSigner(l1BridgeWallet.address); - const tx = await ( - await erc20BridgeWithL1BridgeWallet.connect(l1Bridge)["finalizeDeposit(address,address,address,uint256,bytes)"]( - // Depositor and l2Receiver can be any here - l1Depositor.address, - l2Receiver.address, - L1_TOKEN_ADDRESS, - 100, - encodedTokenData("TestToken", "TT", 18) - ) - ).wait(); - const l2TokenInfo = tx.events.find((event) => event.event === "FinalizeDepositSharedBridge").args.assetId; - const l2TokenAddress = await erc20NativeTokenVault.tokenAddress(l2TokenInfo); - // Checking the correctness of the balance: - erc20Token = L2StandardERC20Factory.connect(l2TokenAddress, deployerWallet); - expect(await erc20Token.balanceOf(l2Receiver.address)).to.equal(100); - expect(await erc20Token.totalSupply()).to.equal(100); - expect(await erc20Token.name()).to.equal("TestToken"); - expect(await erc20Token.symbol()).to.equal("TT"); - expect(await erc20Token.decimals()).to.equal(18); - }); - - it("Governance should be able to reinitialize the token", async () => { - const erc20TokenWithGovernor = L2StandardERC20Factory.connect(erc20Token.address, governorWallet); - - await ( - await erc20TokenWithGovernor.reinitializeToken( - { - ignoreName: false, - ignoreSymbol: false, - ignoreDecimals: false, - }, - "TestTokenNewName", - "TTN", - 2 - ) - ).wait(); - - expect(await erc20Token.name()).to.equal("TestTokenNewName"); - expect(await erc20Token.symbol()).to.equal("TTN"); - // The decimals should stay the same - expect(await erc20Token.decimals()).to.equal(18); - }); - - it("Governance should not be able to skip initializer versions", async () => { - const erc20TokenWithGovernor = L2StandardERC20Factory.connect(erc20Token.address, governorWallet); - - await expect( - erc20TokenWithGovernor.reinitializeToken( - { - ignoreName: false, - ignoreSymbol: false, - ignoreDecimals: false, - }, - "TestTokenNewName", - "TTN", - 20, - { gasLimit: 10000000 } - ) - ).to.be.reverted; - }); -}); - -function encodedTokenData(name: string, symbol: string, decimals: number) { - const abiCoder = ethers.utils.defaultAbiCoder; - const encodedName = abiCoder.encode(["string"], [name]); - const encodedSymbol = abiCoder.encode(["string"], [symbol]); - const encodedDecimals = abiCoder.encode(["uint8"], [decimals]); - - return abiCoder.encode(["bytes", "bytes", "bytes"], [encodedName, encodedSymbol, encodedDecimals]); -}