diff --git a/foundry.toml b/foundry.toml index 741b5bbe..ff1021bd 100644 --- a/foundry.toml +++ b/foundry.toml @@ -7,8 +7,8 @@ libs = ["lib"] remappings = [] # a list of remappings libraries = [] # a list of deployed libraries to link against cache = true # whether to cache builds or not -force = true # whether to ignore the cache (clean build) -# evm_version = 'london' # the evm version (by hardfork name) +force = false # whether to ignore the cache (clean build) +evm_version = 'cancun' # the evm version (by hardfork name) solc_version = '0.8.24' # override for the solc version (setting this ignores `auto_detect_solc`) optimizer = true # enable or disable the solc optimizer optimizer_runs = 200 # the number of optimizer runs diff --git a/src/L1/L1ScrollMessenger.sol b/src/L1/L1ScrollMessenger.sol index 544beb2b..498757ab 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 c268a1ba..a522f2bf 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/L1GasTokenGateway.sol b/src/alternative-gas-token/L1GasTokenGateway.sol new file mode 100644 index 00000000..c571ee31 --- /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 00000000..969e7aa2 --- /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 00000000..12800339 --- /dev/null +++ b/src/alternative-gas-token/L1WrappedTokenGateway.sol @@ -0,0 +1,88 @@ +// 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; + + /********* + * 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 = 200000; + + /// @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; + + // 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 00000000..70692863 --- /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 00000000..1da96451 --- /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 00000000..19cd0285 --- /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 00000000..00c47743 --- /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))); + } +}