From 65c5b4ea62758afd5f86aaeb68260aad4c0d5012 Mon Sep 17 00:00:00 2001 From: Morty <70688412+yiweichi@users.noreply.github.com> Date: Wed, 21 Aug 2024 01:15:04 +0800 Subject: [PATCH] feat: alternative gas token (#20) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: zimpha Co-authored-by: Péter Garamvölgyi --- docker/config-example.toml | 4 + docker/scripts/deploy.sh | 5 - docker/scripts/gen-configs.sh | 19 +- docker/templates/bridge-history-config.json | 4 +- docker/templates/chain-monitor-config.json | 8 +- docker/templates/config-contracts.toml | 4 + docker/templates/rollup-config.json | 2 +- scripts/deterministic/Configuration.sol | 4 + scripts/deterministic/DeployScroll.s.sol | 284 +++++++++- scripts/deterministic/GenerateConfigs.s.sol | 8 + src/L1/L1ScrollMessenger.sol | 172 ++++--- src/L1/gateways/L1ETHGateway.sol | 2 +- src/alternative-gas-token/GasTokenExample.sol | 24 + .../L1GasTokenGateway.sol | 186 +++++++ .../L1ScrollMessengerNonETH.sol | 243 +++++++++ .../L1WrappedTokenGateway.sol | 100 ++++ .../AlternativeGasTokenTestBase.t.sol | 272 ++++++++++ .../GasTokenDecimalGateway.t.sol | 481 +++++++++++++++++ .../L1ScrollMessengerNonETH.t.sol | 486 ++++++++++++++++++ .../L1WrappedTokenGateway.t.sol | 106 ++++ 20 files changed, 2300 insertions(+), 114 deletions(-) create mode 100644 src/alternative-gas-token/GasTokenExample.sol create mode 100644 src/alternative-gas-token/L1GasTokenGateway.sol create mode 100644 src/alternative-gas-token/L1ScrollMessengerNonETH.sol create mode 100644 src/alternative-gas-token/L1WrappedTokenGateway.sol create mode 100644 src/test/alternative-gas-token/AlternativeGasTokenTestBase.t.sol create mode 100644 src/test/alternative-gas-token/GasTokenDecimalGateway.t.sol create mode 100644 src/test/alternative-gas-token/L1ScrollMessengerNonETH.t.sol create mode 100644 src/test/alternative-gas-token/L1WrappedTokenGateway.t.sol diff --git a/docker/config-example.toml b/docker/config-example.toml index 0d57e66..09df81d 100644 --- a/docker/config-example.toml +++ b/docker/config-example.toml @@ -13,6 +13,9 @@ MAX_L1_MESSAGE_GAS_LIMIT = 10000000 L1_CONTRACT_DEPLOYMENT_BLOCK = 0 +ALTERNATIVE_GAS_TOKEN_ENABLED = true +# EXAMPLE_GAS_TOKEN_DECIMAL = 6 + TEST_ENV_MOCK_FINALIZE_ENABLED = true TEST_ENV_MOCK_FINALIZE_TIMEOUT_SEC = 3600 @@ -64,6 +67,7 @@ L1_PLONK_VERIFIER_ADDR = "0x0000000000000000000000000000000000000001" [contracts.overrides] # L1_WETH = "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14" +# L1_GAS_TOKEN = "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" L2_MESSAGE_QUEUE = "0x5300000000000000000000000000000000000000" L1_GAS_PRICE_ORACLE = "0x5300000000000000000000000000000000000002" diff --git a/docker/scripts/deploy.sh b/docker/scripts/deploy.sh index 6441b2b..286dcd1 100755 --- a/docker/scripts/deploy.sh +++ b/docker/scripts/deploy.sh @@ -13,11 +13,6 @@ if [ "$L2_RPC_ENDPOINT" = "" ]; then L2_RPC_ENDPOINT="http://host.docker.internal:8545" fi -if [ "${L1_RPC_ENDPOINT}" = "" ]; then - echo "L1_RPC_ENDPOINT is not set" - L1_RPC_ENDPOINT="http://host.docker.internal:8543" -fi - if [ "${BATCH_SIZE}" = "" ]; then BATCH_SIZE="100" fi diff --git a/docker/scripts/gen-configs.sh b/docker/scripts/gen-configs.sh index 0a42c78..43167a6 100755 --- a/docker/scripts/gen-configs.sh +++ b/docker/scripts/gen-configs.sh @@ -1,8 +1,25 @@ #!/bin/bash +# the deployment of the L1GasTokenGateway implementation necessitates fetching the gas token decimal +# in this case it requires the context of layer 1 +gen_config_contracts_toml() { + config_file="./volume/config.toml" + gas_token_addr=$(grep -E "^L1_GAS_TOKEN =" "$config_file" | sed 's/ *= */=/' | cut -d'=' -f2-) + gas_token_enabled=$(grep -E "^ALTERNATIVE_GAS_TOKEN_ENABLED =" "$config_file" | sed 's/ *= */=/' | cut -d'=' -f2-) + l1_rpc_url=$(grep -E "^L1_RPC_ENDPOINT =" "$config_file" | sed 's/ *= */=/' | cut -d'=' -f2- | sed 's/"//g') + + if [[ "$gas_token_enabled" == "true" && "$gas_token_addr" != "" && "$gas_token_addr" != "0x0000000000000000000000000000000000000000" ]]; then + echo "gas token enabled and address provided" + forge script scripts/deterministic/DeployScroll.s.sol:DeployScroll --rpc-url "$l1_rpc_url" --sig "run(string,string)" "none" "write-config" || exit 1 + else + echo "gas token disabled or address not provided" + forge script scripts/deterministic/DeployScroll.s.sol:DeployScroll --sig "run(string,string)" "none" "write-config" || exit 1 + fi +} + echo "" echo "generating config-contracts.toml" -forge script scripts/deterministic/DeployScroll.s.sol:DeployScroll --sig "run(string,string)" "none" "write-config" || exit 1 +gen_config_contracts_toml echo "" echo "generating genesis.json" diff --git a/docker/templates/bridge-history-config.json b/docker/templates/bridge-history-config.json index 5ae1a9e..f625a31 100644 --- a/docker/templates/bridge-history-config.json +++ b/docker/templates/bridge-history-config.json @@ -18,7 +18,9 @@ "USDCGatewayAddr": "0x0000000000000000000000000000000000000000", "LIDOGatewayAddr": "0x0000000000000000000000000000000000000000", "DAIGatewayAddr": "0x0000000000000000000000000000000000000000", - "PufferGatewayAddr": "0x0000000000000000000000000000000000000000" + "PufferGatewayAddr": "0x0000000000000000000000000000000000000000", + "GasTokenGatewayAddr": null, + "WrappedTokenGatewayAddr": null }, "L2": { "confirmation": 0, diff --git a/docker/templates/chain-monitor-config.json b/docker/templates/chain-monitor-config.json index 09fb89b..1bc7fba 100644 --- a/docker/templates/chain-monitor-config.json +++ b/docker/templates/chain-monitor-config.json @@ -14,13 +14,15 @@ "dai_gateway": "0x0000000000000000000000000000000000000000", "usdc_gateway": "0x0000000000000000000000000000000000000000", "lido_gateway": "0x0000000000000000000000000000000000000000", - "puffer_gateway": "0x0000000000000000000000000000000000000000" + "puffer_gateway": "0x0000000000000000000000000000000000000000", + "gas_token_gateway": null }, "scroll_messenger": null, "message_queue": null, - "scroll_chain": null + "scroll_chain": null, + "gas_token": null }, - "start_messenger_balance": null + "start_messenger_balance": 0 }, "l2_config": { "l2_url": null, diff --git a/docker/templates/config-contracts.toml b/docker/templates/config-contracts.toml index b0681a0..998003c 100644 --- a/docker/templates/config-contracts.toml +++ b/docker/templates/config-contracts.toml @@ -23,6 +23,10 @@ L1_ERC721_GATEWAY_PROXY_ADDR = "" L1_ERC1155_GATEWAY_PROXY_ADDR = "" L2_MESSAGE_QUEUE_ADDR = "" L1_GAS_PRICE_ORACLE_ADDR = "" +L1_GAS_TOKEN_ADDR = "" +L1_GAS_TOKEN_GATEWAY_IMPLEMENTATION_ADDR = "" +L1_GAS_TOKEN_GATEWAY_PROXY_ADDR = "" +L1_WRAPPED_TOKEN_GATEWAY_ADDR = "" L2_WHITELIST_ADDR = "" L2_WETH_ADDR = "" L2_TX_FEE_VAULT_ADDR = "" diff --git a/docker/templates/rollup-config.json b/docker/templates/rollup-config.json index 54ee273..9c093bb 100644 --- a/docker/templates/rollup-config.json +++ b/docker/templates/rollup-config.json @@ -49,7 +49,7 @@ "gas_price_diff": 50000 }, "chain_monitor": { - "enabled": false, + "enabled": true, "timeout": 3, "try_times": 5, "base_url": "http://chain-monitor:8080" diff --git a/scripts/deterministic/Configuration.sol b/scripts/deterministic/Configuration.sol index 8d7fbe5..ab72a7e 100644 --- a/scripts/deterministic/Configuration.sol +++ b/scripts/deterministic/Configuration.sol @@ -36,6 +36,8 @@ abstract contract Configuration is Script { uint256 internal L1_CONTRACT_DEPLOYMENT_BLOCK; + bool internal ALTERNATIVE_GAS_TOKEN_ENABLED; + bool internal TEST_ENV_MOCK_FINALIZE_ENABLED; uint256 internal TEST_ENV_MOCK_FINALIZE_TIMEOUT_SEC; @@ -110,6 +112,8 @@ abstract contract Configuration is Script { L1_CONTRACT_DEPLOYMENT_BLOCK = cfg.readUint(".general.L1_CONTRACT_DEPLOYMENT_BLOCK"); + ALTERNATIVE_GAS_TOKEN_ENABLED = cfg.readBool(".general.ALTERNATIVE_GAS_TOKEN_ENABLED"); + TEST_ENV_MOCK_FINALIZE_ENABLED = cfg.readBool(".general.TEST_ENV_MOCK_FINALIZE_ENABLED"); TEST_ENV_MOCK_FINALIZE_TIMEOUT_SEC = cfg.readUint(".general.TEST_ENV_MOCK_FINALIZE_TIMEOUT_SEC"); diff --git a/scripts/deterministic/DeployScroll.s.sol b/scripts/deterministic/DeployScroll.s.sol index 528cb7f..fac74df 100644 --- a/scripts/deterministic/DeployScroll.s.sol +++ b/scripts/deterministic/DeployScroll.s.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity =0.8.24; +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; import {TransparentUpgradeableProxy, ITransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; @@ -21,6 +22,10 @@ import {L2GasPriceOracle} from "../../src/L1/rollup/L2GasPriceOracle.sol"; import {MultipleVersionRollupVerifier} from "../../src/L1/rollup/MultipleVersionRollupVerifier.sol"; import {ScrollChain} from "../../src/L1/rollup/ScrollChain.sol"; import {ZkEvmVerifierV1} from "../../src/libraries/verifier/ZkEvmVerifierV1.sol"; +import {GasTokenExample} from "../../src/alternative-gas-token/GasTokenExample.sol"; +import {L1ScrollMessengerNonETH} from "../../src/alternative-gas-token/L1ScrollMessengerNonETH.sol"; +import {L1GasTokenGateway} from "../../src/alternative-gas-token/L1GasTokenGateway.sol"; +import {L1WrappedTokenGateway} from "../../src/alternative-gas-token/L1WrappedTokenGateway.sol"; import {L2CustomERC20Gateway} from "../../src/L2/gateways/L2CustomERC20Gateway.sol"; import {L2ERC1155Gateway} from "../../src/L2/gateways/L2ERC1155Gateway.sol"; @@ -76,6 +81,8 @@ contract ScrollStandardERC20FactorySetOwner is ScrollStandardERC20Factory { } contract DeployScroll is DeterminsticDeployment { + using stdToml for string; + /********* * Types * *********/ @@ -128,6 +135,10 @@ contract DeployScroll is DeterminsticDeployment { address internal L1_ZKEVM_VERIFIER_V1_ADDR; address internal L2_GAS_PRICE_ORACLE_IMPLEMENTATION_ADDR; address internal L2_GAS_PRICE_ORACLE_PROXY_ADDR; + address internal L1_GAS_TOKEN_ADDR; + address internal L1_GAS_TOKEN_GATEWAY_IMPLEMENTATION_ADDR; + address internal L1_GAS_TOKEN_GATEWAY_PROXY_ADDR; + address internal L1_WRAPPED_TOKEN_GATEWAY_ADDR; // L2 addresses address internal L1_GAS_PRICE_ORACLE_ADDR; @@ -186,6 +197,14 @@ contract DeployScroll is DeterminsticDeployment { _; } + /// @dev Only execute block if it's requied by alternative gas token mode. + modifier gasToken(bool gasTokenRequire) { + if (ALTERNATIVE_GAS_TOKEN_ENABLED != gasTokenRequire) { + return; + } + _; + } + /*************** * Entry point * ***************/ @@ -223,23 +242,88 @@ contract DeployScroll is DeterminsticDeployment { } } - function checkDeployerBalance() private view { + function checkDeployerBalance() private { // ignore balance during simulation if (broadcastLayer == Layer.None) { return; } + // check funds for deployment (L1 & L2) if (DEPLOYER_ADDR.balance < MINIMUM_DEPLOYER_BALANCE) { revert( string( abi.encodePacked( - "[ERROR] insufficient funds on deployer account (", + "[ERROR] insufficient funds on deployer account for contract deployment (", vm.toString(DEPLOYER_ADDR), - ")" + ") minimum ETH balance (in wei): ", + vm.toString(MINIMUM_DEPLOYER_BALANCE) ) ) ); } + + // check funds for initial deposit (L1, ETH as gas token) + if (broadcastLayer == Layer.L1 && !ALTERNATIVE_GAS_TOKEN_ENABLED) { + uint256 l1MessengerBalance = address(L1_SCROLL_MESSENGER_PROXY_ADDR).balance; + uint256 amountToLock = L2_DEPLOYER_INITIAL_BALANCE; + + uint256 amountToSend = 0; + if (l1MessengerBalance < amountToLock) { + amountToSend = amountToLock - l1MessengerBalance; + } + + uint256 minBalance = MINIMUM_DEPLOYER_BALANCE + amountToSend; + + if (DEPLOYER_ADDR.balance < minBalance) { + revert( + string( + abi.encodePacked( + "[ERROR] insufficient funds on deployer account for initial deposit (", + vm.toString(DEPLOYER_ADDR), + ") minimum ETH balance (in wei): ", + vm.toString(minBalance) + ) + ) + ); + } + } + + // check funds for initial deposit (L1, alternative gas token) + // skip it if L1_GAS_TOKEN is not configured in the config file + address gasTokenAddr = tryGetOverride("L1_GAS_TOKEN"); + if (broadcastLayer == Layer.L1 && ALTERNATIVE_GAS_TOKEN_ENABLED && gasTokenAddr != address(0)) { + uint256 l1GasTokenGatewayBalance = IERC20Metadata(L1_GAS_TOKEN_ADDR).balanceOf( + L1_GAS_TOKEN_GATEWAY_PROXY_ADDR + ); + + uint256 scale = 10**(18 - IERC20Metadata(L1_GAS_TOKEN_ADDR).decimals()); + uint256 amountToLock = L2_DEPLOYER_INITIAL_BALANCE / scale; + if (L2_DEPLOYER_INITIAL_BALANCE % scale != 0) { + amountToLock += 1; + } + + uint256 amountToSend = 0; + if (l1GasTokenGatewayBalance < amountToLock) { + amountToSend = amountToLock - l1GasTokenGatewayBalance; + } + + uint256 minBalance = amountToSend; + + if (IERC20Metadata(L1_GAS_TOKEN_ADDR).balanceOf(DEPLOYER_ADDR) < minBalance) { + revert( + string( + abi.encodePacked( + "[ERROR] insufficient funds on deployer account for initial deposit (", + vm.toString(DEPLOYER_ADDR), + ") minimum ", + IERC20Metadata(L1_GAS_TOKEN_ADDR).symbol(), + " balance (in min token unit): ", + vm.toString(minBalance) + ) + ) + ); + } + } } function deployAllContracts() private { @@ -270,6 +354,10 @@ contract DeployScroll is DeterminsticDeployment { deployL1CustomERC20GatewayProxy(); deployL1ERC721GatewayProxy(); deployL1ERC1155GatewayProxy(); + + // alternative gas token contracts + deployGasToken(); + deployL1GasTokenGatewayProxy(); } // @notice deployL2Contracts1stPass deploys L2 contracts whose initialization does not depend on any L1 addresses. @@ -300,6 +388,10 @@ contract DeployScroll is DeterminsticDeployment { deployL1CustomERC20Gateway(); deployL1ERC721Gateway(); deployL1ERC1155Gateway(); + + // alternative gas token contracts + deployL1GasTokenGateway(); + deployL1WrappedTokenGateway(); } // @notice deployL2Contracts2ndPass deploys L2 contracts whose initialization depends on some L1 addresses. @@ -331,6 +423,14 @@ contract DeployScroll is DeterminsticDeployment { initializeL1WETHGateway(); initializeL1Whitelist(); + // alternative gas token contracts + initializeL1GasTokenGateway(); + + // lock tokens on L1 to ensure bridge parity, + // we lock ETH in L1ScrollMessenger or GAS_TOKEN in L1GasTokenGateway + // note: this can only be done before transferring ownership + lockTokensOnL1(); + transferL1ContractOwnership(); } @@ -527,7 +627,7 @@ contract DeployScroll is DeterminsticDeployment { ); } - function deployL1ETHGatewayProxy() private { + function deployL1ETHGatewayProxy() private gasToken(false) { bytes memory args = abi.encode( notnull(L1_PROXY_IMPLEMENTATION_PLACEHOLDER_ADDR), notnull(L1_PROXY_ADMIN_ADDR), @@ -541,7 +641,7 @@ contract DeployScroll is DeterminsticDeployment { ); } - function deployL1WETHGatewayProxy() private { + function deployL1WETHGatewayProxy() private gasToken(false) { bytes memory args = abi.encode( notnull(L1_PROXY_IMPLEMENTATION_PLACEHOLDER_ADDR), notnull(L1_PROXY_ADMIN_ADDR), @@ -611,6 +711,38 @@ contract DeployScroll is DeterminsticDeployment { ); } + function deployGasToken() private gasToken(true) { + uint8 decimal = 18; + string memory key = ".general.EXAMPLE_GAS_TOKEN_DECIMAL"; + if (vm.keyExistsToml(cfg, key)) { + decimal = uint8(cfg.readUint(key)); + } + + bytes memory args = abi.encode( + "ScrollGasToken", // _name + "GasToken", // _symbol + decimal, // _decimals + DEPLOYER_ADDR, // _recipient + 10**28 // _amount + ); + + L1_GAS_TOKEN_ADDR = deploy("L1_GAS_TOKEN", type(GasTokenExample).creationCode, args); + } + + function deployL1GasTokenGatewayProxy() private gasToken(true) { + bytes memory args = abi.encode( + notnull(L1_PROXY_IMPLEMENTATION_PLACEHOLDER_ADDR), + notnull(L1_PROXY_ADMIN_ADDR), + new bytes(0) + ); + + L1_GAS_TOKEN_GATEWAY_PROXY_ADDR = deploy( + "L1_GAS_TOKEN_GATEWAY_PROXY", + type(TransparentUpgradeableProxy).creationCode, + args + ); + } + /*************************** * L2: 1st pass deployment * ***************************/ @@ -693,7 +825,7 @@ contract DeployScroll is DeterminsticDeployment { ); } - function deployL2WETHGatewayProxy() private { + function deployL2WETHGatewayProxy() private gasToken(false) { bytes memory args = abi.encode( notnull(L2_PROXY_IMPLEMENTATION_PLACEHOLDER_ADDR), notnull(L2_PROXY_ADMIN_ADDR), @@ -765,22 +897,37 @@ contract DeployScroll is DeterminsticDeployment { ***************************/ function deployL1ScrollMessenger() private { - bytes memory args = abi.encode( - notnull(L2_SCROLL_MESSENGER_PROXY_ADDR), - notnull(L1_SCROLL_CHAIN_PROXY_ADDR), - notnull(L1_MESSAGE_QUEUE_PROXY_ADDR) - ); + if (ALTERNATIVE_GAS_TOKEN_ENABLED) { + bytes memory args = abi.encode( + notnull(L1_GAS_TOKEN_GATEWAY_PROXY_ADDR), + notnull(L2_SCROLL_MESSENGER_PROXY_ADDR), + notnull(L1_SCROLL_CHAIN_PROXY_ADDR), + notnull(L1_MESSAGE_QUEUE_PROXY_ADDR) + ); - L1_SCROLL_MESSENGER_IMPLEMENTATION_ADDR = deploy( - "L1_SCROLL_MESSENGER_IMPLEMENTATION", - type(L1ScrollMessenger).creationCode, - args - ); + L1_SCROLL_MESSENGER_IMPLEMENTATION_ADDR = deploy( + "L1_SCROLL_MESSENGER_IMPLEMENTATION", + type(L1ScrollMessengerNonETH).creationCode, + args + ); + } else { + bytes memory args = abi.encode( + notnull(L2_SCROLL_MESSENGER_PROXY_ADDR), + notnull(L1_SCROLL_CHAIN_PROXY_ADDR), + notnull(L1_MESSAGE_QUEUE_PROXY_ADDR) + ); + + L1_SCROLL_MESSENGER_IMPLEMENTATION_ADDR = deploy( + "L1_SCROLL_MESSENGER_IMPLEMENTATION", + type(L1ScrollMessenger).creationCode, + args + ); + } upgrade(L1_PROXY_ADMIN_ADDR, L1_SCROLL_MESSENGER_PROXY_ADDR, L1_SCROLL_MESSENGER_IMPLEMENTATION_ADDR); } - function deployL1ETHGateway() private { + function deployL1ETHGateway() private gasToken(false) { bytes memory args = abi.encode( notnull(L2_ETH_GATEWAY_PROXY_ADDR), notnull(L1_GATEWAY_ROUTER_PROXY_ADDR), @@ -796,7 +943,7 @@ contract DeployScroll is DeterminsticDeployment { upgrade(L1_PROXY_ADMIN_ADDR, L1_ETH_GATEWAY_PROXY_ADDR, L1_ETH_GATEWAY_IMPLEMENTATION_ADDR); } - function deployL1WETHGateway() private { + function deployL1WETHGateway() private gasToken(false) { bytes memory args = abi.encode( notnull(L1_WETH_ADDR), notnull(L2_WETH_ADDR), @@ -876,6 +1023,33 @@ contract DeployScroll is DeterminsticDeployment { upgrade(L1_PROXY_ADMIN_ADDR, L1_ERC1155_GATEWAY_PROXY_ADDR, L1_ERC1155_GATEWAY_IMPLEMENTATION_ADDR); } + function deployL1GasTokenGateway() private gasToken(true) { + bytes memory args = abi.encode( + notnull(L1_GAS_TOKEN_ADDR), + notnull(L2_ETH_GATEWAY_PROXY_ADDR), + notnull(L1_GATEWAY_ROUTER_PROXY_ADDR), + notnull(L1_SCROLL_MESSENGER_PROXY_ADDR) + ); + + L1_GAS_TOKEN_GATEWAY_IMPLEMENTATION_ADDR = deploy( + "L1_GAS_TOKEN_GATEWAY_IMPLEMENTATION", + type(L1GasTokenGateway).creationCode, + args + ); + + upgrade(L1_PROXY_ADMIN_ADDR, L1_GAS_TOKEN_GATEWAY_PROXY_ADDR, L1_GAS_TOKEN_GATEWAY_IMPLEMENTATION_ADDR); + } + + function deployL1WrappedTokenGateway() private gasToken(true) { + bytes memory args = abi.encode(notnull(L1_WETH_ADDR), notnull(L1_STANDARD_ERC20_GATEWAY_PROXY_ADDR)); + + L1_WRAPPED_TOKEN_GATEWAY_ADDR = deploy( + "L1_WRAPPED_TOKEN_GATEWAY", + type(L1WrappedTokenGateway).creationCode, + args + ); + } + /*************************** * L2: 2nd pass deployment * ***************************/ @@ -933,8 +1107,14 @@ contract DeployScroll is DeterminsticDeployment { } function deployL2ETHGateway() private { + address COUNTERPART; + if (ALTERNATIVE_GAS_TOKEN_ENABLED) { + COUNTERPART = L1_GAS_TOKEN_GATEWAY_PROXY_ADDR; + } else { + COUNTERPART = L1_ETH_GATEWAY_PROXY_ADDR; + } bytes memory args = abi.encode( - notnull(L1_ETH_GATEWAY_PROXY_ADDR), + notnull(COUNTERPART), notnull(L2_GATEWAY_ROUTER_PROXY_ADDR), notnull(L2_SCROLL_MESSENGER_PROXY_ADDR) ); @@ -948,7 +1128,7 @@ contract DeployScroll is DeterminsticDeployment { upgrade(L2_PROXY_ADMIN_ADDR, L2_ETH_GATEWAY_PROXY_ADDR, L2_ETH_GATEWAY_IMPLEMENTATION_ADDR); } - function deployL2WETHGateway() private { + function deployL2WETHGateway() private gasToken(false) { bytes memory args = abi.encode( notnull(L2_WETH_ADDR), notnull(L1_WETH_ADDR), @@ -1084,9 +1264,15 @@ contract DeployScroll is DeterminsticDeployment { } function initializeL1GatewayRouter() private { + address L2_ETH_GATEWAY_COUNTERPART; + if (ALTERNATIVE_GAS_TOKEN_ENABLED) { + L2_ETH_GATEWAY_COUNTERPART = L1_GAS_TOKEN_GATEWAY_PROXY_ADDR; + } else { + L2_ETH_GATEWAY_COUNTERPART = L1_ETH_GATEWAY_PROXY_ADDR; + } if (getInitializeCount(L1_GATEWAY_ROUTER_PROXY_ADDR) == 0) { L1GatewayRouter(L1_GATEWAY_ROUTER_PROXY_ADDR).initialize( - notnull(L1_ETH_GATEWAY_PROXY_ADDR), + notnull(L2_ETH_GATEWAY_COUNTERPART), notnull(L1_STANDARD_ERC20_GATEWAY_PROXY_ADDR) ); } @@ -1120,7 +1306,7 @@ contract DeployScroll is DeterminsticDeployment { } } - function initializeL1ETHGateway() private { + function initializeL1ETHGateway() private gasToken(false) { if (getInitializeCount(L1_ETH_GATEWAY_PROXY_ADDR) == 0) { L1ETHGateway(L1_ETH_GATEWAY_PROXY_ADDR).initialize( notnull(L2_ETH_GATEWAY_PROXY_ADDR), @@ -1142,7 +1328,7 @@ contract DeployScroll is DeterminsticDeployment { } } - function initializeL1WETHGateway() private { + function initializeL1WETHGateway() private gasToken(false) { if (getInitializeCount(L1_WETH_GATEWAY_PROXY_ADDR) == 0) { L1WETHGateway(payable(L1_WETH_GATEWAY_PROXY_ADDR)).initialize( notnull(L2_WETH_GATEWAY_PROXY_ADDR), @@ -1171,6 +1357,39 @@ contract DeployScroll is DeterminsticDeployment { } } + function initializeL1GasTokenGateway() private gasToken(true) { + if (getInitializeCount(L1_GAS_TOKEN_GATEWAY_PROXY_ADDR) == 0) { + L1GasTokenGateway(payable(L1_GAS_TOKEN_GATEWAY_PROXY_ADDR)).initialize(); + } + } + + function lockTokensOnL1() private { + if (!ALTERNATIVE_GAS_TOKEN_ENABLED) { + uint256 l1MessengerBalance = address(L1_SCROLL_MESSENGER_PROXY_ADDR).balance; + uint256 amountToLock = L2_DEPLOYER_INITIAL_BALANCE; + + if (l1MessengerBalance < amountToLock) { + uint256 amountToSend = amountToLock - l1MessengerBalance; + payable(L1_SCROLL_MESSENGER_PROXY_ADDR).transfer(amountToSend); + } + } else { + uint256 l1GasTokenGatewayBalance = IERC20Metadata(L1_GAS_TOKEN_ADDR).balanceOf( + L1_GAS_TOKEN_GATEWAY_PROXY_ADDR + ); + + uint256 scale = 10**(18 - IERC20Metadata(L1_GAS_TOKEN_ADDR).decimals()); + uint256 amountToLock = L2_DEPLOYER_INITIAL_BALANCE / scale; + if (L2_DEPLOYER_INITIAL_BALANCE % scale != 0) { + amountToLock += 1; + } + + if (l1GasTokenGatewayBalance < amountToLock) { + uint256 amountTosend = amountToLock - l1GasTokenGatewayBalance; + IERC20Metadata(L1_GAS_TOKEN_ADDR).transfer(L1_GAS_TOKEN_GATEWAY_PROXY_ADDR, amountTosend); + } + } + } + function transferL1ContractOwnership() private { if (Ownable(L1_ENFORCED_TX_GATEWAY_PROXY_ADDR).owner() != OWNER_ADDR) { Ownable(L1_ENFORCED_TX_GATEWAY_PROXY_ADDR).transferOwnership(OWNER_ADDR); @@ -1184,7 +1403,7 @@ contract DeployScroll is DeterminsticDeployment { if (Ownable(L1_ERC721_GATEWAY_PROXY_ADDR).owner() != OWNER_ADDR) { Ownable(L1_ERC721_GATEWAY_PROXY_ADDR).transferOwnership(OWNER_ADDR); } - if (Ownable(L1_ETH_GATEWAY_PROXY_ADDR).owner() != OWNER_ADDR) { + if (!ALTERNATIVE_GAS_TOKEN_ENABLED && Ownable(L1_ETH_GATEWAY_PROXY_ADDR).owner() != OWNER_ADDR) { Ownable(L1_ETH_GATEWAY_PROXY_ADDR).transferOwnership(OWNER_ADDR); } if (Ownable(L1_GATEWAY_ROUTER_PROXY_ADDR).owner() != OWNER_ADDR) { @@ -1199,7 +1418,7 @@ contract DeployScroll is DeterminsticDeployment { if (Ownable(L1_STANDARD_ERC20_GATEWAY_PROXY_ADDR).owner() != OWNER_ADDR) { Ownable(L1_STANDARD_ERC20_GATEWAY_PROXY_ADDR).transferOwnership(OWNER_ADDR); } - if (Ownable(L1_WETH_GATEWAY_PROXY_ADDR).owner() != OWNER_ADDR) { + if (!ALTERNATIVE_GAS_TOKEN_ENABLED && Ownable(L1_WETH_GATEWAY_PROXY_ADDR).owner() != OWNER_ADDR) { Ownable(L1_WETH_GATEWAY_PROXY_ADDR).transferOwnership(OWNER_ADDR); } if (Ownable(L2_GAS_PRICE_ORACLE_PROXY_ADDR).owner() != OWNER_ADDR) { @@ -1217,6 +1436,9 @@ contract DeployScroll is DeterminsticDeployment { if (Ownable(L1_WHITELIST_ADDR).owner() != OWNER_ADDR) { Ownable(L1_WHITELIST_ADDR).transferOwnership(OWNER_ADDR); } + if (ALTERNATIVE_GAS_TOKEN_ENABLED && Ownable(L1_GAS_TOKEN_GATEWAY_PROXY_ADDR).owner() != OWNER_ADDR) { + Ownable(L1_GAS_TOKEN_GATEWAY_PROXY_ADDR).transferOwnership(OWNER_ADDR); + } } /********************** @@ -1287,9 +1509,15 @@ contract DeployScroll is DeterminsticDeployment { } function initializeL2ETHGateway() private { + address COUNTERPART; + if (ALTERNATIVE_GAS_TOKEN_ENABLED) { + COUNTERPART = L1_GAS_TOKEN_GATEWAY_PROXY_ADDR; + } else { + COUNTERPART = L1_ETH_GATEWAY_PROXY_ADDR; + } if (getInitializeCount(L2_ETH_GATEWAY_PROXY_ADDR) == 0) { L2ETHGateway(L2_ETH_GATEWAY_PROXY_ADDR).initialize( - notnull(L1_ETH_GATEWAY_PROXY_ADDR), + notnull(COUNTERPART), notnull(L2_GATEWAY_ROUTER_PROXY_ADDR), notnull(L2_SCROLL_MESSENGER_PROXY_ADDR) ); @@ -1307,7 +1535,7 @@ contract DeployScroll is DeterminsticDeployment { } } - function initializeL2WETHGateway() private { + function initializeL2WETHGateway() private gasToken(false) { if (getInitializeCount(L2_WETH_GATEWAY_PROXY_ADDR) == 0) { L2WETHGateway(payable(L2_WETH_GATEWAY_PROXY_ADDR)).initialize( notnull(L1_WETH_GATEWAY_PROXY_ADDR), @@ -1378,7 +1606,7 @@ contract DeployScroll is DeterminsticDeployment { if (Ownable(L2_TX_FEE_VAULT_ADDR).owner() != OWNER_ADDR) { Ownable(L2_TX_FEE_VAULT_ADDR).transferOwnership(OWNER_ADDR); } - if (Ownable(L2_WETH_GATEWAY_PROXY_ADDR).owner() != OWNER_ADDR) { + if (!ALTERNATIVE_GAS_TOKEN_ENABLED && Ownable(L2_WETH_GATEWAY_PROXY_ADDR).owner() != OWNER_ADDR) { Ownable(L2_WETH_GATEWAY_PROXY_ADDR).transferOwnership(OWNER_ADDR); } if (Ownable(L2_PROXY_ADMIN_ADDR).owner() != OWNER_ADDR) { diff --git a/scripts/deterministic/GenerateConfigs.s.sol b/scripts/deterministic/GenerateConfigs.s.sol index 5e055de..68d9f4b 100644 --- a/scripts/deterministic/GenerateConfigs.s.sol +++ b/scripts/deterministic/GenerateConfigs.s.sol @@ -128,9 +128,11 @@ contract GenerateChainMonitorConfig is DeployScroll { vm.writeJson(vm.toString(L1_CUSTOM_ERC20_GATEWAY_PROXY_ADDR), CHAIN_MONITOR_CONFIG_PATH, ".l1_config.l1_contracts.l1_gateways.custom_erc20_gateway"); vm.writeJson(vm.toString(L1_ERC721_GATEWAY_PROXY_ADDR), CHAIN_MONITOR_CONFIG_PATH, ".l1_config.l1_contracts.l1_gateways.erc721_gateway"); vm.writeJson(vm.toString(L1_ERC1155_GATEWAY_PROXY_ADDR), CHAIN_MONITOR_CONFIG_PATH, ".l1_config.l1_contracts.l1_gateways.erc1155_gateway"); + vm.writeJson(vm.toString(L1_GAS_TOKEN_GATEWAY_PROXY_ADDR), CHAIN_MONITOR_CONFIG_PATH, ".l1_config.l1_contracts.l1_gateways.gas_token_gateway"); vm.writeJson(vm.toString(L1_SCROLL_MESSENGER_PROXY_ADDR), CHAIN_MONITOR_CONFIG_PATH, ".l1_config.l1_contracts.scroll_messenger"); vm.writeJson(vm.toString(L1_MESSAGE_QUEUE_PROXY_ADDR), CHAIN_MONITOR_CONFIG_PATH, ".l1_config.l1_contracts.message_queue"); vm.writeJson(vm.toString(L1_SCROLL_CHAIN_PROXY_ADDR), CHAIN_MONITOR_CONFIG_PATH, ".l1_config.l1_contracts.scroll_chain"); + vm.writeJson(vm.toString(L1_GAS_TOKEN_ADDR), CHAIN_MONITOR_CONFIG_PATH, ".l1_config.l1_contracts.gas_token"); vm.writeJson(vm.toString(L2_DEPLOYER_INITIAL_BALANCE), CHAIN_MONITOR_CONFIG_PATH, ".l1_config.start_messenger_balance"); // L2 @@ -186,6 +188,8 @@ contract GenerateBridgeHistoryConfig is DeployScroll { vm.writeJson(vm.toString(L1_CUSTOM_ERC20_GATEWAY_PROXY_ADDR), BRIDGE_HISTORY_CONFIG_PATH, ".L1.CustomERC20GatewayAddr"); vm.writeJson(vm.toString(L1_ERC721_GATEWAY_PROXY_ADDR), BRIDGE_HISTORY_CONFIG_PATH, ".L1.ERC721GatewayAddr"); vm.writeJson(vm.toString(L1_ERC1155_GATEWAY_PROXY_ADDR), BRIDGE_HISTORY_CONFIG_PATH, ".L1.ERC1155GatewayAddr"); + vm.writeJson(vm.toString(L1_GAS_TOKEN_GATEWAY_PROXY_ADDR), BRIDGE_HISTORY_CONFIG_PATH, ".L1.GasTokenGatewayAddr"); + vm.writeJson(vm.toString(L1_WRAPPED_TOKEN_GATEWAY_ADDR), BRIDGE_HISTORY_CONFIG_PATH, ".L1.WrappedTokenGatewayAddr"); // L2 contracts vm.writeJson(vm.toString(L2_MESSAGE_QUEUE_ADDR), BRIDGE_HISTORY_CONFIG_PATH, ".L2.MessageQueueAddr"); @@ -300,6 +304,10 @@ contract GenerateFrontendConfig is DeployScroll { vm.writeLine(FRONTEND_ENV_PATH, string.concat("REACT_APP_L1_STANDARD_ERC20_GATEWAY_PROXY_ADDR = \"", vm.toString(L1_STANDARD_ERC20_GATEWAY_PROXY_ADDR), "\"")); vm.writeLine(FRONTEND_ENV_PATH, string.concat("REACT_APP_L1_WETH_GATEWAY_PROXY_ADDR = \"", vm.toString(L1_WETH_GATEWAY_PROXY_ADDR), "\"")); vm.writeLine(FRONTEND_ENV_PATH, string.concat("REACT_APP_SCROLL_CHAIN = \"", vm.toString(L1_SCROLL_CHAIN_PROXY_ADDR), "\"")); + vm.writeLine(FRONTEND_ENV_PATH, string.concat("REACT_APP_L1_GAS_TOKEN_GATEWAY = \"", vm.toString(L1_GAS_TOKEN_GATEWAY_PROXY_ADDR), "\"")); + vm.writeLine(FRONTEND_ENV_PATH, string.concat("REACT_APP_L1_WRAPPED_TOKEN_GATEWAY = \"", vm.toString(L1_WRAPPED_TOKEN_GATEWAY_ADDR), "\"")); + vm.writeLine(FRONTEND_ENV_PATH, string.concat("REACT_APP_L1_GAS_TOKEN_ADDR = \"", vm.toString(L1_GAS_TOKEN_ADDR), "\"")); + vm.writeLine(FRONTEND_ENV_PATH, string.concat("REACT_APP_L1_WETH_ADDR = \"", vm.toString(L1_WETH_ADDR), "\"")); // L2 contracts vm.writeLine(FRONTEND_ENV_PATH, ""); diff --git a/src/L1/L1ScrollMessenger.sol b/src/L1/L1ScrollMessenger.sol index 544beb2..498757a 100644 --- a/src/L1/L1ScrollMessenger.sol +++ b/src/L1/L1ScrollMessenger.sol @@ -118,7 +118,7 @@ contract L1ScrollMessenger is ScrollMessengerBase, IL1ScrollMessenger { address _feeVault, address _rollup, address _messageQueue - ) public initializer { + ) external initializer { ScrollMessengerBase.__ScrollMessengerBase_init(_counterpart, _feeVault); __rollup = _rollup; @@ -162,36 +162,7 @@ contract L1ScrollMessenger is ScrollMessengerBase, IL1ScrollMessenger { bytes memory _message, L2MessageProof memory _proof ) external override whenNotPaused notInExecution { - bytes32 _xDomainCalldataHash = keccak256(_encodeXDomainCalldata(_from, _to, _value, _nonce, _message)); - require(!isL2MessageExecuted[_xDomainCalldataHash], "Message was already successfully executed"); - - { - require(IScrollChain(rollup).isBatchFinalized(_proof.batchIndex), "Batch is not finalized"); - bytes32 _messageRoot = IScrollChain(rollup).withdrawRoots(_proof.batchIndex); - require( - WithdrawTrieVerifier.verifyMerkleProof(_messageRoot, _xDomainCalldataHash, _nonce, _proof.merkleProof), - "Invalid proof" - ); - } - - // @note check more `_to` address to avoid attack in the future when we add more gateways. - require(_to != messageQueue, "Forbid to call message queue"); - _validateTargetAddress(_to); - - // @note This usually will never happen, just in case. - require(_from != xDomainMessageSender, "Invalid message sender"); - - xDomainMessageSender = _from; - (bool success, ) = _to.call{value: _value}(_message); - // reset value to refund gas. - xDomainMessageSender = ScrollConstants.DEFAULT_XDOMAIN_MESSAGE_SENDER; - - if (success) { - isL2MessageExecuted[_xDomainCalldataHash] = true; - emit RelayedMessage(_xDomainCalldataHash); - } else { - emit FailedRelayedMessage(_xDomainCalldataHash); - } + _relayMessageWithProof(_from, _to, _value, _nonce, _message, _proof); } /// @inheritdoc IL1ScrollMessenger @@ -266,48 +237,7 @@ contract L1ScrollMessenger is ScrollMessengerBase, IL1ScrollMessenger { uint256 _messageNonce, bytes memory _message ) external override whenNotPaused notInExecution { - // The criteria for dropping a message: - // 1. The message is a L1 message. - // 2. The message has not been dropped before. - // 3. the message and all of its replacement are finalized in L1. - // 4. the message and all of its replacement are skipped. - // - // Possible denial of service attack: - // + replayMessage is called every time someone want to drop the message. - // + replayMessage is called so many times for a skipped message, thus results a long list. - // - // We limit the number of `replayMessage` calls of each message, which may solve the above problem. - - // check message exists - bytes memory _xDomainCalldata = _encodeXDomainCalldata(_from, _to, _value, _messageNonce, _message); - bytes32 _xDomainCalldataHash = keccak256(_xDomainCalldata); - require(messageSendTimestamp[_xDomainCalldataHash] > 0, "Provided message has not been enqueued"); - - // check message not dropped - require(!isL1MessageDropped[_xDomainCalldataHash], "Message already dropped"); - - // check message is finalized - uint256 _lastIndex = replayStates[_xDomainCalldataHash].lastIndex; - if (_lastIndex == 0) _lastIndex = _messageNonce; - - // check message is skipped and drop it. - // @note If the list is very long, the message may never be dropped. - while (true) { - IL1MessageQueue(messageQueue).dropCrossDomainMessage(_lastIndex); - _lastIndex = prevReplayIndex[_lastIndex]; - if (_lastIndex == 0) break; - unchecked { - _lastIndex = _lastIndex - 1; - } - } - - isL1MessageDropped[_xDomainCalldataHash] = true; - - // set execution context - xDomainMessageSender = ScrollConstants.DROP_XDOMAIN_MESSAGE_SENDER; - IMessageDropCallback(_from).onDropMessage{value: _value}(_message); - // clear execution context - xDomainMessageSender = ScrollConstants.DEFAULT_XDOMAIN_MESSAGE_SENDER; + _dropMessage(_from, _to, _value, _messageNonce, _message); } /************************ @@ -328,13 +258,14 @@ contract L1ScrollMessenger is ScrollMessengerBase, IL1ScrollMessenger { * Internal Functions * **********************/ + /// @dev Internal function to do `sendMessage` function call. function _sendMessage( address _to, uint256 _value, bytes memory _message, uint256 _gasLimit, address _refundAddress - ) internal nonReentrant { + ) internal virtual nonReentrant { // compute the actual cross domain message calldata. uint256 _messageNonce = IL1MessageQueue(messageQueue).nextCrossDomainMessageIndex(); bytes memory _xDomainCalldata = _encodeXDomainCalldata(_msgSender(), _to, _value, _messageNonce, _message); @@ -368,4 +299,97 @@ contract L1ScrollMessenger is ScrollMessengerBase, IL1ScrollMessenger { } } } + + /// @dev Internal function to do `relayMessageWithProof` function call. + function _relayMessageWithProof( + address _from, + address _to, + uint256 _value, + uint256 _nonce, + bytes memory _message, + L2MessageProof memory _proof + ) internal virtual { + bytes32 _xDomainCalldataHash = keccak256(_encodeXDomainCalldata(_from, _to, _value, _nonce, _message)); + require(!isL2MessageExecuted[_xDomainCalldataHash], "Message was already successfully executed"); + + { + require(IScrollChain(rollup).isBatchFinalized(_proof.batchIndex), "Batch is not finalized"); + bytes32 _messageRoot = IScrollChain(rollup).withdrawRoots(_proof.batchIndex); + require( + WithdrawTrieVerifier.verifyMerkleProof(_messageRoot, _xDomainCalldataHash, _nonce, _proof.merkleProof), + "Invalid proof" + ); + } + + // @note check more `_to` address to avoid attack in the future when we add more gateways. + require(_to != messageQueue, "Forbid to call message queue"); + _validateTargetAddress(_to); + + // @note This usually will never happen, just in case. + require(_from != xDomainMessageSender, "Invalid message sender"); + + xDomainMessageSender = _from; + (bool success, ) = _to.call{value: _value}(_message); + // reset value to refund gas. + xDomainMessageSender = ScrollConstants.DEFAULT_XDOMAIN_MESSAGE_SENDER; + + if (success) { + isL2MessageExecuted[_xDomainCalldataHash] = true; + emit RelayedMessage(_xDomainCalldataHash); + } else { + emit FailedRelayedMessage(_xDomainCalldataHash); + } + } + + /// @dev Internal function to do `dropMessage` function call. + function _dropMessage( + address _from, + address _to, + uint256 _value, + uint256 _messageNonce, + bytes memory _message + ) internal virtual { + // The criteria for dropping a message: + // 1. The message is a L1 message. + // 2. The message has not been dropped before. + // 3. the message and all of its replacement are finalized in L1. + // 4. the message and all of its replacement are skipped. + // + // Possible denial of service attack: + // + replayMessage is called every time someone want to drop the message. + // + replayMessage is called so many times for a skipped message, thus results a long list. + // + // We limit the number of `replayMessage` calls of each message, which may solve the above problem. + + // check message exists + bytes memory _xDomainCalldata = _encodeXDomainCalldata(_from, _to, _value, _messageNonce, _message); + bytes32 _xDomainCalldataHash = keccak256(_xDomainCalldata); + require(messageSendTimestamp[_xDomainCalldataHash] > 0, "Provided message has not been enqueued"); + + // check message not dropped + require(!isL1MessageDropped[_xDomainCalldataHash], "Message already dropped"); + + // check message is finalized + uint256 _lastIndex = replayStates[_xDomainCalldataHash].lastIndex; + if (_lastIndex == 0) _lastIndex = _messageNonce; + + // check message is skipped and drop it. + // @note If the list is very long, the message may never be dropped. + while (true) { + IL1MessageQueue(messageQueue).dropCrossDomainMessage(_lastIndex); + _lastIndex = prevReplayIndex[_lastIndex]; + if (_lastIndex == 0) break; + unchecked { + _lastIndex = _lastIndex - 1; + } + } + + isL1MessageDropped[_xDomainCalldataHash] = true; + + // set execution context + xDomainMessageSender = ScrollConstants.DROP_XDOMAIN_MESSAGE_SENDER; + IMessageDropCallback(_from).onDropMessage{value: _value}(_message); + // clear execution context + xDomainMessageSender = ScrollConstants.DEFAULT_XDOMAIN_MESSAGE_SENDER; + } } diff --git a/src/L1/gateways/L1ETHGateway.sol b/src/L1/gateways/L1ETHGateway.sol index c268a1b..a522f2b 100644 --- a/src/L1/gateways/L1ETHGateway.sol +++ b/src/L1/gateways/L1ETHGateway.sol @@ -14,7 +14,7 @@ import {ScrollGatewayBase} from "../../libraries/gateway/ScrollGatewayBase.sol"; /// @title L1ETHGateway /// @notice The `L1ETHGateway` is used to deposit ETH on layer 1 and /// finalize withdraw ETH from layer 2. -/// @dev The deposited ETH tokens are held in this gateway. On finalizing withdraw, the corresponding +/// @dev The deposited ETH tokens are held in `L1ScrollMessenger`. On finalizing withdraw, the corresponding /// ETH will be transfer to the recipient directly. contract L1ETHGateway is ScrollGatewayBase, IL1ETHGateway, IMessageDropCallback { /*************** diff --git a/src/alternative-gas-token/GasTokenExample.sol b/src/alternative-gas-token/GasTokenExample.sol new file mode 100644 index 0000000..dff43af --- /dev/null +++ b/src/alternative-gas-token/GasTokenExample.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT + +pragma solidity =0.8.24; + +import "@openzeppelin/contracts/token/ERC20/extensions/draft-ERC20Permit.sol"; + +contract GasTokenExample is ERC20 { + uint8 private decimals_; + + constructor( + string memory _name, + string memory _symbol, + uint8 _decimals, + address _recipient, + uint256 _amount + ) ERC20(_name, _symbol) { + decimals_ = _decimals; + _mint(_recipient, _amount); + } + + function decimals() public view virtual override returns (uint8) { + return decimals_; + } +} diff --git a/src/alternative-gas-token/L1GasTokenGateway.sol b/src/alternative-gas-token/L1GasTokenGateway.sol new file mode 100644 index 0000000..c571ee3 --- /dev/null +++ b/src/alternative-gas-token/L1GasTokenGateway.sol @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: MIT + +pragma solidity =0.8.24; + +import {IERC20MetadataUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/IERC20MetadataUpgradeable.sol"; +import {SafeERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; +import {IERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; + +import {IL1ETHGateway} from "../L1/gateways/IL1ETHGateway.sol"; +import {IL1ScrollMessenger} from "../L1/IL1ScrollMessenger.sol"; +import {IL2ETHGateway} from "../L2/gateways/IL2ETHGateway.sol"; + +import {IMessageDropCallback} from "../libraries/callbacks/IMessageDropCallback.sol"; +import {ScrollGatewayBase} from "../libraries/gateway/ScrollGatewayBase.sol"; + +// solhint-disable avoid-low-level-calls + +/// @title L1GasTokenGateway +/// @notice The `L1GasTokenGateway` is used to deposit gas token on layer 1 and +/// finalize withdraw gas token from layer 2. +/// @dev The deposited gas tokens are held in this gateway. On finalizing withdraw, the corresponding +/// gas token will be transfer to the recipient directly. +contract L1GasTokenGateway is ScrollGatewayBase, IL1ETHGateway, IMessageDropCallback { + using SafeERC20Upgradeable for IERC20Upgradeable; + + /********** + * Errors * + **********/ + + /// @dev Thrown when `msg.value` is not zero. + error ErrorNonZeroMsgValue(); + + /// @dev Thrown when the selector is invalid during `onDropMessage`. + error ErrorInvalidSelector(); + + /// @dev Thrown when the deposit amount is zero. + error ErrorDepositZeroGasToken(); + + /************* + * Constants * + *************/ + + /// @dev The address of gas token. + address public immutable gasToken; + + /// @dev The scalar to scale the gas token decimals to 18. + uint256 public immutable scale; + + /*************** + * Constructor * + ***************/ + + /// @notice Constructor for `L1GasTokenGateway` implementation contract. + /// + /// @param _gasToken The address of gas token in L1. + /// @param _counterpart The address of `L2ETHGateway` contract in L2. + /// @param _router The address of `L1GatewayRouter` contract in L1. + /// @param _messenger The address of `L1ScrollMessenger` contract in L1. + constructor( + address _gasToken, + address _counterpart, + address _router, + address _messenger + ) ScrollGatewayBase(_counterpart, _router, _messenger) { + if (_gasToken == address(0) || _router == address(0)) revert ErrorZeroAddress(); + + _disableInitializers(); + + gasToken = _gasToken; + scale = 10**(18 - IERC20MetadataUpgradeable(_gasToken).decimals()); + } + + /// @notice Initialize the storage of L1GasTokenGateway. + function initialize() external initializer { + ScrollGatewayBase._initialize(address(0), address(0), address(0)); + } + + /***************************** + * Public Mutating Functions * + *****************************/ + + /// @inheritdoc IL1ETHGateway + function depositETH(uint256 _amount, uint256 _gasLimit) external payable override { + _deposit(_msgSender(), _amount, new bytes(0), _gasLimit); + } + + /// @inheritdoc IL1ETHGateway + function depositETH( + address _to, + uint256 _amount, + uint256 _gasLimit + ) external payable override { + _deposit(_to, _amount, new bytes(0), _gasLimit); + } + + /// @inheritdoc IL1ETHGateway + function depositETHAndCall( + address _to, + uint256 _amount, + bytes calldata _data, + uint256 _gasLimit + ) external payable override { + _deposit(_to, _amount, _data, _gasLimit); + } + + /// @inheritdoc IL1ETHGateway + function finalizeWithdrawETH( + address _from, + address _to, + uint256 _amount, + bytes calldata _data + ) external payable override onlyCallByCounterpart nonReentrant { + if (msg.value > 0) { + revert ErrorNonZeroMsgValue(); + } + + uint256 downScaledAmount = _amount / scale; + IERC20Upgradeable(gasToken).safeTransfer(_to, downScaledAmount); + _doCallback(_to, _data); + + emit FinalizeWithdrawETH(_from, _to, downScaledAmount, _data); + } + + /// @inheritdoc IMessageDropCallback + function onDropMessage(bytes calldata _message) external payable virtual onlyInDropContext nonReentrant { + // _message should start with 0x232e8748 => finalizeDepositETH(address,address,uint256,bytes) + if (bytes4(_message[0:4]) != IL2ETHGateway.finalizeDepositETH.selector) { + revert ErrorInvalidSelector(); + } + + // decode (receiver, amount) + (address _receiver, , uint256 _amount, ) = abi.decode(_message[4:], (address, address, uint256, bytes)); + uint256 downScaledAmount = _amount / scale; + + IERC20Upgradeable(gasToken).safeTransfer(_receiver, downScaledAmount); + + emit RefundETH(_receiver, downScaledAmount); + } + + /********************** + * Internal Functions * + **********************/ + + /// @dev The internal ETH deposit implementation. + /// @param _to The address of recipient's account on L2. + /// @param _amount The amount of ETH to be deposited. + /// @param _data Optional data to forward to recipient's account. + /// @param _gasLimit Gas limit required to complete the deposit on L2. + function _deposit( + address _to, + uint256 _amount, + bytes memory _data, + uint256 _gasLimit + ) internal virtual nonReentrant { + // 1. Extract real sender if this call is from L1GatewayRouter. + address _from = _msgSender(); + + if (router == _from) { + (_from, _data) = abi.decode(_data, (address, bytes)); + } + + // 2. transfer gas token from caller + uint256 _before = IERC20Upgradeable(gasToken).balanceOf(address(this)); + IERC20Upgradeable(gasToken).safeTransferFrom(_from, address(this), _amount); + uint256 _after = IERC20Upgradeable(gasToken).balanceOf(address(this)); + _amount = _after - _before; + if (_amount == 0) { + revert ErrorDepositZeroGasToken(); + } + + uint256 upScaledAmount = _amount * scale; + + // 3. Generate message passed to L1ScrollMessenger. + bytes memory _message = abi.encodeCall(IL2ETHGateway.finalizeDepositETH, (_from, _to, upScaledAmount, _data)); + + IL1ScrollMessenger(messenger).sendMessage{value: msg.value}( + counterpart, + upScaledAmount, + _message, + _gasLimit, + _from + ); + + emit DepositETH(_from, _to, _amount, _data); + } +} diff --git a/src/alternative-gas-token/L1ScrollMessengerNonETH.sol b/src/alternative-gas-token/L1ScrollMessengerNonETH.sol new file mode 100644 index 0000000..969e7aa --- /dev/null +++ b/src/alternative-gas-token/L1ScrollMessengerNonETH.sol @@ -0,0 +1,243 @@ +// SPDX-License-Identifier: MIT + +pragma solidity =0.8.24; + +import {AddressUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol"; + +import {IL1MessageQueue} from "../L1/rollup/IL1MessageQueue.sol"; +import {IScrollChain} from "../L1/rollup/IScrollChain.sol"; +import {L1ScrollMessenger} from "../L1/L1ScrollMessenger.sol"; +import {IMessageDropCallback} from "../libraries/callbacks/IMessageDropCallback.sol"; +import {ScrollConstants} from "../libraries/constants/ScrollConstants.sol"; +import {WithdrawTrieVerifier} from "../libraries/verifier/WithdrawTrieVerifier.sol"; + +contract L1ScrollMessengerNonETH is L1ScrollMessenger { + /********** + * Errors * + **********/ + + /// @dev Thrown when the message is duplicated. + error ErrorDuplicatedMessage(); + + /// @dev Thrown when caller pass non-zero value in `sendMessage`. + error ErrorNonZeroValueFromCaller(); + + /// @dev Thrown when caller pass non-zero value in `relayMessageWithProof`. + error ErrorNonZeroValueFromCrossDomainCaller(); + + /// @dev Thrown when the `msg.value` cannot cover cross domain fee. + error ErrorInsufficientMsgValue(); + + /// @dev Thrown when the message is executed before. + error ErrorMessageExecuted(); + + /// @dev Thrown when the message has not enqueued before. + error ErrorMessageNotEnqueued(); + + /// @dev Thrown when the message is dropped before. + error ErrorMessageDropped(); + + /// @dev Thrown when relay a message belonging to an unfinalized batch. + error ErrorBatchNotFinalized(); + + /// @dev Thrown when the provided merkle proof is invalid. + error ErrorInvalidMerkleProof(); + + /// @dev Thrown when call to message queue. + error ErrorForbidToCallMessageQueue(); + + /// @dev Thrown when the message sender is invalid. + error ErrorInvalidMessageSender(); + + /************* + * Constants * + *************/ + + /// @notice The address of `L1NativeTokenGateway` contract. + address public immutable nativeTokenGateway; + + /*************** + * Constructor * + ***************/ + + constructor( + address _nativeTokenGateway, + address _counterpart, + address _rollup, + address _messageQueue + ) L1ScrollMessenger(_counterpart, _rollup, _messageQueue) { + nativeTokenGateway = _nativeTokenGateway; + } + + /********************** + * Internal Functions * + **********************/ + + /// @inheritdoc L1ScrollMessenger + function _sendMessage( + address _to, + uint256 _l2GasTokenValue, + bytes memory _message, + uint256 _gasLimit, + address _refundAddress + ) internal override { + // if we want to pass value to L2, must call from `L1NativeTokenGateway`. + if (_l2GasTokenValue > 0 && _msgSender() != nativeTokenGateway) { + revert ErrorNonZeroValueFromCaller(); + } + + // compute the actual cross domain message calldata. + uint256 _messageNonce = IL1MessageQueue(messageQueue).nextCrossDomainMessageIndex(); + bytes memory _xDomainCalldata = _encodeXDomainCalldata( + _msgSender(), + _to, + _l2GasTokenValue, + _messageNonce, + _message + ); + + // compute and deduct the messaging fee to fee vault. + uint256 _fee = IL1MessageQueue(messageQueue).estimateCrossDomainMessageFee(_gasLimit); + if (msg.value < _fee) { + revert ErrorInsufficientMsgValue(); + } + if (_fee > 0) { + AddressUpgradeable.sendValue(payable(feeVault), _fee); + } + + // append message to L1MessageQueue + IL1MessageQueue(messageQueue).appendCrossDomainMessage(counterpart, _gasLimit, _xDomainCalldata); + + // record the message hash for future use. + bytes32 _xDomainCalldataHash = keccak256(_xDomainCalldata); + + // normally this won't happen, since each message has different nonce, but just in case. + if (messageSendTimestamp[_xDomainCalldataHash] != 0) { + revert ErrorDuplicatedMessage(); + } + messageSendTimestamp[_xDomainCalldataHash] = block.timestamp; + + emit SentMessage(_msgSender(), _to, _l2GasTokenValue, _messageNonce, _gasLimit, _message); + + // refund fee to `_refundAddress` + unchecked { + uint256 _refund = msg.value - _fee; + if (_refund > 0) { + AddressUpgradeable.sendValue(payable(_refundAddress), _refund); + } + } + } + + /// @inheritdoc L1ScrollMessenger + function _relayMessageWithProof( + address _from, + address _to, + uint256 _l2GasTokenValue, + uint256 _nonce, + bytes memory _message, + L2MessageProof memory _proof + ) internal virtual override { + // if we want to pass value to L1, must call to `L1NativeTokenGateway`. + if (_l2GasTokenValue > 0 && _to != nativeTokenGateway) { + revert ErrorNonZeroValueFromCrossDomainCaller(); + } + + bytes32 _xDomainCalldataHash = keccak256( + _encodeXDomainCalldata(_from, _to, _l2GasTokenValue, _nonce, _message) + ); + if (isL2MessageExecuted[_xDomainCalldataHash]) { + revert ErrorMessageExecuted(); + } + + { + if (!IScrollChain(rollup).isBatchFinalized(_proof.batchIndex)) { + revert ErrorBatchNotFinalized(); + } + bytes32 _messageRoot = IScrollChain(rollup).withdrawRoots(_proof.batchIndex); + if ( + !WithdrawTrieVerifier.verifyMerkleProof(_messageRoot, _xDomainCalldataHash, _nonce, _proof.merkleProof) + ) { + revert ErrorInvalidMerkleProof(); + } + } + + // @note check more `_to` address to avoid attack in the future when we add more gateways. + if (_to == messageQueue) { + revert ErrorForbidToCallMessageQueue(); + } + _validateTargetAddress(_to); + + // @note This usually will never happen, just in case. + if (_from == xDomainMessageSender) { + revert ErrorInvalidMessageSender(); + } + + xDomainMessageSender = _from; + (bool success, ) = _to.call(_message); + // reset value to refund gas. + xDomainMessageSender = ScrollConstants.DEFAULT_XDOMAIN_MESSAGE_SENDER; + + if (success) { + isL2MessageExecuted[_xDomainCalldataHash] = true; + emit RelayedMessage(_xDomainCalldataHash); + } else { + emit FailedRelayedMessage(_xDomainCalldataHash); + } + } + + /// @inheritdoc L1ScrollMessenger + function _dropMessage( + address _from, + address _to, + uint256 _l2GasTokenValue, + uint256 _messageNonce, + bytes memory _message + ) internal virtual override { + // The criteria for dropping a message: + // 1. The message is a L1 message. + // 2. The message has not been dropped before. + // 3. the message and all of its replacement are finalized in L1. + // 4. the message and all of its replacement are skipped. + // + // Possible denial of service attack: + // + replayMessage is called every time someone want to drop the message. + // + replayMessage is called so many times for a skipped message, thus results a long list. + // + // We limit the number of `replayMessage` calls of each message, which may solve the above problem. + + // check message exists + bytes memory _xDomainCalldata = _encodeXDomainCalldata(_from, _to, _l2GasTokenValue, _messageNonce, _message); + bytes32 _xDomainCalldataHash = keccak256(_xDomainCalldata); + if (messageSendTimestamp[_xDomainCalldataHash] == 0) { + revert ErrorMessageNotEnqueued(); + } + + // check message not dropped + if (isL1MessageDropped[_xDomainCalldataHash]) { + revert ErrorMessageDropped(); + } + + // check message is finalized + uint256 _lastIndex = replayStates[_xDomainCalldataHash].lastIndex; + if (_lastIndex == 0) _lastIndex = _messageNonce; + + // check message is skipped and drop it. + // @note If the list is very long, the message may never be dropped. + while (true) { + IL1MessageQueue(messageQueue).dropCrossDomainMessage(_lastIndex); + _lastIndex = prevReplayIndex[_lastIndex]; + if (_lastIndex == 0) break; + unchecked { + _lastIndex = _lastIndex - 1; + } + } + + isL1MessageDropped[_xDomainCalldataHash] = true; + + // set execution context + xDomainMessageSender = ScrollConstants.DROP_XDOMAIN_MESSAGE_SENDER; + IMessageDropCallback(_from).onDropMessage(_message); + // clear execution context + xDomainMessageSender = ScrollConstants.DEFAULT_XDOMAIN_MESSAGE_SENDER; + } +} diff --git a/src/alternative-gas-token/L1WrappedTokenGateway.sol b/src/alternative-gas-token/L1WrappedTokenGateway.sol new file mode 100644 index 0000000..065a00f --- /dev/null +++ b/src/alternative-gas-token/L1WrappedTokenGateway.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: MIT + +pragma solidity =0.8.24; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + +import {IL1ERC20Gateway} from "../L1/gateways/IL1ERC20Gateway.sol"; +import {IWETH} from "../interfaces/IWETH.sol"; + +contract L1WrappedTokenGateway { + using SafeERC20 for IERC20; + + /********** + * Events * + **********/ + + /// @notice Emitted when someone wrap ETH to WETH and then deposit WETH from L1 to L2. + /// @param from The address of sender in L1. + /// @param to The address of recipient in L2. + /// @param amount The amount of ETH will be deposited from L1 to L2. + event DepositWrappedToken(address indexed from, address indexed to, uint256 amount); + + /********* + * Error * + *********/ + + /// @dev Thrown when someone try to send ETH to this contract. + error ErrorCallNotFromFeeRefund(); + + /************* + * Constants * + *************/ + + /// @dev The safe gas limit used to bridge WETH to L2. + uint256 private constant SAFE_GAS_LIMIT = 450000; + + /// @dev The default value of `sender`. + address private constant DEFAULT_SENDER = address(1); + + /// @notice The address of Wrapped Ether. + address public immutable WETH; + + /// @notice The address of ERC20 gateway used to bridge WETH. + address public immutable gateway; + + /************* + * Variables * + *************/ + + /// @notice The address of caller who called `deposit`. + /// @dev This will be reset after call `gateway.depositERC20`, which is used to + /// prevent malicious user sending ETH to this contract. + address public sender; + + /*************** + * Constructor * + ***************/ + + constructor(address _weth, address _gateway) { + WETH = _weth; + gateway = _gateway; + + sender = DEFAULT_SENDER; + } + + /***************************** + * Public Mutating Functions * + *****************************/ + + /// @dev Only receive cross domain fee refund + receive() external payable { + if (sender == DEFAULT_SENDER) { + revert ErrorCallNotFromFeeRefund(); + } + } + + /// @notice Deposit ETH. + /// @dev This will wrap ETH to WETH first and then deposit as WETH. + /// @param _to The address of recipient in L2. + /// @param _amount The amount of ETH to deposit. + function deposit(address _to, uint256 _amount) external payable { + IWETH(WETH).deposit{value: _amount}(); + + IERC20(WETH).safeApprove(gateway, 0); + IERC20(WETH).safeApprove(gateway, _amount); + sender = msg.sender; + IL1ERC20Gateway(gateway).depositERC20{value: msg.value - _amount}(WETH, _to, _amount, SAFE_GAS_LIMIT); + sender = DEFAULT_SENDER; + + emit DepositWrappedToken(msg.sender, _to, _amount); + + // refund exceed fee + uint256 balance = address(this).balance; + if (balance > 0) { + Address.sendValue(payable(msg.sender), balance); + } + } +} diff --git a/src/test/alternative-gas-token/AlternativeGasTokenTestBase.t.sol b/src/test/alternative-gas-token/AlternativeGasTokenTestBase.t.sol new file mode 100644 index 0000000..7069286 --- /dev/null +++ b/src/test/alternative-gas-token/AlternativeGasTokenTestBase.t.sol @@ -0,0 +1,272 @@ +// SPDX-License-Identifier: MIT + +pragma solidity =0.8.24; + +import {Test} from "forge-std/Test.sol"; +import {Vm} from "forge-std/Vm.sol"; + +import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; +import {ITransparentUpgradeableProxy, TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +import {L1GasTokenGateway} from "../../alternative-gas-token/L1GasTokenGateway.sol"; +import {L1ScrollMessengerNonETH} from "../../alternative-gas-token/L1ScrollMessengerNonETH.sol"; +import {L1GatewayRouter} from "../../L1/gateways/L1GatewayRouter.sol"; +import {EnforcedTxGateway} from "../../L1/gateways/EnforcedTxGateway.sol"; +import {L1MessageQueueWithGasPriceOracle} from "../../L1/rollup/L1MessageQueueWithGasPriceOracle.sol"; +import {L2GasPriceOracle} from "../../L1/rollup/L2GasPriceOracle.sol"; +import {ScrollChain, IScrollChain} from "../../L1/rollup/ScrollChain.sol"; +import {L2GatewayRouter} from "../../L2/gateways/L2GatewayRouter.sol"; +import {L2ETHGateway} from "../../L2/gateways/L2ETHGateway.sol"; +import {L2MessageQueue} from "../../L2/predeploys/L2MessageQueue.sol"; +import {Whitelist} from "../../L2/predeploys/Whitelist.sol"; +import {L2ScrollMessenger, IL2ScrollMessenger} from "../../L2/L2ScrollMessenger.sol"; +import {AddressAliasHelper} from "../../libraries/common/AddressAliasHelper.sol"; +import {EmptyContract} from "../../misc/EmptyContract.sol"; + +import {MockRollupVerifier} from "../mocks/MockRollupVerifier.sol"; + +abstract contract AlternativeGasTokenTestBase is Test { + // from L1MessageQueue + event QueueTransaction( + address indexed sender, + address indexed target, + uint256 value, + uint64 queueIndex, + uint256 gasLimit, + bytes data + ); + + // from L1ScrollMessengerNonETH + event SentMessage( + address indexed sender, + address indexed target, + uint256 value, + uint256 messageNonce, + uint256 gasLimit, + bytes message + ); + event RelayedMessage(bytes32 indexed messageHash); + event FailedRelayedMessage(bytes32 indexed messageHash); + + bytes32 private constant SENT_MESSAGE_TOPIC = + keccak256("SentMessage(address,address,uint256,uint256,uint256,bytes)"); + + ProxyAdmin internal admin; + EmptyContract private placeholder; + + // L1 contracts + L1ScrollMessengerNonETH internal l1Messenger; + L1MessageQueueWithGasPriceOracle internal l1MessageQueue; + ScrollChain internal rollup; + L1GasTokenGateway internal l1GasTokenGateway; + L1GatewayRouter internal l1Router; + address internal l1FeeVault; + + // L2 contracts + L2ScrollMessenger internal l2Messenger; + L2MessageQueue internal l2MessageQueue; + L2ETHGateway internal l2ETHGateway; + L2GatewayRouter internal l2Router; + + uint256 private lastFromL2LogIndex; + uint256 private lastFromL1LogIndex; + + function __AlternativeGasTokenTestBase_setUp(uint64 l2ChainId, address gasToken) internal { + admin = new ProxyAdmin(); + placeholder = new EmptyContract(); + + // deploy proxy and contracts in L1 + l1FeeVault = address(uint160(address(this)) - 1); + l1MessageQueue = L1MessageQueueWithGasPriceOracle(_deployProxy(address(0))); + rollup = ScrollChain(_deployProxy(address(0))); + l1Messenger = L1ScrollMessengerNonETH(payable(_deployProxy(address(0)))); + l1GasTokenGateway = L1GasTokenGateway(_deployProxy(address(0))); + l1Router = L1GatewayRouter(_deployProxy(address(0))); + L2GasPriceOracle gasOracle = L2GasPriceOracle(_deployProxy(address(new L2GasPriceOracle()))); + Whitelist whitelist = new Whitelist(address(this)); + MockRollupVerifier verifier = new MockRollupVerifier(); + + // deploy proxy and contracts in L2 + l2MessageQueue = new L2MessageQueue(address(this)); + l2Messenger = L2ScrollMessenger(payable(_deployProxy(address(0)))); + l2ETHGateway = L2ETHGateway(payable(_deployProxy(address(0)))); + l2Router = L2GatewayRouter(_deployProxy(address(0))); + + // Upgrade the L1ScrollMessengerNonETH implementation and initialize + admin.upgrade( + ITransparentUpgradeableProxy(address(l1Messenger)), + address( + new L1ScrollMessengerNonETH( + address(l1GasTokenGateway), + address(l2Messenger), + address(rollup), + address(l1MessageQueue) + ) + ) + ); + l1Messenger.initialize(address(l2Messenger), l1FeeVault, address(rollup), address(l1MessageQueue)); + + // initialize L2GasPriceOracle + gasOracle.initialize(1, 2, 1, 1); + gasOracle.updateWhitelist(address(whitelist)); + + // Upgrade the L1MessageQueueWithGasPriceOracle implementation and initialize + admin.upgrade( + ITransparentUpgradeableProxy(address(l1MessageQueue)), + address(new L1MessageQueueWithGasPriceOracle(address(l1Messenger), address(rollup), address(1))) + ); + l1MessageQueue.initialize(address(l1Messenger), address(rollup), address(this), address(gasOracle), 10000000); + l1MessageQueue.initializeV2(); + + // Upgrade the ScrollChain implementation and initialize + admin.upgrade( + ITransparentUpgradeableProxy(address(rollup)), + address(new ScrollChain(l2ChainId, address(l1MessageQueue), address(verifier))) + ); + rollup.initialize(address(l1MessageQueue), address(verifier), 44); + + // Upgrade the L1GasTokenGateway implementation and initialize + admin.upgrade( + ITransparentUpgradeableProxy(address(l1GasTokenGateway)), + address(new L1GasTokenGateway(gasToken, address(l2ETHGateway), address(l1Router), address(l1Messenger))) + ); + l1GasTokenGateway.initialize(); + + // Upgrade the L1GatewayRouter implementation and initialize + admin.upgrade(ITransparentUpgradeableProxy(address(l1Router)), address(new L1GatewayRouter())); + l1Router.initialize(address(l1GasTokenGateway), address(0)); + + // L2ScrollMessenger + admin.upgrade( + ITransparentUpgradeableProxy(address(l2Messenger)), + address(new L2ScrollMessenger(address(l1Messenger), address(l2MessageQueue))) + ); + l2Messenger.initialize(address(0)); + l2MessageQueue.initialize(address(l2Messenger)); + + // L2ETHGateway + admin.upgrade( + ITransparentUpgradeableProxy(address(l2ETHGateway)), + address(new L2ETHGateway(address(l1GasTokenGateway), address(l2Router), address(l2Messenger))) + ); + l2ETHGateway.initialize(address(l1GasTokenGateway), address(l2Router), address(l2Messenger)); + + // L2GatewayRouter + admin.upgrade(ITransparentUpgradeableProxy(address(l2Router)), address(new L2GatewayRouter())); + l2Router.initialize(address(l2ETHGateway), address(0)); + + // Setup whitelist in L1 + address[] memory _accounts = new address[](1); + _accounts[0] = address(this); + whitelist.updateWhitelistStatus(_accounts, true); + + // Make nonzero block.timestamp + vm.warp(1); + + // Allocate balance to l2Messenger + vm.deal(address(l2Messenger), type(uint256).max / 2); + } + + function _deployProxy(address _logic) internal returns (address) { + if (_logic == address(0)) _logic = address(placeholder); + TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(_logic, address(admin), new bytes(0)); + return address(proxy); + } + + function relayFromL1() internal { + address malias = AddressAliasHelper.applyL1ToL2Alias(address(l1Messenger)); + + // Read all L1 -> L2 messages and relay them + Vm.Log[] memory allLogs = vm.getRecordedLogs(); + for (; lastFromL1LogIndex < allLogs.length; lastFromL1LogIndex++) { + Vm.Log memory _log = allLogs[lastFromL1LogIndex]; + if (_log.topics[0] == SENT_MESSAGE_TOPIC && _log.emitter == address(l1Messenger)) { + address sender = address(uint160(uint256(_log.topics[1]))); + address target = address(uint160(uint256(_log.topics[2]))); + (uint256 value, uint256 nonce, uint256 gasLimit, bytes memory message) = abi.decode( + _log.data, + (uint256, uint256, uint256, bytes) + ); + vm.prank(malias); + IL2ScrollMessenger(l2Messenger).relayMessage{gas: gasLimit}(sender, target, value, nonce, message); + } + } + } + + function relayFromL2() internal { + // Read all L2 -> L1 messages and relay them + // Note: We bypass the L1 messenger relay here because it's easier to not have to generate valid state roots / merkle proofs + Vm.Log[] memory allLogs = vm.getRecordedLogs(); + for (; lastFromL2LogIndex < allLogs.length; lastFromL2LogIndex++) { + Vm.Log memory _log = allLogs[lastFromL2LogIndex]; + if (_log.topics[0] == SENT_MESSAGE_TOPIC && _log.emitter == address(l2Messenger)) { + address sender = address(uint160(uint256(_log.topics[1]))); + address target = address(uint160(uint256(_log.topics[2]))); + (, , , bytes memory message) = abi.decode(_log.data, (uint256, uint256, uint256, bytes)); + // Set xDomainMessageSender + vm.store(address(l1Messenger), bytes32(uint256(201)), bytes32(uint256(uint160(sender)))); + vm.startPrank(address(l1Messenger)); + (bool success, bytes memory response) = target.call(message); + vm.stopPrank(); + vm.store(address(l1Messenger), bytes32(uint256(201)), bytes32(uint256(1))); + if (!success) { + assembly { + revert(add(response, 32), mload(response)) + } + } + } + } + } + + function encodeXDomainCalldata( + address _sender, + address _target, + uint256 _value, + uint256 _messageNonce, + bytes memory _message + ) internal pure returns (bytes memory) { + return abi.encodeCall(IL2ScrollMessenger.relayMessage, (_sender, _target, _value, _messageNonce, _message)); + } + + function prepareFinalizedBatch(bytes32 messageHash) internal { + rollup.addSequencer(address(0)); + rollup.addProver(address(0)); + + // import genesis batch + bytes memory batchHeader0 = new bytes(89); + assembly { + mstore(add(batchHeader0, add(0x20, 25)), 1) + } + rollup.importGenesisBatch(batchHeader0, bytes32(uint256(1))); + bytes32 batchHash0 = rollup.committedBatches(0); + + // commit one batch + bytes[] memory chunks = new bytes[](1); + bytes memory chunk0 = new bytes(1 + 60); + chunk0[0] = bytes1(uint8(1)); // one block in this chunk + chunks[0] = chunk0; + vm.startPrank(address(0)); + rollup.commitBatch(0, batchHeader0, chunks, new bytes(0)); + vm.stopPrank(); + + bytes memory batchHeader1 = new bytes(89); + assembly { + mstore(add(batchHeader1, 0x20), 0) // version + mstore(add(batchHeader1, add(0x20, 1)), shl(192, 1)) // batchIndex + mstore(add(batchHeader1, add(0x20, 9)), 0) // l1MessagePopped + mstore(add(batchHeader1, add(0x20, 17)), 0) // totalL1MessagePopped + mstore(add(batchHeader1, add(0x20, 25)), 0x246394445f4fe64ed5598554d55d1682d6fb3fe04bf58eb54ef81d1189fafb51) // dataHash + mstore(add(batchHeader1, add(0x20, 57)), batchHash0) // parentBatchHash + } + + vm.startPrank(address(0)); + rollup.finalizeBatchWithProof( + batchHeader1, + bytes32(uint256(1)), + bytes32(uint256(2)), + messageHash, + new bytes(0) + ); + vm.stopPrank(); + } +} diff --git a/src/test/alternative-gas-token/GasTokenDecimalGateway.t.sol b/src/test/alternative-gas-token/GasTokenDecimalGateway.t.sol new file mode 100644 index 0000000..1da9645 --- /dev/null +++ b/src/test/alternative-gas-token/GasTokenDecimalGateway.t.sol @@ -0,0 +1,481 @@ +// SPDX-License-Identifier: MIT + +pragma solidity =0.8.24; + +import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; + +import {ITransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +import {L1GasTokenGateway} from "../../alternative-gas-token/L1GasTokenGateway.sol"; +import {IL1ScrollMessenger} from "../../L1/IL1ScrollMessenger.sol"; +import {IL2ETHGateway} from "../../L2/gateways/IL2ETHGateway.sol"; +import {AddressAliasHelper} from "../../libraries/common/AddressAliasHelper.sol"; +import {ScrollConstants} from "../../libraries/constants/ScrollConstants.sol"; +import {IScrollGateway} from "../../libraries/gateway/IScrollGateway.sol"; + +import {AlternativeGasTokenTestBase} from "./AlternativeGasTokenTestBase.t.sol"; + +import {MockGatewayRecipient} from "../mocks/MockGatewayRecipient.sol"; +import {MockScrollMessenger} from "../mocks/MockScrollMessenger.sol"; + +contract L1GasTokenGatewayForTest is L1GasTokenGateway { + constructor( + address _gasToken, + address _counterpart, + address _router, + address _messenger + ) L1GasTokenGateway(_gasToken, _counterpart, _router, _messenger) {} + + function reentrantCall(address target, bytes calldata data) external payable nonReentrant { + (bool success, ) = target.call{value: msg.value}(data); + if (!success) { + // solhint-disable-next-line no-inline-assembly + assembly { + let ptr := mload(0x40) + let size := returndatasize() + returndatacopy(ptr, 0, size) + revert(ptr, size) + } + } + } +} + +abstract contract GasTokenGatewayTest is AlternativeGasTokenTestBase { + // from L1GasTokenGateway + event DepositETH(address indexed from, address indexed to, uint256 amount, bytes data); + event FinalizeWithdrawETH(address indexed from, address indexed to, uint256 amount, bytes data); + event RefundETH(address indexed recipient, uint256 amount); + + uint256 private constant NONZERO_TIMESTAMP = 123456; + + MockERC20 private gasToken; + uint256 private tokenScale; + + struct DepositParams { + uint256 methodType; + uint256 amount; + address recipient; + bytes dataToCall; + uint256 gasLimit; + uint256 feeToPay; + uint256 exceedValue; + } + + receive() external payable {} + + function __GasTokenGatewayTest_setUp(uint8 decimals) internal { + gasToken = new MockERC20("X", "Y", decimals); + + __AlternativeGasTokenTestBase_setUp(1234, address(gasToken)); + + admin.upgrade( + ITransparentUpgradeableProxy(address(l1GasTokenGateway)), + address( + new L1GasTokenGatewayForTest( + address(gasToken), + address(l2ETHGateway), + address(l1Router), + address(l1Messenger) + ) + ) + ); + + gasToken.mint(address(this), type(uint128).max); + vm.warp(NONZERO_TIMESTAMP); + + gasToken.approve(address(l1GasTokenGateway), type(uint256).max); + tokenScale = 10**(18 - decimals); + } + + function testDepositETH(DepositParams memory params) external { + params.methodType = 0; + params.recipient = address(this); + params.dataToCall = new bytes(0); + _depositETH(false, params); + } + + function testDepositETHWithRecipient(DepositParams memory params) external { + params.methodType = 1; + params.dataToCall = new bytes(0); + _depositETH(false, params); + } + + function testDepositETHAndCall(DepositParams memory params) external { + params.methodType = 2; + _depositETH(false, params); + } + + function testDepositETHWithRouter(DepositParams memory params) external { + params.methodType = 0; + params.recipient = address(this); + params.dataToCall = new bytes(0); + _depositETH(true, params); + } + + function testDepositETHWithRecipientWithRouter(DepositParams memory params) external { + params.methodType = 1; + params.dataToCall = new bytes(0); + _depositETH(true, params); + } + + function testDepositETHAndCallWithRouter(DepositParams memory params) external { + params.methodType = 2; + _depositETH(true, params); + } + + function testFinalizeWithdrawETH( + address sender, + address target, + uint256 amount, + bytes memory dataToCall + ) external { + vm.assume(target != address(0)); + amount = bound(amount, 1, type(uint128).max); + + // revert when ErrorCallerIsNotMessenger + vm.expectRevert(IScrollGateway.ErrorCallerIsNotMessenger.selector); + l1GasTokenGateway.finalizeWithdrawETH(sender, target, amount, dataToCall); + + MockScrollMessenger mockMessenger = new MockScrollMessenger(); + admin.upgrade( + ITransparentUpgradeableProxy(address(l1GasTokenGateway)), + address( + new L1GasTokenGatewayForTest( + address(gasToken), + address(l2ETHGateway), + address(l1Router), + address(mockMessenger) + ) + ) + ); + + bytes memory message = abi.encodeCall( + L1GasTokenGateway.finalizeWithdrawETH, + (sender, target, amount, dataToCall) + ); + // revert when ErrorCallerIsNotCounterpartGateway + vm.expectRevert(IScrollGateway.ErrorCallerIsNotCounterpartGateway.selector); + mockMessenger.callTarget(address(l1GasTokenGateway), message); + + // revert when reentrant + mockMessenger.setXDomainMessageSender(address(l2ETHGateway)); + vm.expectRevert("ReentrancyGuard: reentrant call"); + L1GasTokenGatewayForTest(address(l1GasTokenGateway)).reentrantCall( + address(mockMessenger), + abi.encodeCall(mockMessenger.callTarget, (address(l1GasTokenGateway), message)) + ); + + // revert when ErrorNonZeroMsgValue + vm.expectRevert(L1GasTokenGateway.ErrorNonZeroMsgValue.selector); + mockMessenger.callTarget{value: 1}(address(l1GasTokenGateway), message); + + admin.upgrade( + ITransparentUpgradeableProxy(address(l1GasTokenGateway)), + address( + new L1GasTokenGatewayForTest( + address(gasToken), + address(l2ETHGateway), + address(l1Router), + address(l1Messenger) + ) + ) + ); + + // succeed when finalize + uint256 scaledAmount = amount / tokenScale; + gasToken.mint(address(l1GasTokenGateway), type(uint128).max); + MockGatewayRecipient recipient = new MockGatewayRecipient(); + message = abi.encodeCall( + L1GasTokenGateway.finalizeWithdrawETH, + (sender, address(recipient), amount, dataToCall) + ); + bytes32 messageHash = keccak256( + encodeXDomainCalldata(address(l2ETHGateway), address(l1GasTokenGateway), 0, 0, message) + ); + prepareFinalizedBatch(messageHash); + IL1ScrollMessenger.L2MessageProof memory proof; + proof.batchIndex = rollup.lastFinalizedBatchIndex(); + + // should emit FinalizeWithdrawETH from L1GasTokenGateway + { + vm.expectEmit(true, true, true, true); + emit FinalizeWithdrawETH(sender, address(recipient), scaledAmount, dataToCall); + } + // should emit RelayedMessage from L1ScrollMessenger + { + vm.expectEmit(true, false, false, true); + emit RelayedMessage(messageHash); + } + + uint256 gatewayBalance = gasToken.balanceOf(address(l1GasTokenGateway)); + uint256 recipientBalance = gasToken.balanceOf(address(recipient)); + assertEq(false, l1Messenger.isL2MessageExecuted(messageHash)); + l1Messenger.relayMessageWithProof(address(l2ETHGateway), address(l1GasTokenGateway), 0, 0, message, proof); + assertEq(true, l1Messenger.isL2MessageExecuted(messageHash)); + assertEq(recipientBalance + scaledAmount, gasToken.balanceOf(address(recipient))); + assertEq(gatewayBalance - scaledAmount, gasToken.balanceOf(address(l1GasTokenGateway))); + } + + function testDropMessage(uint256 amount, address recipient) external { + vm.assume(recipient != address(0)); + + amount = bound(amount, 1, gasToken.balanceOf(address(this))); + uint256 scaledAmount = amount * tokenScale; + bytes memory message = abi.encodeCall( + IL2ETHGateway.finalizeDepositETH, + (address(this), recipient, scaledAmount, new bytes(0)) + ); + l1GasTokenGateway.depositETH(recipient, amount, 1000000); + + // revert when ErrorCallerIsNotMessenger + vm.expectRevert(IScrollGateway.ErrorCallerIsNotMessenger.selector); + l1GasTokenGateway.onDropMessage(message); + + MockScrollMessenger mockMessenger = new MockScrollMessenger(); + admin.upgrade( + ITransparentUpgradeableProxy(address(l1GasTokenGateway)), + address( + new L1GasTokenGatewayForTest( + address(gasToken), + address(l2ETHGateway), + address(l1Router), + address(mockMessenger) + ) + ) + ); + + // revert not in drop context + vm.expectRevert(IScrollGateway.ErrorNotInDropMessageContext.selector); + mockMessenger.callTarget( + address(l1GasTokenGateway), + abi.encodeCall(l1GasTokenGateway.onDropMessage, (message)) + ); + + // revert when reentrant + mockMessenger.setXDomainMessageSender(ScrollConstants.DROP_XDOMAIN_MESSAGE_SENDER); + vm.expectRevert("ReentrancyGuard: reentrant call"); + L1GasTokenGatewayForTest(address(l1GasTokenGateway)).reentrantCall( + address(mockMessenger), + abi.encodeCall( + mockMessenger.callTarget, + (address(l1GasTokenGateway), abi.encodeCall(l1GasTokenGateway.onDropMessage, (message))) + ) + ); + + // revert when invalid selector + vm.expectRevert(L1GasTokenGateway.ErrorInvalidSelector.selector); + mockMessenger.callTarget( + address(l1GasTokenGateway), + abi.encodeCall(l1GasTokenGateway.onDropMessage, (new bytes(4))) + ); + + admin.upgrade( + ITransparentUpgradeableProxy(address(l1GasTokenGateway)), + address( + new L1GasTokenGatewayForTest( + address(gasToken), + address(l2ETHGateway), + address(l1Router), + address(l1Messenger) + ) + ) + ); + + // succeed on drop + // skip message 0 + vm.startPrank(address(rollup)); + l1MessageQueue.popCrossDomainMessage(0, 1, 0x1); + assertEq(l1MessageQueue.pendingQueueIndex(), 1); + vm.stopPrank(); + + // should emit RefundERC20 + vm.expectEmit(true, true, false, true); + emit RefundETH(address(this), amount); + + uint256 balance = gasToken.balanceOf(address(this)); + uint256 gatewayBalance = gasToken.balanceOf(address(l1GasTokenGateway)); + l1Messenger.dropMessage(address(l1GasTokenGateway), address(l2ETHGateway), scaledAmount, 0, message); + assertEq(gatewayBalance - amount, gasToken.balanceOf(address(l1GasTokenGateway))); + assertEq(balance + amount, gasToken.balanceOf(address(this))); + } + + function testRelayFromL1ToL2(uint256 l1Amount, address recipient) external { + vm.assume(recipient.code.length == 0); // only refund to EOA to avoid revert + vm.assume(uint256(uint160(recipient)) > 2**152); // ignore some precompile contracts + vm.recordLogs(); + + l1Amount = bound(l1Amount, 1, gasToken.balanceOf(address(this))); + uint256 l2Amount = l1Amount * tokenScale; + + l1GasTokenGateway.depositETH(recipient, l1Amount, 1000000); + + uint256 recipientBalance = recipient.balance; + uint256 l2MessengerBalance = address(l2Messenger).balance; + relayFromL1(); + assertEq(recipientBalance + l2Amount, recipient.balance); + assertEq(l2MessengerBalance - l2Amount, address(l2Messenger).balance); + } + + function testRelayFromL2ToL1(uint256 l2Amount, address recipient) external { + vm.assume(recipient != address(0)); + vm.recordLogs(); + + l2Amount = bound(l2Amount, 1, address(this).balance); + uint256 l1Amount = l2Amount / tokenScale; + + gasToken.mint(address(l1GasTokenGateway), type(uint128).max); + l2ETHGateway.withdrawETH{value: l2Amount}(recipient, l2Amount, 1000000); + + uint256 recipientBalance = gasToken.balanceOf(recipient); + uint256 gatewayBalance = gasToken.balanceOf(address(l1GasTokenGateway)); + relayFromL2(); + assertEq(recipientBalance + l1Amount, gasToken.balanceOf(recipient)); + assertEq(gatewayBalance - l1Amount, gasToken.balanceOf(address(l1GasTokenGateway))); + } + + function _depositETH(bool useRouter, DepositParams memory params) private { + vm.assume(params.recipient != address(0)); + + params.amount = bound(params.amount, 1, gasToken.balanceOf(address(this))); + uint256 scaledAmount = params.amount * tokenScale; + + bytes memory message = abi.encodeCall( + IL2ETHGateway.finalizeDepositETH, + (address(this), params.recipient, scaledAmount, params.dataToCall) + ); + bytes memory xDomainCalldata = encodeXDomainCalldata( + address(l1GasTokenGateway), + address(l2ETHGateway), + scaledAmount, + 0, + message + ); + + params.gasLimit = bound(params.gasLimit, xDomainCalldata.length * 16 + 21000, 1000000); + params.feeToPay = bound(params.feeToPay, 0, 1 ether); + params.exceedValue = bound(params.exceedValue, 0, 1 ether); + + l1MessageQueue.setL2BaseFee(params.feeToPay); + params.feeToPay = params.feeToPay * params.gasLimit; + + // revert when reentrant + { + bytes memory reentrantData; + if (params.methodType == 0) { + reentrantData = abi.encodeWithSignature("depositETH(uint256,uint256)", params.amount, params.gasLimit); + } else if (params.methodType == 1) { + reentrantData = abi.encodeWithSignature( + "depositETH(address,uint256,uint256)", + params.recipient, + params.amount, + params.gasLimit + ); + } else if (params.methodType == 2) { + reentrantData = abi.encodeCall( + l1GasTokenGateway.depositETHAndCall, + (params.recipient, params.amount, params.dataToCall, params.gasLimit) + ); + } + vm.expectRevert("ReentrancyGuard: reentrant call"); + L1GasTokenGatewayForTest(address(l1GasTokenGateway)).reentrantCall( + useRouter ? address(l1Router) : address(l1GasTokenGateway), + reentrantData + ); + } + + // revert when ErrorDepositZeroGasToken + { + uint256 amount = params.amount; + params.amount = 0; + vm.expectRevert(L1GasTokenGateway.ErrorDepositZeroGasToken.selector); + _invokeDepositETHCall(useRouter, params); + params.amount = amount; + } + + // succeed to deposit + // should emit QueueTransaction from L1MessageQueue + { + vm.expectEmit(true, true, false, true); + address sender = AddressAliasHelper.applyL1ToL2Alias(address(l1Messenger)); + emit QueueTransaction(sender, address(l2Messenger), 0, 0, params.gasLimit, xDomainCalldata); + } + // should emit SentMessage from L1ScrollMessenger + { + vm.expectEmit(true, true, false, true); + emit SentMessage( + address(l1GasTokenGateway), + address(l2ETHGateway), + scaledAmount, + 0, + params.gasLimit, + message + ); + } + // should emit DepositERC20 from L1CustomERC20Gateway + { + vm.expectEmit(true, true, false, true); + emit DepositETH(address(this), params.recipient, params.amount, params.dataToCall); + } + + uint256 gatewayBalance = gasToken.balanceOf(address(l1GasTokenGateway)); + uint256 feeVaultBalance = l1FeeVault.balance; + uint256 thisBalance = gasToken.balanceOf(address(this)); + assertEq(l1Messenger.messageSendTimestamp(keccak256(xDomainCalldata)), 0); + uint256 balance = address(this).balance; + _invokeDepositETHCall(useRouter, params); + assertEq(balance - params.feeToPay, address(this).balance); // extra value is transferred back + assertEq(l1Messenger.messageSendTimestamp(keccak256(xDomainCalldata)), NONZERO_TIMESTAMP); + assertEq(thisBalance - params.amount, gasToken.balanceOf(address(this))); + assertEq(feeVaultBalance + params.feeToPay, l1FeeVault.balance); + assertEq(gatewayBalance + params.amount, gasToken.balanceOf(address(l1GasTokenGateway))); + } + + function _invokeDepositETHCall(bool useRouter, DepositParams memory params) private { + uint256 value = params.feeToPay + params.exceedValue; + if (useRouter) { + if (params.methodType == 0) { + l1Router.depositETH{value: value}(params.amount, params.gasLimit); + } else if (params.methodType == 1) { + l1Router.depositETH{value: value}(params.recipient, params.amount, params.gasLimit); + } else if (params.methodType == 2) { + l1Router.depositETHAndCall{value: value}( + params.recipient, + params.amount, + params.dataToCall, + params.gasLimit + ); + } + } else { + if (params.methodType == 0) { + l1GasTokenGateway.depositETH{value: value}(params.amount, params.gasLimit); + } else if (params.methodType == 1) { + l1GasTokenGateway.depositETH{value: value}(params.recipient, params.amount, params.gasLimit); + } else if (params.methodType == 2) { + l1GasTokenGateway.depositETHAndCall{value: value}( + params.recipient, + params.amount, + params.dataToCall, + params.gasLimit + ); + } + } + } +} + +contract GasTokenDecimal18GatewayTest is GasTokenGatewayTest { + function setUp() external { + __GasTokenGatewayTest_setUp(18); + } +} + +contract GasTokenDecimal8GatewayTest is GasTokenGatewayTest { + function setUp() external { + __GasTokenGatewayTest_setUp(8); + } +} + +contract GasTokenDecimal6GatewayTest is GasTokenGatewayTest { + function setUp() external { + __GasTokenGatewayTest_setUp(6); + } +} diff --git a/src/test/alternative-gas-token/L1ScrollMessengerNonETH.t.sol b/src/test/alternative-gas-token/L1ScrollMessengerNonETH.t.sol new file mode 100644 index 0000000..19cd028 --- /dev/null +++ b/src/test/alternative-gas-token/L1ScrollMessengerNonETH.t.sol @@ -0,0 +1,486 @@ +// SPDX-License-Identifier: MIT + +pragma solidity =0.8.24; + +import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; + +import {ITransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +import {L1ScrollMessengerNonETH} from "../../alternative-gas-token/L1ScrollMessengerNonETH.sol"; +import {IL1ScrollMessenger} from "../../L1/IL1ScrollMessenger.sol"; +import {AddressAliasHelper} from "../../libraries/common/AddressAliasHelper.sol"; +import {ScrollConstants} from "../../libraries/constants/ScrollConstants.sol"; + +import {AlternativeGasTokenTestBase} from "./AlternativeGasTokenTestBase.t.sol"; + +contract L1ScrollMessengerNonETHForTest is L1ScrollMessengerNonETH { + constructor( + address _nativeTokenGateway, + address _counterpart, + address _rollup, + address _messageQueue + ) L1ScrollMessengerNonETH(_nativeTokenGateway, _counterpart, _rollup, _messageQueue) {} + + function setMessageSendTimestamp(bytes32 hash, uint256 value) external { + messageSendTimestamp[hash] = value; + } +} + +contract L1ScrollMessengerNonETHTest is AlternativeGasTokenTestBase { + event OnDropMessageCalled(uint256, bytes); + + event OnRelayMessageWithProof(uint256, bytes); + + MockERC20 private gasToken; + + receive() external payable {} + + function setUp() external { + gasToken = new MockERC20("X", "Y", 18); + + __AlternativeGasTokenTestBase_setUp(1234, address(gasToken)); + } + + function testInitialization() external view { + assertEq(l1Messenger.nativeTokenGateway(), address(l1GasTokenGateway)); + assertEq(l1Messenger.messageQueue(), address(l1MessageQueue)); + assertEq(l1Messenger.rollup(), address(rollup)); + } + + function testSendMessageRevertOnErrorNonZeroValueFromCaller(uint256 value) external { + vm.assume(value > 0); + // revert ErrorNonZeroValueFromCaller + vm.expectRevert(L1ScrollMessengerNonETH.ErrorNonZeroValueFromCaller.selector); + l1Messenger.sendMessage(address(0), value, new bytes(0), 0); + } + + function testSendMessageRevertOnErrorInsufficientMsgValue( + uint256 l2BaseFee, + uint256 gasLimit, + bytes memory message + ) external { + bytes memory encoded = encodeXDomainCalldata(address(this), address(0), 0, 0, message); + vm.assume(encoded.length < 60000); + gasLimit = bound(gasLimit, encoded.length * 16 + 21000, 1000000); + l2BaseFee = bound(l2BaseFee, 1, 1 ether); + + l1MessageQueue.setL2BaseFee(l2BaseFee); + + // revert ErrorInsufficientMsgValue + vm.expectRevert(L1ScrollMessengerNonETH.ErrorInsufficientMsgValue.selector); + l1Messenger.sendMessage{value: gasLimit * l2BaseFee - 1}(address(0), 0, message, gasLimit); + } + + function testSendMessageRevertOnErrorDuplicatedMessage( + address target, + uint256 gasLimit, + bytes memory message + ) external { + bytes memory encoded = encodeXDomainCalldata(address(this), target, 0, 0, message); + vm.assume(encoded.length < 60000); + gasLimit = bound(gasLimit, encoded.length * 16 + 21000, 1000000); + + admin.upgrade( + ITransparentUpgradeableProxy(address(l1Messenger)), + address( + new L1ScrollMessengerNonETHForTest( + address(l1GasTokenGateway), + address(l2Messenger), + address(rollup), + address(l1MessageQueue) + ) + ) + ); + L1ScrollMessengerNonETHForTest(payable(address(l1Messenger))).setMessageSendTimestamp(keccak256(encoded), 1); + l1MessageQueue.setL2BaseFee(0); + + // revert ErrorDuplicatedMessage + vm.expectRevert(L1ScrollMessengerNonETH.ErrorDuplicatedMessage.selector); + l1Messenger.sendMessage(target, 0, message, gasLimit); + } + + function testSendMessage( + uint256 l2BaseFee, + address target, + uint256 gasLimit, + bytes memory message, + uint256 exceedValue, + address refundAddress + ) external { + vm.assume(refundAddress.code.length == 0); // only refund to EOA to avoid revert + vm.assume(uint256(uint160(refundAddress)) > 2**152); // ignore some precompile contracts + vm.assume(refundAddress != l1FeeVault); + + uint256 NONZERO_TIMESTAMP = 123456; + vm.warp(NONZERO_TIMESTAMP); + + bytes memory encoded0 = encodeXDomainCalldata(address(this), target, 0, 0, message); + bytes memory encoded1 = encodeXDomainCalldata(address(this), target, 0, 1, message); + bytes memory encoded2 = encodeXDomainCalldata(address(this), target, 0, 2, message); + vm.assume(encoded0.length < 60000); + + gasLimit = bound(gasLimit, encoded0.length * 16 + 21000, 1000000); + exceedValue = bound(exceedValue, 1, address(this).balance / 2); + l2BaseFee = bound(l2BaseFee, 1, 1 ether); + + l1MessageQueue.setL2BaseFee(l2BaseFee); + + assertEq(l1MessageQueue.nextCrossDomainMessageIndex(), 0); + + // send message 0, exact fee + // emit QueueTransaction from L1MessageQueue + { + vm.expectEmit(true, true, false, true); + address sender = AddressAliasHelper.applyL1ToL2Alias(address(l1Messenger)); + emit QueueTransaction(sender, address(l2Messenger), 0, 0, gasLimit, encoded0); + } + // emit SentMessage from L1ScrollMessengerNonETH + { + vm.expectEmit(true, true, false, true); + emit SentMessage(address(this), target, 0, 0, gasLimit, message); + } + uint256 thisBalance = address(this).balance; + assertEq(l1Messenger.messageSendTimestamp(keccak256(encoded0)), 0); + l1Messenger.sendMessage{value: gasLimit * l2BaseFee}(target, 0, message, gasLimit); + assertEq(address(this).balance, thisBalance - gasLimit * l2BaseFee); + assertEq(l1MessageQueue.nextCrossDomainMessageIndex(), 1); + assertEq(l1Messenger.messageSendTimestamp(keccak256(encoded0)), NONZERO_TIMESTAMP); + + // send message 1, over fee, refund to self + // emit QueueTransaction from L1MessageQueue + { + vm.expectEmit(true, true, false, true); + address sender = AddressAliasHelper.applyL1ToL2Alias(address(l1Messenger)); + emit QueueTransaction(sender, address(l2Messenger), 0, 1, gasLimit, encoded1); + } + // emit SentMessage from L1ScrollMessengerNonETH + { + vm.expectEmit(true, true, false, true); + emit SentMessage(address(this), target, 0, 1, gasLimit, message); + } + thisBalance = address(this).balance; + assertEq(l1Messenger.messageSendTimestamp(keccak256(encoded1)), 0); + l1Messenger.sendMessage{value: gasLimit * l2BaseFee + exceedValue}(target, 0, message, gasLimit); + assertEq(address(this).balance, thisBalance - gasLimit * l2BaseFee); + assertEq(l1MessageQueue.nextCrossDomainMessageIndex(), 2); + assertEq(l1Messenger.messageSendTimestamp(keccak256(encoded1)), NONZERO_TIMESTAMP); + + // send message 2, over fee, refund to other + // emit QueueTransaction from L1MessageQueue + { + vm.expectEmit(true, true, false, true); + address sender = AddressAliasHelper.applyL1ToL2Alias(address(l1Messenger)); + emit QueueTransaction(sender, address(l2Messenger), 0, 2, gasLimit, encoded2); + } + // emit SentMessage from L1ScrollMessengerNonETH + { + vm.expectEmit(true, true, false, true); + emit SentMessage(address(this), target, 0, 2, gasLimit, message); + } + thisBalance = address(this).balance; + uint256 refundBalance = refundAddress.balance; + assertEq(l1Messenger.messageSendTimestamp(keccak256(encoded2)), 0); + l1Messenger.sendMessage{value: gasLimit * l2BaseFee + exceedValue}(target, 0, message, gasLimit, refundAddress); + assertEq(address(this).balance, thisBalance - gasLimit * l2BaseFee - exceedValue); + assertEq(refundAddress.balance, refundBalance + exceedValue); + assertEq(l1MessageQueue.nextCrossDomainMessageIndex(), 3); + assertEq(l1Messenger.messageSendTimestamp(keccak256(encoded2)), NONZERO_TIMESTAMP); + } + + function testRelayMessageWithProofRevertOnErrorNonZeroValueFromCrossDomainCaller( + address sender, + address target, + uint256 value, + uint256 nonce, + bytes memory message, + IL1ScrollMessenger.L2MessageProof memory proof + ) external { + vm.assume(value > 0); + vm.assume(target != address(l1GasTokenGateway)); + + // revert ErrorNonZeroValueFromCrossDomainCaller + vm.expectRevert(L1ScrollMessengerNonETH.ErrorNonZeroValueFromCrossDomainCaller.selector); + l1Messenger.relayMessageWithProof(sender, target, value, nonce, message, proof); + } + + function testRelayMessageWithProofRevertOnErrorMessageExecuted( + address sender, + address target, + uint256 nonce, + bytes memory message + ) external { + vm.assume(target.code.length == 0); // only refund to EOA to avoid revert + vm.assume(uint256(uint160(target)) > 2**152); // ignore some precompile contracts + + prepareFinalizedBatch(keccak256(encodeXDomainCalldata(sender, target, 0, nonce, message))); + IL1ScrollMessenger.L2MessageProof memory proof; + proof.batchIndex = rollup.lastFinalizedBatchIndex(); + + l1Messenger.relayMessageWithProof(sender, target, 0, nonce, message, proof); + + // revert ErrorMessageExecuted + vm.expectRevert(L1ScrollMessengerNonETH.ErrorMessageExecuted.selector); + l1Messenger.relayMessageWithProof(sender, target, 0, nonce, message, proof); + } + + function testRelayMessageWithProofRevertOnErrorBatchNotFinalized( + address sender, + address target, + uint256 nonce, + bytes memory message + ) external { + vm.assume(target.code.length == 0); // only refund to EOA to avoid revert + vm.assume(uint256(uint160(target)) > 2**152); // ignore some precompile contracts + + prepareFinalizedBatch(keccak256(encodeXDomainCalldata(sender, target, 0, nonce, message))); + IL1ScrollMessenger.L2MessageProof memory proof; + proof.batchIndex = rollup.lastFinalizedBatchIndex() + 1; + + // revert ErrorBatchNotFinalized + vm.expectRevert(L1ScrollMessengerNonETH.ErrorBatchNotFinalized.selector); + l1Messenger.relayMessageWithProof(sender, target, 0, nonce, message, proof); + } + + function testRelayMessageWithProofRevertOnErrorInvalidMerkleProof( + address sender, + address target, + uint256 nonce, + bytes memory message, + IL1ScrollMessenger.L2MessageProof memory proof + ) external { + vm.assume(target.code.length == 0); // only refund to EOA to avoid revert + vm.assume(uint256(uint160(target)) > 2**152); // ignore some precompile contracts + vm.assume(proof.merkleProof.length > 0); + vm.assume(proof.merkleProof.length % 32 == 0); + + prepareFinalizedBatch(keccak256(encodeXDomainCalldata(sender, target, 0, nonce, message))); + proof.batchIndex = rollup.lastFinalizedBatchIndex(); + + // revert ErrorInvalidMerkleProof + vm.expectRevert(L1ScrollMessengerNonETH.ErrorInvalidMerkleProof.selector); + l1Messenger.relayMessageWithProof(sender, target, 0, nonce, message, proof); + } + + function testRelayMessageWithProofRevertOnErrorForbidToCallMessageQueue( + address sender, + uint256 nonce, + bytes memory message + ) external { + address target = address(l1MessageQueue); + + prepareFinalizedBatch(keccak256(encodeXDomainCalldata(sender, target, 0, nonce, message))); + IL1ScrollMessenger.L2MessageProof memory proof; + proof.batchIndex = rollup.lastFinalizedBatchIndex(); + + // revert ErrorForbidToCallMessageQueue + vm.expectRevert(L1ScrollMessengerNonETH.ErrorForbidToCallMessageQueue.selector); + l1Messenger.relayMessageWithProof(sender, target, 0, nonce, message, proof); + } + + function testRelayMessageWithProofRevertOnCallSelfFromL2( + address sender, + uint256 nonce, + bytes memory message + ) external { + address target = address(l1Messenger); + + prepareFinalizedBatch(keccak256(encodeXDomainCalldata(sender, target, 0, nonce, message))); + IL1ScrollMessenger.L2MessageProof memory proof; + proof.batchIndex = rollup.lastFinalizedBatchIndex(); + + // revert when call self + vm.expectRevert("Forbid to call self"); + l1Messenger.relayMessageWithProof(sender, target, 0, nonce, message, proof); + } + + function testRelayMessageWithProofRevertOnErrorInvalidMessageSender( + address target, + uint256 nonce, + bytes memory message + ) external { + vm.assume(target.code.length == 0); // only refund to EOA to avoid revert + vm.assume(uint256(uint160(target)) > 2**152); // ignore some precompile contracts + address sender = ScrollConstants.DEFAULT_XDOMAIN_MESSAGE_SENDER; + + prepareFinalizedBatch(keccak256(encodeXDomainCalldata(sender, target, 0, nonce, message))); + IL1ScrollMessenger.L2MessageProof memory proof; + proof.batchIndex = rollup.lastFinalizedBatchIndex(); + + // revert ErrorInvalidMessageSender + vm.expectRevert(L1ScrollMessengerNonETH.ErrorInvalidMessageSender.selector); + l1Messenger.relayMessageWithProof(sender, target, 0, nonce, message, proof); + } + + bool revertOnRelayMessageWithProof; + + function onRelayMessageWithProof(bytes memory message) external payable { + emit OnRelayMessageWithProof(msg.value, message); + + if (revertOnRelayMessageWithProof) revert(); + } + + function testRelayMessageWithProofFailed( + address sender, + uint256 nonce, + bytes memory message + ) external { + vm.assume(sender != ScrollConstants.DEFAULT_XDOMAIN_MESSAGE_SENDER); + + revertOnRelayMessageWithProof = true; + bytes memory encoded = abi.encodeCall(L1ScrollMessengerNonETHTest.onRelayMessageWithProof, (message)); + address target = address(this); + + bytes32 hash = keccak256(encodeXDomainCalldata(sender, target, 0, nonce, encoded)); + prepareFinalizedBatch(hash); + IL1ScrollMessenger.L2MessageProof memory proof; + proof.batchIndex = rollup.lastFinalizedBatchIndex(); + + assertEq(l1Messenger.isL2MessageExecuted(hash), false); + vm.expectEmit(true, false, false, true); + emit FailedRelayedMessage(hash); + l1Messenger.relayMessageWithProof(sender, target, 0, nonce, encoded, proof); + assertEq(l1Messenger.isL2MessageExecuted(hash), false); + } + + function testRelayMessageWithProofSucceed( + address sender, + uint256 nonce, + bytes memory message + ) external { + vm.assume(sender != ScrollConstants.DEFAULT_XDOMAIN_MESSAGE_SENDER); + + revertOnRelayMessageWithProof = false; + bytes memory encoded = abi.encodeCall(L1ScrollMessengerNonETHTest.onRelayMessageWithProof, (message)); + address target = address(this); + + bytes32 hash = keccak256(encodeXDomainCalldata(sender, target, 0, nonce, encoded)); + prepareFinalizedBatch(hash); + IL1ScrollMessenger.L2MessageProof memory proof; + proof.batchIndex = rollup.lastFinalizedBatchIndex(); + + assertEq(l1Messenger.isL2MessageExecuted(hash), false); + vm.expectEmit(false, false, false, true); + emit OnRelayMessageWithProof(0, message); + vm.expectEmit(true, false, false, true); + emit RelayedMessage(hash); + l1Messenger.relayMessageWithProof(sender, target, 0, nonce, encoded, proof); + assertEq(l1Messenger.isL2MessageExecuted(hash), true); + } + + function onDropMessage(bytes memory message) external payable { + emit OnDropMessageCalled(msg.value, message); + } + + function testDropMessageRevertOnErrorMessageNotEnqueued( + address sender, + address target, + uint256 value, + uint256 messageNonce, + bytes memory message + ) external { + // revert on ErrorMessageNotEnqueued + vm.expectRevert(L1ScrollMessengerNonETH.ErrorMessageNotEnqueued.selector); + l1Messenger.dropMessage(sender, target, value, messageNonce, message); + } + + function testDropMessage( + address target, + bytes memory message, + uint32 gasLimit + ) external { + bytes memory encoded = encodeXDomainCalldata(address(this), target, 0, 0, message); + vm.assume(encoded.length < 60000); + gasLimit = uint32(bound(gasLimit, encoded.length * 16 + 21000, 1000000)); + + l1MessageQueue.setL2BaseFee(0); + + // send one message with nonce 0 + l1Messenger.sendMessage(target, 0, message, gasLimit); + assertEq(l1MessageQueue.nextCrossDomainMessageIndex(), 1); + + // drop pending message, revert + vm.expectRevert("cannot drop pending message"); + l1Messenger.dropMessage(address(this), target, 0, 0, message); + + l1Messenger.updateMaxReplayTimes(10); + + // replay 1 time + l1Messenger.replayMessage(address(this), target, 0, 0, message, gasLimit, address(0)); + assertEq(l1MessageQueue.nextCrossDomainMessageIndex(), 2); + + // skip all 2 messages + vm.startPrank(address(rollup)); + l1MessageQueue.popCrossDomainMessage(0, 2, 0x3); + assertEq(l1MessageQueue.pendingQueueIndex(), 2); + vm.stopPrank(); + for (uint256 i = 0; i < 2; ++i) { + assertEq(l1MessageQueue.isMessageSkipped(i), true); + assertEq(l1MessageQueue.isMessageDropped(i), false); + } + vm.expectEmit(false, false, false, true); + emit OnDropMessageCalled(0, message); + l1Messenger.dropMessage(address(this), target, 0, 0, message); + for (uint256 i = 0; i < 2; ++i) { + assertEq(l1MessageQueue.isMessageSkipped(i), true); + assertEq(l1MessageQueue.isMessageDropped(i), true); + } + + // send one message with nonce 2 and replay 3 times + l1Messenger.sendMessage(target, 0, message, gasLimit); + assertEq(l1MessageQueue.nextCrossDomainMessageIndex(), 3); + for (uint256 i = 0; i < 3; i++) { + l1Messenger.replayMessage(address(this), target, 0, 2, message, gasLimit, address(0)); + } + assertEq(l1MessageQueue.nextCrossDomainMessageIndex(), 6); + + // only first 3 are skipped + vm.startPrank(address(rollup)); + l1MessageQueue.popCrossDomainMessage(2, 4, 0x7); + assertEq(l1MessageQueue.pendingQueueIndex(), 6); + vm.stopPrank(); + for (uint256 i = 2; i < 6; i++) { + assertEq(l1MessageQueue.isMessageSkipped(i), i < 5); + assertEq(l1MessageQueue.isMessageDropped(i), false); + } + + // drop non-skipped message, revert + vm.expectRevert("drop non-skipped message"); + l1Messenger.dropMessage(address(this), target, 0, 2, message); + + // send one message with nonce 6 and replay 4 times + l1Messenger.sendMessage(target, 0, message, gasLimit); + for (uint256 i = 0; i < 4; i++) { + l1Messenger.replayMessage(address(this), target, 0, 6, message, gasLimit, address(0)); + } + assertEq(l1MessageQueue.nextCrossDomainMessageIndex(), 11); + + // skip all 5 messages + vm.startPrank(address(rollup)); + l1MessageQueue.popCrossDomainMessage(6, 5, 0x1f); + assertEq(l1MessageQueue.pendingQueueIndex(), 11); + vm.stopPrank(); + for (uint256 i = 6; i < 11; ++i) { + assertEq(l1MessageQueue.isMessageSkipped(i), true); + assertEq(l1MessageQueue.isMessageDropped(i), false); + } + vm.expectEmit(false, false, false, true); + emit OnDropMessageCalled(0, message); + l1Messenger.dropMessage(address(this), target, 0, 6, message); + for (uint256 i = 6; i < 11; ++i) { + assertEq(l1MessageQueue.isMessageSkipped(i), true); + assertEq(l1MessageQueue.isMessageDropped(i), true); + } + + // Message already dropped, revert + vm.expectRevert(L1ScrollMessengerNonETH.ErrorMessageDropped.selector); + l1Messenger.dropMessage(address(this), target, 0, 0, message); + vm.expectRevert(L1ScrollMessengerNonETH.ErrorMessageDropped.selector); + l1Messenger.dropMessage(address(this), target, 0, 6, message); + + // replay dropped message, revert + vm.expectRevert("Message already dropped"); + l1Messenger.replayMessage(address(this), target, 0, 0, message, gasLimit, address(0)); + vm.expectRevert("Message already dropped"); + l1Messenger.replayMessage(address(this), target, 0, 6, message, gasLimit, address(0)); + } +} diff --git a/src/test/alternative-gas-token/L1WrappedTokenGateway.t.sol b/src/test/alternative-gas-token/L1WrappedTokenGateway.t.sol new file mode 100644 index 0000000..00c4774 --- /dev/null +++ b/src/test/alternative-gas-token/L1WrappedTokenGateway.t.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: MIT + +pragma solidity =0.8.24; + +import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; +import {WETH} from "solmate/tokens/WETH.sol"; + +import {ITransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +import {L1WrappedTokenGateway} from "../../alternative-gas-token/L1WrappedTokenGateway.sol"; +import {L1StandardERC20Gateway} from "../../L1/gateways/L1StandardERC20Gateway.sol"; +import {L2StandardERC20Gateway} from "../../L2/gateways/L2StandardERC20Gateway.sol"; +import {ScrollStandardERC20} from "../../libraries/token/ScrollStandardERC20.sol"; +import {ScrollStandardERC20Factory} from "../../libraries/token/ScrollStandardERC20Factory.sol"; + +import {AlternativeGasTokenTestBase} from "./AlternativeGasTokenTestBase.t.sol"; + +contract L1WrappedTokenGatewayTest is AlternativeGasTokenTestBase { + event OnDropMessageCalled(uint256, bytes); + + event OnRelayMessageWithProof(uint256, bytes); + + MockERC20 private gasToken; + + ScrollStandardERC20 private template; + ScrollStandardERC20Factory private factory; + + L1StandardERC20Gateway private l1ERC20Gateway; + L2StandardERC20Gateway private l2ERC20Gateway; + + WETH private weth; + L1WrappedTokenGateway private gateway; + + receive() external payable {} + + function setUp() external { + gasToken = new MockERC20("X", "Y", 18); + + __AlternativeGasTokenTestBase_setUp(1234, address(gasToken)); + + template = new ScrollStandardERC20(); + factory = new ScrollStandardERC20Factory(address(template)); + l1ERC20Gateway = L1StandardERC20Gateway(_deployProxy(address(0))); + l2ERC20Gateway = L2StandardERC20Gateway(_deployProxy(address(0))); + + admin.upgrade( + ITransparentUpgradeableProxy(address(l1ERC20Gateway)), + address( + new L1StandardERC20Gateway( + address(l2ERC20Gateway), + address(l1Router), + address(l1Messenger), + address(template), + address(factory) + ) + ) + ); + admin.upgrade( + ITransparentUpgradeableProxy(address(l2ERC20Gateway)), + address( + new L2StandardERC20Gateway( + address(l1ERC20Gateway), + address(l2Router), + address(l2Messenger), + address(factory) + ) + ) + ); + + weth = new WETH(); + gateway = new L1WrappedTokenGateway(address(weth), address(l1ERC20Gateway)); + } + + function testInitialization() external view { + assertEq(gateway.WETH(), address(weth)); + assertEq(gateway.gateway(), address(l1ERC20Gateway)); + assertEq(gateway.sender(), address(1)); + } + + function testReceive(uint256 amount) external { + amount = bound(amount, 0, address(this).balance); + + vm.expectRevert(L1WrappedTokenGateway.ErrorCallNotFromFeeRefund.selector); + payable(address(gateway)).transfer(amount); + } + + function testDeposit( + uint256 amount, + address recipient, + uint256 l2BaseFee, + uint256 exceedValue + ) external { + amount = bound(amount, 1, address(this).balance / 2); + l2BaseFee = bound(l2BaseFee, 0, 10**9); + exceedValue = bound(exceedValue, 0, 1 ether); + + l1MessageQueue.setL2BaseFee(l2BaseFee); + uint256 fee = l2BaseFee * 200000; + + uint256 ethBalance = address(this).balance; + uint256 wethBalance = weth.balanceOf(address(l1ERC20Gateway)); + gateway.deposit{value: amount + fee + exceedValue}(recipient, amount); + assertEq(ethBalance - amount - fee, address(this).balance); + assertEq(wethBalance + amount, weth.balanceOf(address(l1ERC20Gateway))); + } +}