From a9e8d35ffdf702a83a952f2b9c2fddc4641aaf88 Mon Sep 17 00:00:00 2001 From: zimpha Date: Mon, 22 Jul 2024 13:35:44 +0800 Subject: [PATCH 01/24] feat: add contracts for alternative gas token design --- src/L1/L1ScrollMessenger.sol | 172 +++++++++------- .../L1GasTokenGateway.sol | 151 ++++++++++++++ .../L1ScrollMessengerNonETH.sol | 187 ++++++++++++++++++ .../L1WrappedTokenGateway.sol | 32 +++ 4 files changed, 468 insertions(+), 74 deletions(-) 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 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/alternative-gas-token/L1GasTokenGateway.sol b/src/alternative-gas-token/L1GasTokenGateway.sol new file mode 100644 index 0000000..0011570 --- /dev/null +++ b/src/alternative-gas-token/L1GasTokenGateway.sol @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: MIT + +pragma solidity =0.8.24; + +import {IERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import {SafeERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.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 `L1ScrollMessenger`. On finalizing withdraw, the corresponding +/// gas token will be transfer to the recipient directly. +contract L1GasTokenGateway is ScrollGatewayBase, IL1ETHGateway, IMessageDropCallback { + using SafeERC20Upgradeable for IERC20Upgradeable; + + /************* + * Constants * + *************/ + + address public immutable gasToken; + + /*************** + * 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; + } + + /// @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 { + require(msg.value == 0, "msg.value mismatch"); + + IERC20Upgradeable(gasToken).safeTransfer(_to, _amount); + _doCallback(_to, _data); + + emit FinalizeWithdrawETH(_from, _to, _amount, _data); + } + + /// @inheritdoc IMessageDropCallback + function onDropMessage(bytes calldata _message) external payable virtual onlyInDropContext nonReentrant { + // _message should start with 0x232e8748 => finalizeDepositETH(address,address,uint256,bytes) + require(bytes4(_message[0:4]) == IL2ETHGateway.finalizeDepositETH.selector, "invalid selector"); + + // decode (receiver, amount) + (address _receiver, , uint256 _amount, ) = abi.decode(_message[4:], (address, address, uint256, bytes)); + + IERC20Upgradeable(gasToken).safeTransfer(_receiver, _amount); + + emit RefundETH(_receiver, _amount); + } + + /********************** + * 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; + require(_amount > 0, "deposit zero gas token"); + + // 3. Generate message passed to L1ScrollMessenger. + bytes memory _message = abi.encodeCall(IL2ETHGateway.finalizeDepositETH, (_from, _to, _amount, _data)); + + IL1ScrollMessenger(messenger).sendMessage{value: msg.value}(counterpart, _amount, _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..764aefa --- /dev/null +++ b/src/alternative-gas-token/L1ScrollMessengerNonETH.sol @@ -0,0 +1,187 @@ +// 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 * + **********/ + + error ErrorDuplicatedMessage(); + + error ErrorNonZeroValueFromCaller(); + + error ErrorInsufficientMsgValue(); + + /************* + * 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 _value, + bytes memory _message, + uint256 _gasLimit, + address _refundAddress + ) internal override { + // if we want to pass value to L2, must call from `L1NativeTokenGateway`. + if (_value > 0 && _msgSender() != nativeTokenGateway) revert ErrorNonZeroValueFromCaller(); + + // compute the actual cross domain message calldata. + uint256 _messageNonce = IL1MessageQueue(messageQueue).nextCrossDomainMessageIndex(); + bytes memory _xDomainCalldata = _encodeXDomainCalldata(_msgSender(), _to, _value, _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, _value, _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 _value, + uint256 _nonce, + bytes memory _message, + L2MessageProof memory _proof + ) internal virtual override { + // if we want to pass value to L1, must call to `L1NativeTokenGateway`. + if (_value > 0 && _to != nativeTokenGateway) revert(); + + 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(_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 _value, + 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, _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(_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..d670ea0 --- /dev/null +++ b/src/alternative-gas-token/L1WrappedTokenGateway.sol @@ -0,0 +1,32 @@ +// 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 {IL1ERC20Gateway} from "../L1/gateways/IL1ERC20Gateway.sol"; +import {IWETH} from "../interfaces/IWETH.sol"; + +contract L1WrappedTokenGateway { + using SafeERC20 for IERC20; + + uint256 public constant SAFE_GAS_LIMIT = 200000; + + address public immutable WETH; + + address public immutable gateway; + + constructor(address _weth, address _gateway) { + WETH = _weth; + gateway = _gateway; + } + + function deposit(address _to, uint256 _amount) external payable { + IWETH(WETH).deposit{value: _amount}(); + + IERC20(WETH).safeApprove(gateway, 0); + IERC20(WETH).safeApprove(gateway, _amount); + IL1ERC20Gateway(gateway).depositERC20{value: msg.value - _amount}(WETH, _to, _amount, SAFE_GAS_LIMIT); + } +} From 4be30018038cb9c6b680ab95104b6e69434e39da Mon Sep 17 00:00:00 2001 From: zimpha Date: Mon, 22 Jul 2024 13:37:59 +0800 Subject: [PATCH 02/24] fix: comments --- src/L1/gateways/L1ETHGateway.sol | 2 +- src/alternative-gas-token/L1GasTokenGateway.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/L1GasTokenGateway.sol b/src/alternative-gas-token/L1GasTokenGateway.sol index 0011570..3d2a94f 100644 --- a/src/alternative-gas-token/L1GasTokenGateway.sol +++ b/src/alternative-gas-token/L1GasTokenGateway.sol @@ -17,7 +17,7 @@ import {ScrollGatewayBase} from "../libraries/gateway/ScrollGatewayBase.sol"; /// @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 `L1ScrollMessenger`. On finalizing withdraw, the corresponding +/// @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; From 0e62f080008561b9ae04f4297760755e834311f7 Mon Sep 17 00:00:00 2001 From: zimpha Date: Wed, 24 Jul 2024 15:26:03 +0800 Subject: [PATCH 03/24] feat: add scale to 18 decimals --- foundry.toml | 2 +- .../L1GasTokenGateway.sol | 34 ++++++++++++++----- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/foundry.toml b/foundry.toml index 741b5bb..bf97ac4 100644 --- a/foundry.toml +++ b/foundry.toml @@ -7,7 +7,7 @@ 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) +force = false # whether to ignore the cache (clean build) # evm_version = 'london' # 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 diff --git a/src/alternative-gas-token/L1GasTokenGateway.sol b/src/alternative-gas-token/L1GasTokenGateway.sol index 3d2a94f..1a447fa 100644 --- a/src/alternative-gas-token/L1GasTokenGateway.sol +++ b/src/alternative-gas-token/L1GasTokenGateway.sol @@ -2,8 +2,9 @@ pragma solidity =0.8.24; -import {IERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +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"; @@ -26,8 +27,12 @@ contract L1GasTokenGateway is ScrollGatewayBase, IL1ETHGateway, IMessageDropCall * 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 * ***************/ @@ -49,6 +54,7 @@ contract L1GasTokenGateway is ScrollGatewayBase, IL1ETHGateway, IMessageDropCall _disableInitializers(); gasToken = _gasToken; + scale = 10**(18 - IERC20MetadataUpgradeable(_gasToken).decimals()); } /// @notice Initialize the storage of L1GasTokenGateway. @@ -93,10 +99,11 @@ contract L1GasTokenGateway is ScrollGatewayBase, IL1ETHGateway, IMessageDropCall ) external payable override onlyCallByCounterpart nonReentrant { require(msg.value == 0, "msg.value mismatch"); - IERC20Upgradeable(gasToken).safeTransfer(_to, _amount); + uint256 downScaledAmount = _amount / scale; + IERC20Upgradeable(gasToken).safeTransfer(_to, downScaledAmount); _doCallback(_to, _data); - emit FinalizeWithdrawETH(_from, _to, _amount, _data); + emit FinalizeWithdrawETH(_from, _to, downScaledAmount, _data); } /// @inheritdoc IMessageDropCallback @@ -106,10 +113,11 @@ contract L1GasTokenGateway is ScrollGatewayBase, IL1ETHGateway, IMessageDropCall // decode (receiver, amount) (address _receiver, , uint256 _amount, ) = abi.decode(_message[4:], (address, address, uint256, bytes)); + uint256 downScaledAmount = _amount / scale; - IERC20Upgradeable(gasToken).safeTransfer(_receiver, _amount); + IERC20Upgradeable(gasToken).safeTransfer(_receiver, downScaledAmount); - emit RefundETH(_receiver, _amount); + emit RefundETH(_receiver, downScaledAmount); } /********************** @@ -138,13 +146,21 @@ contract L1GasTokenGateway is ScrollGatewayBase, IL1ETHGateway, IMessageDropCall uint256 _before = IERC20Upgradeable(gasToken).balanceOf(address(this)); IERC20Upgradeable(gasToken).safeTransferFrom(_from, address(this), _amount); uint256 _after = IERC20Upgradeable(gasToken).balanceOf(address(this)); - _amount = _after - _before; + _amount = (_after - _before); require(_amount > 0, "deposit zero gas token"); - // 3. Generate message passed to L1ScrollMessenger. - bytes memory _message = abi.encodeCall(IL2ETHGateway.finalizeDepositETH, (_from, _to, _amount, _data)); + uint256 upScaledAmount = _amount * scale; - IL1ScrollMessenger(messenger).sendMessage{value: msg.value}(counterpart, _amount, _message, _gasLimit, _from); + // 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); } From a87c7d7734429656ec5aed2a2a3f4a3a6946b5bb Mon Sep 17 00:00:00 2001 From: Morty Date: Thu, 25 Jul 2024 01:03:20 +0800 Subject: [PATCH 04/24] feat: l1 robust deployment script --- .gitignore | 2 + scripts/deterministic/DeployScroll.s.sol | 240 +++++++++++------- .../deterministic/DeterministicDeployment.sol | 25 +- 3 files changed, 175 insertions(+), 92 deletions(-) diff --git a/.gitignore b/.gitignore index 88e59a6..ecf447b 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,5 @@ broadcast # Visual Studio Code .vscode + +volume \ No newline at end of file diff --git a/scripts/deterministic/DeployScroll.s.sol b/scripts/deterministic/DeployScroll.s.sol index 197e42f..00512c3 100644 --- a/scripts/deterministic/DeployScroll.s.sol +++ b/scripts/deterministic/DeployScroll.s.sol @@ -1011,111 +1011,145 @@ contract DeployScroll is DeterminsticDeployment { **********************/ function initializeScrollChain() private { - ScrollChain(L1_SCROLL_CHAIN_PROXY_ADDR).initialize( - notnull(L1_MESSAGE_QUEUE_PROXY_ADDR), - notnull(L1_MULTIPLE_VERSION_ROLLUP_VERIFIER_ADDR), - MAX_TX_IN_CHUNK - ); + if (getInitializeCount(L1_SCROLL_CHAIN_PROXY_ADDR) == 0) { + ScrollChain(L1_SCROLL_CHAIN_PROXY_ADDR).initialize( + notnull(L1_MESSAGE_QUEUE_PROXY_ADDR), + notnull(L1_MULTIPLE_VERSION_ROLLUP_VERIFIER_ADDR), + MAX_TX_IN_CHUNK + ); + } - ScrollChain(L1_SCROLL_CHAIN_PROXY_ADDR).addSequencer(L1_COMMIT_SENDER_ADDR); - ScrollChain(L1_SCROLL_CHAIN_PROXY_ADDR).addProver(L1_FINALIZE_SENDER_ADDR); + if (!ScrollChain(L1_SCROLL_CHAIN_PROXY_ADDR).isSequencer(L1_COMMIT_SENDER_ADDR)) { + ScrollChain(L1_SCROLL_CHAIN_PROXY_ADDR).addSequencer(L1_COMMIT_SENDER_ADDR); + } + if (!ScrollChain(L1_SCROLL_CHAIN_PROXY_ADDR).isProver(L1_FINALIZE_SENDER_ADDR)) { + ScrollChain(L1_SCROLL_CHAIN_PROXY_ADDR).addProver(L1_FINALIZE_SENDER_ADDR); + } } function initializeL2GasPriceOracle() private { - L2GasPriceOracle(L2_GAS_PRICE_ORACLE_PROXY_ADDR).initialize( - 21000, // _txGas - 53000, // _txGasContractCreation - 4, // _zeroGas - 16 // _nonZeroGas - ); - - L2GasPriceOracle(L2_GAS_PRICE_ORACLE_PROXY_ADDR).updateWhitelist(L1_WHITELIST_ADDR); + if (getInitializeCount(L2_GAS_PRICE_ORACLE_PROXY_ADDR) == 0) { + L2GasPriceOracle(L2_GAS_PRICE_ORACLE_PROXY_ADDR).initialize( + 21000, // _txGas + 53000, // _txGasContractCreation + 4, // _zeroGas + 16 // _nonZeroGas + ); + } + if (L2GasPriceOracle(L2_GAS_PRICE_ORACLE_PROXY_ADDR).whitelist() != L1_WHITELIST_ADDR) { + L2GasPriceOracle(L2_GAS_PRICE_ORACLE_PROXY_ADDR).updateWhitelist(L1_WHITELIST_ADDR); + } } function initializeL1MessageQueue() private { - L1MessageQueueWithGasPriceOracle(L1_MESSAGE_QUEUE_PROXY_ADDR).initialize( - notnull(L1_SCROLL_MESSENGER_PROXY_ADDR), - notnull(L1_SCROLL_CHAIN_PROXY_ADDR), - notnull(L1_ENFORCED_TX_GATEWAY_PROXY_ADDR), - notnull(L2_GAS_PRICE_ORACLE_PROXY_ADDR), - MAX_L1_MESSAGE_GAS_LIMIT - ); - - L1MessageQueueWithGasPriceOracle(L1_MESSAGE_QUEUE_PROXY_ADDR).initializeV2(); + if (getInitializeCount(L1_MESSAGE_QUEUE_PROXY_ADDR) == 0) { + L1MessageQueueWithGasPriceOracle(L1_MESSAGE_QUEUE_PROXY_ADDR).initialize( + notnull(L1_SCROLL_MESSENGER_PROXY_ADDR), + notnull(L1_SCROLL_CHAIN_PROXY_ADDR), + notnull(L1_ENFORCED_TX_GATEWAY_PROXY_ADDR), + notnull(L2_GAS_PRICE_ORACLE_PROXY_ADDR), + MAX_L1_MESSAGE_GAS_LIMIT + ); + } + if ( + getInitializeCount(L1_MESSAGE_QUEUE_PROXY_ADDR) == 0 || getInitializeCount(L1_MESSAGE_QUEUE_PROXY_ADDR) == 1 + ) { + L1MessageQueueWithGasPriceOracle(L1_MESSAGE_QUEUE_PROXY_ADDR).initializeV2(); + } } function initializeL1ScrollMessenger() private { - L1ScrollMessenger(payable(L1_SCROLL_MESSENGER_PROXY_ADDR)).initialize( - notnull(L2_SCROLL_MESSENGER_PROXY_ADDR), - notnull(L1_FEE_VAULT_ADDR), - notnull(L1_SCROLL_CHAIN_PROXY_ADDR), - notnull(L1_MESSAGE_QUEUE_PROXY_ADDR) - ); + if (getInitializeCount(L1_SCROLL_MESSENGER_PROXY_ADDR) == 0) { + L1ScrollMessenger(payable(L1_SCROLL_MESSENGER_PROXY_ADDR)).initialize( + notnull(L2_SCROLL_MESSENGER_PROXY_ADDR), + notnull(L1_FEE_VAULT_ADDR), + notnull(L1_SCROLL_CHAIN_PROXY_ADDR), + notnull(L1_MESSAGE_QUEUE_PROXY_ADDR) + ); + } } function initializeEnforcedTxGateway() private { - EnforcedTxGateway(payable(L1_ENFORCED_TX_GATEWAY_PROXY_ADDR)).initialize( - notnull(L1_MESSAGE_QUEUE_PROXY_ADDR), - notnull(L1_FEE_VAULT_ADDR) - ); + if (getInitializeCount(L1_ENFORCED_TX_GATEWAY_PROXY_ADDR) == 0) { + EnforcedTxGateway(payable(L1_ENFORCED_TX_GATEWAY_PROXY_ADDR)).initialize( + notnull(L1_MESSAGE_QUEUE_PROXY_ADDR), + notnull(L1_FEE_VAULT_ADDR) + ); + } // disable gateway - EnforcedTxGateway(payable(L1_ENFORCED_TX_GATEWAY_PROXY_ADDR)).setPause(true); + if (!EnforcedTxGateway(payable(L1_ENFORCED_TX_GATEWAY_PROXY_ADDR)).paused()) { + EnforcedTxGateway(payable(L1_ENFORCED_TX_GATEWAY_PROXY_ADDR)).setPause(true); + } } function initializeL1GatewayRouter() private { - L1GatewayRouter(L1_GATEWAY_ROUTER_PROXY_ADDR).initialize( - notnull(L1_ETH_GATEWAY_PROXY_ADDR), - notnull(L1_STANDARD_ERC20_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(L1_STANDARD_ERC20_GATEWAY_PROXY_ADDR) + ); + } } function initializeL1CustomERC20Gateway() private { - L1CustomERC20Gateway(L1_CUSTOM_ERC20_GATEWAY_PROXY_ADDR).initialize( - notnull(L2_CUSTOM_ERC20_GATEWAY_PROXY_ADDR), - notnull(L1_GATEWAY_ROUTER_PROXY_ADDR), - notnull(L1_SCROLL_MESSENGER_PROXY_ADDR) - ); + if (getInitializeCount(L1_CUSTOM_ERC20_GATEWAY_PROXY_ADDR) == 0) { + L1CustomERC20Gateway(L1_CUSTOM_ERC20_GATEWAY_PROXY_ADDR).initialize( + notnull(L2_CUSTOM_ERC20_GATEWAY_PROXY_ADDR), + notnull(L1_GATEWAY_ROUTER_PROXY_ADDR), + notnull(L1_SCROLL_MESSENGER_PROXY_ADDR) + ); + } } function initializeL1ERC1155Gateway() private { - L1ERC1155Gateway(L1_ERC1155_GATEWAY_PROXY_ADDR).initialize( - notnull(L2_ERC1155_GATEWAY_PROXY_ADDR), - notnull(L1_SCROLL_MESSENGER_PROXY_ADDR) - ); + if (getInitializeCount(L1_ERC1155_GATEWAY_PROXY_ADDR) == 0) { + L1ERC1155Gateway(L1_ERC1155_GATEWAY_PROXY_ADDR).initialize( + notnull(L2_ERC1155_GATEWAY_PROXY_ADDR), + notnull(L1_SCROLL_MESSENGER_PROXY_ADDR) + ); + } } function initializeL1ERC721Gateway() private { - L1ERC721Gateway(L1_ERC721_GATEWAY_PROXY_ADDR).initialize( - notnull(L2_ERC721_GATEWAY_PROXY_ADDR), - notnull(L1_SCROLL_MESSENGER_PROXY_ADDR) - ); + if (getInitializeCount(L1_ERC721_GATEWAY_PROXY_ADDR) == 0) { + L1ERC721Gateway(L1_ERC721_GATEWAY_PROXY_ADDR).initialize( + notnull(L2_ERC721_GATEWAY_PROXY_ADDR), + notnull(L1_SCROLL_MESSENGER_PROXY_ADDR) + ); + } } function initializeL1ETHGateway() private { - L1ETHGateway(L1_ETH_GATEWAY_PROXY_ADDR).initialize( - notnull(L2_ETH_GATEWAY_PROXY_ADDR), - notnull(L1_GATEWAY_ROUTER_PROXY_ADDR), - notnull(L1_SCROLL_MESSENGER_PROXY_ADDR) - ); + if (getInitializeCount(L1_ETH_GATEWAY_PROXY_ADDR) == 0) { + L1ETHGateway(L1_ETH_GATEWAY_PROXY_ADDR).initialize( + notnull(L2_ETH_GATEWAY_PROXY_ADDR), + notnull(L1_GATEWAY_ROUTER_PROXY_ADDR), + notnull(L1_SCROLL_MESSENGER_PROXY_ADDR) + ); + } } function initializeL1StandardERC20Gateway() private { - L1StandardERC20Gateway(L1_STANDARD_ERC20_GATEWAY_PROXY_ADDR).initialize( - notnull(L2_STANDARD_ERC20_GATEWAY_PROXY_ADDR), - notnull(L1_GATEWAY_ROUTER_PROXY_ADDR), - notnull(L1_SCROLL_MESSENGER_PROXY_ADDR), - notnull(L2_SCROLL_STANDARD_ERC20_ADDR), - notnull(L2_SCROLL_STANDARD_ERC20_FACTORY_ADDR) - ); + if (getInitializeCount(L1_STANDARD_ERC20_GATEWAY_PROXY_ADDR) == 0) { + L1StandardERC20Gateway(L1_STANDARD_ERC20_GATEWAY_PROXY_ADDR).initialize( + notnull(L2_STANDARD_ERC20_GATEWAY_PROXY_ADDR), + notnull(L1_GATEWAY_ROUTER_PROXY_ADDR), + notnull(L1_SCROLL_MESSENGER_PROXY_ADDR), + notnull(L2_SCROLL_STANDARD_ERC20_ADDR), + notnull(L2_SCROLL_STANDARD_ERC20_FACTORY_ADDR) + ); + } } function initializeL1WETHGateway() private { - L1WETHGateway(payable(L1_WETH_GATEWAY_PROXY_ADDR)).initialize( - notnull(L2_WETH_GATEWAY_PROXY_ADDR), - notnull(L1_GATEWAY_ROUTER_PROXY_ADDR), - notnull(L1_SCROLL_MESSENGER_PROXY_ADDR) - ); + if (getInitializeCount(L1_WETH_GATEWAY_PROXY_ADDR) == 0) { + L1WETHGateway(payable(L1_WETH_GATEWAY_PROXY_ADDR)).initialize( + notnull(L2_WETH_GATEWAY_PROXY_ADDR), + notnull(L1_GATEWAY_ROUTER_PROXY_ADDR), + notnull(L1_SCROLL_MESSENGER_PROXY_ADDR) + ); + } // set WETH gateway in router { @@ -1123,14 +1157,18 @@ contract DeployScroll is DeterminsticDeployment { _tokens[0] = notnull(L1_WETH_ADDR); address[] memory _gateways = new address[](1); _gateways[0] = notnull(L1_WETH_GATEWAY_PROXY_ADDR); - L1GatewayRouter(L1_GATEWAY_ROUTER_PROXY_ADDR).setERC20Gateway(_tokens, _gateways); + if (L1GatewayRouter(L1_GATEWAY_ROUTER_PROXY_ADDR).ERC20Gateway(_tokens[0]) != _gateways[0]) { + L1GatewayRouter(L1_GATEWAY_ROUTER_PROXY_ADDR).setERC20Gateway(_tokens, _gateways); + } } } function initializeL1Whitelist() private { address[] memory accounts = new address[](1); accounts[0] = L1_GAS_ORACLE_SENDER_ADDR; - Whitelist(L1_WHITELIST_ADDR).updateWhitelistStatus(accounts, true); + if (!Whitelist(L1_WHITELIST_ADDR).isSenderAllowed(accounts[0])) { + Whitelist(L1_WHITELIST_ADDR).updateWhitelistStatus(accounts, true); + } } function transferL1ContractOwnership() private { @@ -1138,21 +1176,51 @@ contract DeployScroll is DeterminsticDeployment { return; } - Ownable(L1_ENFORCED_TX_GATEWAY_PROXY_ADDR).transferOwnership(OWNER_ADDR); - Ownable(L1_CUSTOM_ERC20_GATEWAY_PROXY_ADDR).transferOwnership(OWNER_ADDR); - Ownable(L1_ERC1155_GATEWAY_PROXY_ADDR).transferOwnership(OWNER_ADDR); - Ownable(L1_ERC721_GATEWAY_PROXY_ADDR).transferOwnership(OWNER_ADDR); - Ownable(L1_ETH_GATEWAY_PROXY_ADDR).transferOwnership(OWNER_ADDR); - Ownable(L1_GATEWAY_ROUTER_PROXY_ADDR).transferOwnership(OWNER_ADDR); - Ownable(L1_MESSAGE_QUEUE_PROXY_ADDR).transferOwnership(OWNER_ADDR); - Ownable(L1_SCROLL_MESSENGER_PROXY_ADDR).transferOwnership(OWNER_ADDR); - Ownable(L1_STANDARD_ERC20_GATEWAY_PROXY_ADDR).transferOwnership(OWNER_ADDR); - Ownable(L1_WETH_GATEWAY_PROXY_ADDR).transferOwnership(OWNER_ADDR); - Ownable(L2_GAS_PRICE_ORACLE_PROXY_ADDR).transferOwnership(OWNER_ADDR); - Ownable(L1_MULTIPLE_VERSION_ROLLUP_VERIFIER_ADDR).transferOwnership(OWNER_ADDR); - Ownable(L1_PROXY_ADMIN_ADDR).transferOwnership(OWNER_ADDR); - Ownable(L1_SCROLL_CHAIN_PROXY_ADDR).transferOwnership(OWNER_ADDR); - Ownable(L1_WHITELIST_ADDR).transferOwnership(OWNER_ADDR); + if (Ownable(L1_ENFORCED_TX_GATEWAY_PROXY_ADDR).owner() != OWNER_ADDR) { + Ownable(L1_ENFORCED_TX_GATEWAY_PROXY_ADDR).transferOwnership(OWNER_ADDR); + } + if (Ownable(L1_CUSTOM_ERC20_GATEWAY_PROXY_ADDR).owner() != OWNER_ADDR) { + Ownable(L1_CUSTOM_ERC20_GATEWAY_PROXY_ADDR).transferOwnership(OWNER_ADDR); + } + if (Ownable(L1_ERC1155_GATEWAY_PROXY_ADDR).owner() != OWNER_ADDR) { + Ownable(L1_ERC1155_GATEWAY_PROXY_ADDR).transferOwnership(OWNER_ADDR); + } + 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) { + Ownable(L1_ETH_GATEWAY_PROXY_ADDR).transferOwnership(OWNER_ADDR); + } + if (Ownable(L1_GATEWAY_ROUTER_PROXY_ADDR).owner() != OWNER_ADDR) { + Ownable(L1_GATEWAY_ROUTER_PROXY_ADDR).transferOwnership(OWNER_ADDR); + } + if (Ownable(L1_MESSAGE_QUEUE_PROXY_ADDR).owner() != OWNER_ADDR) { + Ownable(L1_MESSAGE_QUEUE_PROXY_ADDR).transferOwnership(OWNER_ADDR); + } + if (Ownable(L1_SCROLL_MESSENGER_PROXY_ADDR).owner() != OWNER_ADDR) { + Ownable(L1_SCROLL_MESSENGER_PROXY_ADDR).transferOwnership(OWNER_ADDR); + } + 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) { + Ownable(L1_WETH_GATEWAY_PROXY_ADDR).transferOwnership(OWNER_ADDR); + } + if (Ownable(L2_GAS_PRICE_ORACLE_PROXY_ADDR).owner() != OWNER_ADDR) { + Ownable(L2_GAS_PRICE_ORACLE_PROXY_ADDR).transferOwnership(OWNER_ADDR); + } + if (Ownable(L1_MULTIPLE_VERSION_ROLLUP_VERIFIER_ADDR).owner() != OWNER_ADDR) { + Ownable(L1_MULTIPLE_VERSION_ROLLUP_VERIFIER_ADDR).transferOwnership(OWNER_ADDR); + } + if (Ownable(L1_PROXY_ADMIN_ADDR).owner() != OWNER_ADDR) { + Ownable(L1_PROXY_ADMIN_ADDR).transferOwnership(OWNER_ADDR); + } + if (Ownable(L1_SCROLL_CHAIN_PROXY_ADDR).owner() != OWNER_ADDR) { + Ownable(L1_SCROLL_CHAIN_PROXY_ADDR).transferOwnership(OWNER_ADDR); + } + if (Ownable(L1_WHITELIST_ADDR).owner() != OWNER_ADDR) { + Ownable(L1_WHITELIST_ADDR).transferOwnership(OWNER_ADDR); + } } /********************** diff --git a/scripts/deterministic/DeterministicDeployment.sol b/scripts/deterministic/DeterministicDeployment.sol index 4bc8d11..cd3eefd 100644 --- a/scripts/deterministic/DeterministicDeployment.sol +++ b/scripts/deterministic/DeterministicDeployment.sol @@ -6,6 +6,7 @@ import {stdToml} from "forge-std/StdToml.sol"; import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; import {ITransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {ERC1967Upgrade} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Upgrade.sol"; import {CONFIG_CONTRACTS_PATH, DEFAULT_DEPLOYMENT_SALT, DETERMINISTIC_DEPLOYMENT_PROXY_ADDR} from "./Constants.sol"; import {Configuration} from "./Configuration.sol"; @@ -115,7 +116,9 @@ abstract contract DeterminsticDeployment is Configuration { address proxyAddr, address implAddr ) internal { - if (!skipDeploy) { + address addr = _getImplementation(proxyAddr); + + if (!skipDeploy && addr != implAddr) { ProxyAdmin(notnull(proxyAdminAddr)).upgrade( ITransparentUpgradeableProxy(notnull(proxyAddr)), notnull(implAddr) @@ -123,11 +126,16 @@ abstract contract DeterminsticDeployment is Configuration { } } + function getInitializeCount(address contractAddr) internal view returns (uint8) { + bytes32 slotValue = vm.load(address(contractAddr), bytes32(uint256(0))); + return uint8(uint256(slotValue)); + } + /********************* * Private functions * *********************/ - function _getSalt(string memory name) internal view returns (bytes32) { + function _getSalt(string memory name) private view returns (bytes32) { return keccak256(abi.encodePacked(saltPrefix, name)); } @@ -148,11 +156,9 @@ abstract contract DeterminsticDeployment is Configuration { return addr; } - // revert if the contract is already deployed + // skip if the contract is already deployed if (addr.code.length > 0) { - revert( - string(abi.encodePacked("[ERROR] contract ", name, " (", vm.toString(addr), ") is already deployed")) - ); + return addr; } // deploy contract @@ -223,4 +229,11 @@ abstract contract DeterminsticDeployment is Configuration { } } } + + function _getImplementation(address proxyAddr) private view returns (address) { + // ERC1967Upgrade implementation slot + bytes32 _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + // get implementation address + return address(uint160(uint256(vm.load(address(proxyAddr), _IMPLEMENTATION_SLOT)))); + } } From 591b875a18e7faa9e51d7233487c40b243091de1 Mon Sep 17 00:00:00 2001 From: Morty Date: Thu, 25 Jul 2024 17:44:24 +0800 Subject: [PATCH 05/24] feat: l2 robust deployment script --- scripts/deterministic/DeployScroll.s.sol | 173 +++++++++++++++-------- 1 file changed, 112 insertions(+), 61 deletions(-) diff --git a/scripts/deterministic/DeployScroll.s.sol b/scripts/deterministic/DeployScroll.s.sol index 00512c3..528cb7f 100644 --- a/scripts/deterministic/DeployScroll.s.sol +++ b/scripts/deterministic/DeployScroll.s.sol @@ -1172,10 +1172,6 @@ contract DeployScroll is DeterminsticDeployment { } function transferL1ContractOwnership() private { - if (DEPLOYER_ADDR == OWNER_ADDR) { - return; - } - if (Ownable(L1_ENFORCED_TX_GATEWAY_PROXY_ADDR).owner() != OWNER_ADDR) { Ownable(L1_ENFORCED_TX_GATEWAY_PROXY_ADDR).transferOwnership(OWNER_ADDR); } @@ -1228,73 +1224,97 @@ contract DeployScroll is DeterminsticDeployment { **********************/ function initializeL2MessageQueue() private { - L2MessageQueue(L2_MESSAGE_QUEUE_ADDR).initialize(notnull(L2_SCROLL_MESSENGER_PROXY_ADDR)); + if (L2MessageQueue(L2_MESSAGE_QUEUE_ADDR).messenger() != notnull(L2_SCROLL_MESSENGER_PROXY_ADDR)) { + L2MessageQueue(L2_MESSAGE_QUEUE_ADDR).initialize(L2_SCROLL_MESSENGER_PROXY_ADDR); + } } function initializeL2TxFeeVault() private { - L2TxFeeVault(payable(L2_TX_FEE_VAULT_ADDR)).updateMessenger(notnull(L2_SCROLL_MESSENGER_PROXY_ADDR)); + if (L2TxFeeVault(payable(L2_TX_FEE_VAULT_ADDR)).messenger() != notnull(L2_SCROLL_MESSENGER_PROXY_ADDR)) { + L2TxFeeVault(payable(L2_TX_FEE_VAULT_ADDR)).updateMessenger(L2_SCROLL_MESSENGER_PROXY_ADDR); + } } function initializeL1GasPriceOracle() private { - L1GasPriceOracle(L1_GAS_PRICE_ORACLE_ADDR).updateWhitelist(notnull(L2_WHITELIST_ADDR)); + if (address(L1GasPriceOracle(L1_GAS_PRICE_ORACLE_ADDR).whitelist()) != notnull(L2_WHITELIST_ADDR)) { + L1GasPriceOracle(L1_GAS_PRICE_ORACLE_ADDR).updateWhitelist(L2_WHITELIST_ADDR); + } } function initializeL2ScrollMessenger() private { - L2ScrollMessenger(payable(L2_SCROLL_MESSENGER_PROXY_ADDR)).initialize(notnull(L1_SCROLL_MESSENGER_PROXY_ADDR)); + if (getInitializeCount(L2_SCROLL_MESSENGER_PROXY_ADDR) == 0) { + L2ScrollMessenger(payable(L2_SCROLL_MESSENGER_PROXY_ADDR)).initialize( + notnull(L1_SCROLL_MESSENGER_PROXY_ADDR) + ); + } } function initializeL2GatewayRouter() private { - L2GatewayRouter(L2_GATEWAY_ROUTER_PROXY_ADDR).initialize( - notnull(L2_ETH_GATEWAY_PROXY_ADDR), - notnull(L2_STANDARD_ERC20_GATEWAY_PROXY_ADDR) - ); + if (getInitializeCount(L2_GATEWAY_ROUTER_PROXY_ADDR) == 0) { + L2GatewayRouter(L2_GATEWAY_ROUTER_PROXY_ADDR).initialize( + notnull(L2_ETH_GATEWAY_PROXY_ADDR), + notnull(L2_STANDARD_ERC20_GATEWAY_PROXY_ADDR) + ); + } } function initializeL2CustomERC20Gateway() private { - L2CustomERC20Gateway(L2_CUSTOM_ERC20_GATEWAY_PROXY_ADDR).initialize( - notnull(L1_CUSTOM_ERC20_GATEWAY_PROXY_ADDR), - notnull(L2_GATEWAY_ROUTER_PROXY_ADDR), - notnull(L2_SCROLL_MESSENGER_PROXY_ADDR) - ); + if (getInitializeCount(L2_CUSTOM_ERC20_GATEWAY_PROXY_ADDR) == 0) { + L2CustomERC20Gateway(L2_CUSTOM_ERC20_GATEWAY_PROXY_ADDR).initialize( + notnull(L1_CUSTOM_ERC20_GATEWAY_PROXY_ADDR), + notnull(L2_GATEWAY_ROUTER_PROXY_ADDR), + notnull(L2_SCROLL_MESSENGER_PROXY_ADDR) + ); + } } function initializeL2ERC1155Gateway() private { - L2ERC1155Gateway(L2_ERC1155_GATEWAY_PROXY_ADDR).initialize( - notnull(L1_ERC1155_GATEWAY_PROXY_ADDR), - notnull(L2_SCROLL_MESSENGER_PROXY_ADDR) - ); + if (getInitializeCount(L2_ERC1155_GATEWAY_PROXY_ADDR) == 0) { + L2ERC1155Gateway(L2_ERC1155_GATEWAY_PROXY_ADDR).initialize( + notnull(L1_ERC1155_GATEWAY_PROXY_ADDR), + notnull(L2_SCROLL_MESSENGER_PROXY_ADDR) + ); + } } function initializeL2ERC721Gateway() private { - L2ERC721Gateway(L2_ERC721_GATEWAY_PROXY_ADDR).initialize( - notnull(L1_ERC721_GATEWAY_PROXY_ADDR), - notnull(L2_SCROLL_MESSENGER_PROXY_ADDR) - ); + if (getInitializeCount(L2_ERC721_GATEWAY_PROXY_ADDR) == 0) { + L2ERC721Gateway(L2_ERC721_GATEWAY_PROXY_ADDR).initialize( + notnull(L1_ERC721_GATEWAY_PROXY_ADDR), + notnull(L2_SCROLL_MESSENGER_PROXY_ADDR) + ); + } } function initializeL2ETHGateway() private { - L2ETHGateway(L2_ETH_GATEWAY_PROXY_ADDR).initialize( - notnull(L1_ETH_GATEWAY_PROXY_ADDR), - notnull(L2_GATEWAY_ROUTER_PROXY_ADDR), - notnull(L2_SCROLL_MESSENGER_PROXY_ADDR) - ); + if (getInitializeCount(L2_ETH_GATEWAY_PROXY_ADDR) == 0) { + L2ETHGateway(L2_ETH_GATEWAY_PROXY_ADDR).initialize( + notnull(L1_ETH_GATEWAY_PROXY_ADDR), + notnull(L2_GATEWAY_ROUTER_PROXY_ADDR), + notnull(L2_SCROLL_MESSENGER_PROXY_ADDR) + ); + } } function initializeL2StandardERC20Gateway() private { - L2StandardERC20Gateway(L2_STANDARD_ERC20_GATEWAY_PROXY_ADDR).initialize( - notnull(L1_STANDARD_ERC20_GATEWAY_PROXY_ADDR), - notnull(L2_GATEWAY_ROUTER_PROXY_ADDR), - notnull(L2_SCROLL_MESSENGER_PROXY_ADDR), - notnull(L2_SCROLL_STANDARD_ERC20_FACTORY_ADDR) - ); + if (getInitializeCount(L2_STANDARD_ERC20_GATEWAY_PROXY_ADDR) == 0) { + L2StandardERC20Gateway(L2_STANDARD_ERC20_GATEWAY_PROXY_ADDR).initialize( + notnull(L1_STANDARD_ERC20_GATEWAY_PROXY_ADDR), + notnull(L2_GATEWAY_ROUTER_PROXY_ADDR), + notnull(L2_SCROLL_MESSENGER_PROXY_ADDR), + notnull(L2_SCROLL_STANDARD_ERC20_FACTORY_ADDR) + ); + } } function initializeL2WETHGateway() private { - L2WETHGateway(payable(L2_WETH_GATEWAY_PROXY_ADDR)).initialize( - notnull(L1_WETH_GATEWAY_PROXY_ADDR), - notnull(L2_GATEWAY_ROUTER_PROXY_ADDR), - notnull(L2_SCROLL_MESSENGER_PROXY_ADDR) - ); + if (getInitializeCount(L2_WETH_GATEWAY_PROXY_ADDR) == 0) { + L2WETHGateway(payable(L2_WETH_GATEWAY_PROXY_ADDR)).initialize( + notnull(L1_WETH_GATEWAY_PROXY_ADDR), + notnull(L2_GATEWAY_ROUTER_PROXY_ADDR), + notnull(L2_SCROLL_MESSENGER_PROXY_ADDR) + ); + } // set WETH gateway in router { @@ -1302,39 +1322,70 @@ contract DeployScroll is DeterminsticDeployment { _tokens[0] = notnull(L2_WETH_ADDR); address[] memory _gateways = new address[](1); _gateways[0] = notnull(L2_WETH_GATEWAY_PROXY_ADDR); - L2GatewayRouter(L2_GATEWAY_ROUTER_PROXY_ADDR).setERC20Gateway(_tokens, _gateways); + if (L2GatewayRouter(L2_GATEWAY_ROUTER_PROXY_ADDR).ERC20Gateway(_tokens[0]) != _gateways[0]) { + L2GatewayRouter(L2_GATEWAY_ROUTER_PROXY_ADDR).setERC20Gateway(_tokens, _gateways); + } } } function initializeScrollStandardERC20Factory() private { - ScrollStandardERC20Factory(L2_SCROLL_STANDARD_ERC20_FACTORY_ADDR).transferOwnership( + if ( + ScrollStandardERC20Factory(L2_SCROLL_STANDARD_ERC20_FACTORY_ADDR).owner() != notnull(L2_STANDARD_ERC20_GATEWAY_PROXY_ADDR) - ); + ) { + ScrollStandardERC20Factory(L2_SCROLL_STANDARD_ERC20_FACTORY_ADDR).transferOwnership( + L2_STANDARD_ERC20_GATEWAY_PROXY_ADDR + ); + } } function initializeL2Whitelist() private { address[] memory accounts = new address[](1); accounts[0] = L2_GAS_ORACLE_SENDER_ADDR; - Whitelist(L2_WHITELIST_ADDR).updateWhitelistStatus(accounts, true); + if (!Whitelist(L2_WHITELIST_ADDR).isSenderAllowed(accounts[0])) { + Whitelist(L2_WHITELIST_ADDR).updateWhitelistStatus(accounts, true); + } } function transferL2ContractOwnership() private { - if (DEPLOYER_ADDR == OWNER_ADDR) { - return; + if (Ownable(L1_GAS_PRICE_ORACLE_ADDR).owner() != OWNER_ADDR) { + Ownable(L1_GAS_PRICE_ORACLE_ADDR).transferOwnership(OWNER_ADDR); + } + if (Ownable(L2_CUSTOM_ERC20_GATEWAY_PROXY_ADDR).owner() != OWNER_ADDR) { + Ownable(L2_CUSTOM_ERC20_GATEWAY_PROXY_ADDR).transferOwnership(OWNER_ADDR); + } + if (Ownable(L2_ERC1155_GATEWAY_PROXY_ADDR).owner() != OWNER_ADDR) { + Ownable(L2_ERC1155_GATEWAY_PROXY_ADDR).transferOwnership(OWNER_ADDR); + } + if (Ownable(L2_ERC721_GATEWAY_PROXY_ADDR).owner() != OWNER_ADDR) { + Ownable(L2_ERC721_GATEWAY_PROXY_ADDR).transferOwnership(OWNER_ADDR); + } + if (Ownable(L2_ETH_GATEWAY_PROXY_ADDR).owner() != OWNER_ADDR) { + Ownable(L2_ETH_GATEWAY_PROXY_ADDR).transferOwnership(OWNER_ADDR); + } + if (Ownable(L2_GATEWAY_ROUTER_PROXY_ADDR).owner() != OWNER_ADDR) { + Ownable(L2_GATEWAY_ROUTER_PROXY_ADDR).transferOwnership(OWNER_ADDR); + } + if (Ownable(L2_MESSAGE_QUEUE_ADDR).owner() != OWNER_ADDR) { + Ownable(L2_MESSAGE_QUEUE_ADDR).transferOwnership(OWNER_ADDR); + } + if (Ownable(L2_SCROLL_MESSENGER_PROXY_ADDR).owner() != OWNER_ADDR) { + Ownable(L2_SCROLL_MESSENGER_PROXY_ADDR).transferOwnership(OWNER_ADDR); + } + if (Ownable(L2_STANDARD_ERC20_GATEWAY_PROXY_ADDR).owner() != OWNER_ADDR) { + Ownable(L2_STANDARD_ERC20_GATEWAY_PROXY_ADDR).transferOwnership(OWNER_ADDR); + } + 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) { + Ownable(L2_WETH_GATEWAY_PROXY_ADDR).transferOwnership(OWNER_ADDR); + } + if (Ownable(L2_PROXY_ADMIN_ADDR).owner() != OWNER_ADDR) { + Ownable(L2_PROXY_ADMIN_ADDR).transferOwnership(OWNER_ADDR); + } + if (Ownable(L2_WHITELIST_ADDR).owner() != OWNER_ADDR) { + Ownable(L2_WHITELIST_ADDR).transferOwnership(OWNER_ADDR); } - - Ownable(L1_GAS_PRICE_ORACLE_ADDR).transferOwnership(OWNER_ADDR); - Ownable(L2_CUSTOM_ERC20_GATEWAY_PROXY_ADDR).transferOwnership(OWNER_ADDR); - Ownable(L2_ERC1155_GATEWAY_PROXY_ADDR).transferOwnership(OWNER_ADDR); - Ownable(L2_ERC721_GATEWAY_PROXY_ADDR).transferOwnership(OWNER_ADDR); - Ownable(L2_ETH_GATEWAY_PROXY_ADDR).transferOwnership(OWNER_ADDR); - Ownable(L2_GATEWAY_ROUTER_PROXY_ADDR).transferOwnership(OWNER_ADDR); - Ownable(L2_MESSAGE_QUEUE_ADDR).transferOwnership(OWNER_ADDR); - Ownable(L2_SCROLL_MESSENGER_PROXY_ADDR).transferOwnership(OWNER_ADDR); - Ownable(L2_STANDARD_ERC20_GATEWAY_PROXY_ADDR).transferOwnership(OWNER_ADDR); - Ownable(L2_TX_FEE_VAULT_ADDR).transferOwnership(OWNER_ADDR); - Ownable(L2_WETH_GATEWAY_PROXY_ADDR).transferOwnership(OWNER_ADDR); - Ownable(L2_PROXY_ADMIN_ADDR).transferOwnership(OWNER_ADDR); - Ownable(L2_WHITELIST_ADDR).transferOwnership(OWNER_ADDR); } } From 466c88a4f8e613c226270d16b133684bac17c62f Mon Sep 17 00:00:00 2001 From: zimpha Date: Mon, 29 Jul 2024 11:32:27 +0800 Subject: [PATCH 06/24] feat: add more comments; use custom error --- foundry.toml | 2 +- .../L1GasTokenGateway.sol | 27 ++++- .../L1ScrollMessengerNonETH.sol | 98 +++++++++++++++---- .../L1WrappedTokenGateway.sol | 21 +++- 4 files changed, 121 insertions(+), 27 deletions(-) diff --git a/foundry.toml b/foundry.toml index bf97ac4..ff1021b 100644 --- a/foundry.toml +++ b/foundry.toml @@ -8,7 +8,7 @@ remappings = [] # a list of remapp libraries = [] # a list of deployed libraries to link against cache = true # whether to cache builds or not force = false # whether to ignore the cache (clean build) -# evm_version = 'london' # the evm version (by hardfork name) +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/alternative-gas-token/L1GasTokenGateway.sol b/src/alternative-gas-token/L1GasTokenGateway.sol index 1a447fa..c571ee3 100644 --- a/src/alternative-gas-token/L1GasTokenGateway.sol +++ b/src/alternative-gas-token/L1GasTokenGateway.sol @@ -23,6 +23,19 @@ import {ScrollGatewayBase} from "../libraries/gateway/ScrollGatewayBase.sol"; 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 * *************/ @@ -97,7 +110,9 @@ contract L1GasTokenGateway is ScrollGatewayBase, IL1ETHGateway, IMessageDropCall uint256 _amount, bytes calldata _data ) external payable override onlyCallByCounterpart nonReentrant { - require(msg.value == 0, "msg.value mismatch"); + if (msg.value > 0) { + revert ErrorNonZeroMsgValue(); + } uint256 downScaledAmount = _amount / scale; IERC20Upgradeable(gasToken).safeTransfer(_to, downScaledAmount); @@ -109,7 +124,9 @@ contract L1GasTokenGateway is ScrollGatewayBase, IL1ETHGateway, IMessageDropCall /// @inheritdoc IMessageDropCallback function onDropMessage(bytes calldata _message) external payable virtual onlyInDropContext nonReentrant { // _message should start with 0x232e8748 => finalizeDepositETH(address,address,uint256,bytes) - require(bytes4(_message[0:4]) == IL2ETHGateway.finalizeDepositETH.selector, "invalid selector"); + 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)); @@ -146,8 +163,10 @@ contract L1GasTokenGateway is ScrollGatewayBase, IL1ETHGateway, IMessageDropCall uint256 _before = IERC20Upgradeable(gasToken).balanceOf(address(this)); IERC20Upgradeable(gasToken).safeTransferFrom(_from, address(this), _amount); uint256 _after = IERC20Upgradeable(gasToken).balanceOf(address(this)); - _amount = (_after - _before); - require(_amount > 0, "deposit zero gas token"); + _amount = _after - _before; + if (_amount == 0) { + revert ErrorDepositZeroGasToken(); + } uint256 upScaledAmount = _amount * scale; diff --git a/src/alternative-gas-token/L1ScrollMessengerNonETH.sol b/src/alternative-gas-token/L1ScrollMessengerNonETH.sol index 764aefa..969e7aa 100644 --- a/src/alternative-gas-token/L1ScrollMessengerNonETH.sol +++ b/src/alternative-gas-token/L1ScrollMessengerNonETH.sol @@ -16,12 +16,39 @@ 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 * *************/ @@ -49,21 +76,31 @@ contract L1ScrollMessengerNonETH is L1ScrollMessenger { /// @inheritdoc L1ScrollMessenger function _sendMessage( address _to, - uint256 _value, + uint256 _l2GasTokenValue, bytes memory _message, uint256 _gasLimit, address _refundAddress ) internal override { // if we want to pass value to L2, must call from `L1NativeTokenGateway`. - if (_value > 0 && _msgSender() != nativeTokenGateway) revert ErrorNonZeroValueFromCaller(); + 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, _value, _messageNonce, _message); + 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 (msg.value < _fee) { + revert ErrorInsufficientMsgValue(); + } if (_fee > 0) { AddressUpgradeable.sendValue(payable(feeVault), _fee); } @@ -75,10 +112,12 @@ contract L1ScrollMessengerNonETH is L1ScrollMessenger { 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(); + if (messageSendTimestamp[_xDomainCalldataHash] != 0) { + revert ErrorDuplicatedMessage(); + } messageSendTimestamp[_xDomainCalldataHash] = block.timestamp; - emit SentMessage(_msgSender(), _to, _value, _messageNonce, _gasLimit, _message); + emit SentMessage(_msgSender(), _to, _l2GasTokenValue, _messageNonce, _gasLimit, _message); // refund fee to `_refundAddress` unchecked { @@ -93,32 +132,45 @@ contract L1ScrollMessengerNonETH is L1ScrollMessenger { function _relayMessageWithProof( address _from, address _to, - uint256 _value, + 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 (_value > 0 && _to != nativeTokenGateway) revert(); + if (_l2GasTokenValue > 0 && _to != nativeTokenGateway) { + revert ErrorNonZeroValueFromCrossDomainCaller(); + } - bytes32 _xDomainCalldataHash = keccak256(_encodeXDomainCalldata(_from, _to, _value, _nonce, _message)); - require(!isL2MessageExecuted[_xDomainCalldataHash], "Message was already successfully executed"); + bytes32 _xDomainCalldataHash = keccak256( + _encodeXDomainCalldata(_from, _to, _l2GasTokenValue, _nonce, _message) + ); + if (isL2MessageExecuted[_xDomainCalldataHash]) { + revert ErrorMessageExecuted(); + } { - require(IScrollChain(rollup).isBatchFinalized(_proof.batchIndex), "Batch is not finalized"); + if (!IScrollChain(rollup).isBatchFinalized(_proof.batchIndex)) { + revert ErrorBatchNotFinalized(); + } bytes32 _messageRoot = IScrollChain(rollup).withdrawRoots(_proof.batchIndex); - require( - WithdrawTrieVerifier.verifyMerkleProof(_messageRoot, _xDomainCalldataHash, _nonce, _proof.merkleProof), - "Invalid proof" - ); + 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. - require(_to != messageQueue, "Forbid to call message queue"); + if (_to == messageQueue) { + revert ErrorForbidToCallMessageQueue(); + } _validateTargetAddress(_to); // @note This usually will never happen, just in case. - require(_from != xDomainMessageSender, "Invalid message sender"); + if (_from == xDomainMessageSender) { + revert ErrorInvalidMessageSender(); + } xDomainMessageSender = _from; (bool success, ) = _to.call(_message); @@ -137,7 +189,7 @@ contract L1ScrollMessengerNonETH is L1ScrollMessenger { function _dropMessage( address _from, address _to, - uint256 _value, + uint256 _l2GasTokenValue, uint256 _messageNonce, bytes memory _message ) internal virtual override { @@ -154,12 +206,16 @@ contract L1ScrollMessengerNonETH is L1ScrollMessenger { // 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); + bytes memory _xDomainCalldata = _encodeXDomainCalldata(_from, _to, _l2GasTokenValue, _messageNonce, _message); bytes32 _xDomainCalldataHash = keccak256(_xDomainCalldata); - require(messageSendTimestamp[_xDomainCalldataHash] > 0, "Provided message has not been enqueued"); + if (messageSendTimestamp[_xDomainCalldataHash] == 0) { + revert ErrorMessageNotEnqueued(); + } // check message not dropped - require(!isL1MessageDropped[_xDomainCalldataHash], "Message already dropped"); + if (isL1MessageDropped[_xDomainCalldataHash]) { + revert ErrorMessageDropped(); + } // check message is finalized uint256 _lastIndex = replayStates[_xDomainCalldataHash].lastIndex; diff --git a/src/alternative-gas-token/L1WrappedTokenGateway.sol b/src/alternative-gas-token/L1WrappedTokenGateway.sol index d670ea0..1eb2033 100644 --- a/src/alternative-gas-token/L1WrappedTokenGateway.sol +++ b/src/alternative-gas-token/L1WrappedTokenGateway.sol @@ -11,17 +11,36 @@ import {IWETH} from "../interfaces/IWETH.sol"; contract L1WrappedTokenGateway { using SafeERC20 for IERC20; - uint256 public constant SAFE_GAS_LIMIT = 200000; + /************* + * Constants * + *************/ + /// @dev The safe gas limit used to bridge WETH to L2. + uint256 private constant SAFE_GAS_LIMIT = 200000; + + /// @notice The address of Wrapped Ether. address public immutable WETH; + /// @notice The address of ERC20 gateway used to bridge WETH. address public immutable gateway; + /*************** + * Constructor * + ***************/ + constructor(address _weth, address _gateway) { WETH = _weth; gateway = _gateway; } + /***************************** + * Public Mutating Functions * + *****************************/ + + /// @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}(); From 1ffb7da678730e62ab47427ff32d8e647cd0972e Mon Sep 17 00:00:00 2001 From: zimpha Date: Mon, 29 Jul 2024 18:18:08 +0800 Subject: [PATCH 07/24] feat: add unit tests --- .../L1WrappedTokenGateway.sol | 37 ++ .../AlternativeGasTokenTestBase.t.sol | 272 ++++++++++ .../GasTokenDecimalGateway.t.sol | 481 +++++++++++++++++ .../L1ScrollMessengerNonETH.t.sol | 486 ++++++++++++++++++ .../L1WrappedTokenGateway.t.sol | 106 ++++ 5 files changed, 1382 insertions(+) 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/src/alternative-gas-token/L1WrappedTokenGateway.sol b/src/alternative-gas-token/L1WrappedTokenGateway.sol index 1eb2033..1280033 100644 --- a/src/alternative-gas-token/L1WrappedTokenGateway.sol +++ b/src/alternative-gas-token/L1WrappedTokenGateway.sol @@ -4,6 +4,7 @@ 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"; @@ -11,6 +12,13 @@ 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 * *************/ @@ -18,12 +26,24 @@ contract L1WrappedTokenGateway { /// @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 * ***************/ @@ -31,12 +51,21 @@ contract L1WrappedTokenGateway { 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. @@ -46,6 +75,14 @@ contract L1WrappedTokenGateway { 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 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))); + } +} From a06bb499ee56620d491d28d8571ea77fd759f112 Mon Sep 17 00:00:00 2001 From: Morty Date: Wed, 31 Jul 2024 04:34:17 +0800 Subject: [PATCH 08/24] feat: alternative gas token deployment script --- docker/config-example.toml | 3 + docker/templates/config-contracts.toml | 4 + scripts/deterministic/Configuration.sol | 4 + scripts/deterministic/DeployScroll.s.sol | 164 +++++++++++++++++++---- 4 files changed, 150 insertions(+), 25 deletions(-) diff --git a/docker/config-example.toml b/docker/config-example.toml index 06786a6..a3fb5bf 100644 --- a/docker/config-example.toml +++ b/docker/config-example.toml @@ -12,6 +12,8 @@ MAX_L1_MESSAGE_GAS_LIMIT = 10000 L1_CONTRACT_DEPLOYMENT_BLOCK = 0 +ALTERNATIVE_GAS_TOKEN_ENABLED = true + TEST_ENV_MOCK_FINALIZE_ENABLED = true TEST_ENV_MOCK_FINALIZE_TIMEOUT_SEC = 3600 @@ -62,6 +64,7 @@ L1_PLONK_VERIFIER_ADDR = "0x0000000000000000000000000000000000000001" [contracts.overrides] # L1_WETH = "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14" +# L1_GAS_TOKEN = "0x0000000000000000000000000000000000000000" L2_MESSAGE_QUEUE = "0x5300000000000000000000000000000000000000" L1_GAS_PRICE_ORACLE = "0x5300000000000000000000000000000000000002" 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/scripts/deterministic/Configuration.sol b/scripts/deterministic/Configuration.sol index 8473bed..288f5f6 100644 --- a/scripts/deterministic/Configuration.sol +++ b/scripts/deterministic/Configuration.sol @@ -35,6 +35,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; @@ -107,6 +109,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..5afc2b6 100644 --- a/scripts/deterministic/DeployScroll.s.sol +++ b/scripts/deterministic/DeployScroll.s.sol @@ -21,6 +21,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 {ScrollStandardERC20} from "../../src/libraries/token/ScrollStandardERC20.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"; @@ -128,6 +132,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 +194,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 * ***************/ @@ -270,6 +286,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 +320,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 +355,9 @@ contract DeployScroll is DeterminsticDeployment { initializeL1WETHGateway(); initializeL1Whitelist(); + // alternative gas token contracts + initializeL1GasTokenGateway(); + transferL1ContractOwnership(); } @@ -527,7 +554,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 +568,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 +638,24 @@ contract DeployScroll is DeterminsticDeployment { ); } + function deployGasToken() private { + L1_GAS_TOKEN_ADDR = deploy("L1_GAS_TOKEN", type(ScrollStandardERC20).creationCode); + } + + function deployL1GasTokenGatewayProxy() private { + 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 +738,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 +810,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 +856,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 +936,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 +1020,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 +1041,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 +1177,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 +1219,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 +1241,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 +1270,12 @@ 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 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 +1289,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 +1304,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 +1322,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 +1395,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 +1421,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 +1492,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 (Ownable(L2_WETH_GATEWAY_PROXY_ADDR).owner() != OWNER_ADDR && !ALTERNATIVE_GAS_TOKEN_ENABLED) { Ownable(L2_WETH_GATEWAY_PROXY_ADDR).transferOwnership(OWNER_ADDR); } if (Ownable(L2_PROXY_ADMIN_ADDR).owner() != OWNER_ADDR) { From a4115979405960bd0d1bbfc641d1ae9c7d6930c1 Mon Sep 17 00:00:00 2001 From: Morty Date: Thu, 1 Aug 2024 01:26:13 +0800 Subject: [PATCH 09/24] fix: example gas token --- scripts/deterministic/DeployScroll.s.sol | 12 ++++++++-- src/alternative-gas-token/GasTokenExample.sol | 24 +++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 src/alternative-gas-token/GasTokenExample.sol diff --git a/scripts/deterministic/DeployScroll.s.sol b/scripts/deterministic/DeployScroll.s.sol index 5afc2b6..9cd7d19 100644 --- a/scripts/deterministic/DeployScroll.s.sol +++ b/scripts/deterministic/DeployScroll.s.sol @@ -21,7 +21,7 @@ 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 {ScrollStandardERC20} from "../../src/libraries/token/ScrollStandardERC20.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"; @@ -639,7 +639,15 @@ contract DeployScroll is DeterminsticDeployment { } function deployGasToken() private { - L1_GAS_TOKEN_ADDR = deploy("L1_GAS_TOKEN", type(ScrollStandardERC20).creationCode); + bytes memory args = abi.encode( + "ScrollGasToken", // _name + "GasToken", // _symbol + 18, // _decimals + DEPLOYER_ADDR, // _recipient + 10**28 // _amount + ); + + L1_GAS_TOKEN_ADDR = deploy("L1_GAS_TOKEN", type(GasTokenExample).creationCode, args); } function deployL1GasTokenGatewayProxy() private { 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_; + } +} From ab8002e5c8fb79370fdc4f045003d72c75a41145 Mon Sep 17 00:00:00 2001 From: Morty Date: Fri, 2 Aug 2024 17:03:23 +0800 Subject: [PATCH 10/24] fix: deployment script L2_WETH_GATEWAY_PROXY_ADDR transfer ownership --- scripts/deterministic/DeployScroll.s.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/deterministic/DeployScroll.s.sol b/scripts/deterministic/DeployScroll.s.sol index 9cd7d19..7933c08 100644 --- a/scripts/deterministic/DeployScroll.s.sol +++ b/scripts/deterministic/DeployScroll.s.sol @@ -1500,7 +1500,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 && !ALTERNATIVE_GAS_TOKEN_ENABLED) { + 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) { From fbd92daf30cda166c16e153f4bc894de00af1b5f Mon Sep 17 00:00:00 2001 From: Morty Date: Wed, 7 Aug 2024 19:50:34 +0800 Subject: [PATCH 11/24] fix: SAFE_GAS_LIMIT in contract L1WrappedTokenGateway --- src/alternative-gas-token/L1WrappedTokenGateway.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/alternative-gas-token/L1WrappedTokenGateway.sol b/src/alternative-gas-token/L1WrappedTokenGateway.sol index 1280033..21ccca9 100644 --- a/src/alternative-gas-token/L1WrappedTokenGateway.sol +++ b/src/alternative-gas-token/L1WrappedTokenGateway.sol @@ -24,7 +24,7 @@ contract L1WrappedTokenGateway { *************/ /// @dev The safe gas limit used to bridge WETH to L2. - uint256 private constant SAFE_GAS_LIMIT = 200000; + uint256 private constant SAFE_GAS_LIMIT = 450000; /// @dev The default value of `sender`. address private constant DEFAULT_SENDER = address(1); From b4d7694eb91be5803ec74919315caf50dab8384b Mon Sep 17 00:00:00 2001 From: Morty Date: Fri, 9 Aug 2024 14:55:52 +0800 Subject: [PATCH 12/24] feat: add gas_token_gateway to bridge-history config --- docker/templates/bridge-history-config.json | 3 ++- scripts/deterministic/DeployScroll.s.sol | 4 ++-- scripts/deterministic/GenerateConfigs.s.sol | 1 + 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docker/templates/bridge-history-config.json b/docker/templates/bridge-history-config.json index 5ae1a9e..73291d8 100644 --- a/docker/templates/bridge-history-config.json +++ b/docker/templates/bridge-history-config.json @@ -18,7 +18,8 @@ "USDCGatewayAddr": "0x0000000000000000000000000000000000000000", "LIDOGatewayAddr": "0x0000000000000000000000000000000000000000", "DAIGatewayAddr": "0x0000000000000000000000000000000000000000", - "PufferGatewayAddr": "0x0000000000000000000000000000000000000000" + "PufferGatewayAddr": "0x0000000000000000000000000000000000000000", + "GasTokenGatewayAddr": null }, "L2": { "confirmation": 0, diff --git a/scripts/deterministic/DeployScroll.s.sol b/scripts/deterministic/DeployScroll.s.sol index 7933c08..e7187d1 100644 --- a/scripts/deterministic/DeployScroll.s.sol +++ b/scripts/deterministic/DeployScroll.s.sol @@ -638,7 +638,7 @@ contract DeployScroll is DeterminsticDeployment { ); } - function deployGasToken() private { + function deployGasToken() private gasToken(true) { bytes memory args = abi.encode( "ScrollGasToken", // _name "GasToken", // _symbol @@ -650,7 +650,7 @@ contract DeployScroll is DeterminsticDeployment { L1_GAS_TOKEN_ADDR = deploy("L1_GAS_TOKEN", type(GasTokenExample).creationCode, args); } - function deployL1GasTokenGatewayProxy() private { + function deployL1GasTokenGatewayProxy() private gasToken(true) { bytes memory args = abi.encode( notnull(L1_PROXY_IMPLEMENTATION_PLACEHOLDER_ADDR), notnull(L1_PROXY_ADMIN_ADDR), diff --git a/scripts/deterministic/GenerateConfigs.s.sol b/scripts/deterministic/GenerateConfigs.s.sol index 5e055de..526ef3b 100644 --- a/scripts/deterministic/GenerateConfigs.s.sol +++ b/scripts/deterministic/GenerateConfigs.s.sol @@ -186,6 +186,7 @@ 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"); // L2 contracts vm.writeJson(vm.toString(L2_MESSAGE_QUEUE_ADDR), BRIDGE_HISTORY_CONFIG_PATH, ".L2.MessageQueueAddr"); From b4cc238d1b05f2145e77bea52e0ad270d5d65de7 Mon Sep 17 00:00:00 2001 From: Morty Date: Fri, 9 Aug 2024 15:43:54 +0800 Subject: [PATCH 13/24] feat: config example gas token decimal --- docker/config-example.toml | 1 + scripts/deterministic/DeployScroll.s.sol | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docker/config-example.toml b/docker/config-example.toml index 7f4c033..7676c5c 100644 --- a/docker/config-example.toml +++ b/docker/config-example.toml @@ -14,6 +14,7 @@ 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 diff --git a/scripts/deterministic/DeployScroll.s.sol b/scripts/deterministic/DeployScroll.s.sol index e7187d1..076e29d 100644 --- a/scripts/deterministic/DeployScroll.s.sol +++ b/scripts/deterministic/DeployScroll.s.sol @@ -80,6 +80,8 @@ contract ScrollStandardERC20FactorySetOwner is ScrollStandardERC20Factory { } contract DeployScroll is DeterminsticDeployment { + using stdToml for string; + /********* * Types * *********/ @@ -639,10 +641,16 @@ 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 - 18, // _decimals + decimal, // _decimals DEPLOYER_ADDR, // _recipient 10**28 // _amount ); From 61ebdd1ff12c02574a95ed484249e9a0106c680f Mon Sep 17 00:00:00 2001 From: Morty Date: Tue, 13 Aug 2024 00:17:41 +0800 Subject: [PATCH 14/24] fit: add frontend config values --- scripts/deterministic/GenerateConfigs.s.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/deterministic/GenerateConfigs.s.sol b/scripts/deterministic/GenerateConfigs.s.sol index 526ef3b..fcc47eb 100644 --- a/scripts/deterministic/GenerateConfigs.s.sol +++ b/scripts/deterministic/GenerateConfigs.s.sol @@ -301,6 +301,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, ""); From 4d2b3c67141dcd1d258fe13a2659fe3388b32412 Mon Sep 17 00:00:00 2001 From: Morty <70688412+yiweichi@users.noreply.github.com> Date: Wed, 14 Aug 2024 03:30:52 +0800 Subject: [PATCH 15/24] fix: emit DepositWrappedToken event (#23) --- docker/templates/bridge-history-config.json | 3 ++- scripts/deterministic/GenerateConfigs.s.sol | 1 + src/alternative-gas-token/L1WrappedTokenGateway.sol | 12 ++++++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/docker/templates/bridge-history-config.json b/docker/templates/bridge-history-config.json index 73291d8..66de7d2 100644 --- a/docker/templates/bridge-history-config.json +++ b/docker/templates/bridge-history-config.json @@ -19,7 +19,8 @@ "LIDOGatewayAddr": "0x0000000000000000000000000000000000000000", "DAIGatewayAddr": "0x0000000000000000000000000000000000000000", "PufferGatewayAddr": "0x0000000000000000000000000000000000000000", - "GasTokenGatewayAddr": null + "GasTokenGatewayAddr": null, + "WrappedTokenGatewayAddr": null }, "L2": { "confirmation": 0, diff --git a/scripts/deterministic/GenerateConfigs.s.sol b/scripts/deterministic/GenerateConfigs.s.sol index fcc47eb..bcc8946 100644 --- a/scripts/deterministic/GenerateConfigs.s.sol +++ b/scripts/deterministic/GenerateConfigs.s.sol @@ -187,6 +187,7 @@ contract GenerateBridgeHistoryConfig is DeployScroll { 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"); diff --git a/src/alternative-gas-token/L1WrappedTokenGateway.sol b/src/alternative-gas-token/L1WrappedTokenGateway.sol index 21ccca9..065a00f 100644 --- a/src/alternative-gas-token/L1WrappedTokenGateway.sol +++ b/src/alternative-gas-token/L1WrappedTokenGateway.sol @@ -12,6 +12,16 @@ 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 * *********/ @@ -79,6 +89,8 @@ contract L1WrappedTokenGateway { 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) { From 5489c1d4e73e30a4ba45e808dc80bee0f6372c72 Mon Sep 17 00:00:00 2001 From: Morty Date: Thu, 15 Aug 2024 06:34:00 +0800 Subject: [PATCH 16/24] feat: chain-monitor config support alt-gas-token --- docker/templates/chain-monitor-config.json | 12 +++++++----- docker/templates/rollup-config.json | 2 +- scripts/deterministic/GenerateConfigs.s.sol | 3 ++- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/docker/templates/chain-monitor-config.json b/docker/templates/chain-monitor-config.json index 09fb89b..428b3d9 100644 --- a/docker/templates/chain-monitor-config.json +++ b/docker/templates/chain-monitor-config.json @@ -1,7 +1,7 @@ { "l1_config": { "l1_url": null, - "confirm": "0x20", + "confirm": "0x0", "start_number": null, "l1_contracts": { "l1_gateways": { @@ -14,17 +14,19 @@ "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, - "confirm": "0x80", + "confirm": "0x0", "l2_contracts": { "l2_gateways": { "eth_gateway": null, 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/GenerateConfigs.s.sol b/scripts/deterministic/GenerateConfigs.s.sol index fcc47eb..f8496c0 100644 --- a/scripts/deterministic/GenerateConfigs.s.sol +++ b/scripts/deterministic/GenerateConfigs.s.sol @@ -128,10 +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.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(L2_DEPLOYER_INITIAL_BALANCE), CHAIN_MONITOR_CONFIG_PATH, ".l1_config.start_messenger_balance"); + vm.writeJson(vm.toString(L1_GAS_TOKEN_ADDR), CHAIN_MONITOR_CONFIG_PATH, ".l1_config.l1_contracts.gas_token"); // L2 vm.writeJson(L2_RPC_ENDPOINT, CHAIN_MONITOR_CONFIG_PATH, ".l2_config.l2_url"); From 429c441a960db66253e9f4d39e9268bfd44a0a42 Mon Sep 17 00:00:00 2001 From: Morty Date: Thu, 15 Aug 2024 13:45:06 +0800 Subject: [PATCH 17/24] fixt: load gas_token_gateway to config --- docker/templates/bridge-history-config.json | 2 +- scripts/deterministic/GenerateConfigs.s.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/templates/bridge-history-config.json b/docker/templates/bridge-history-config.json index 66de7d2..f625a31 100644 --- a/docker/templates/bridge-history-config.json +++ b/docker/templates/bridge-history-config.json @@ -19,7 +19,7 @@ "LIDOGatewayAddr": "0x0000000000000000000000000000000000000000", "DAIGatewayAddr": "0x0000000000000000000000000000000000000000", "PufferGatewayAddr": "0x0000000000000000000000000000000000000000", - "GasTokenGatewayAddr": null, + "GasTokenGatewayAddr": null, "WrappedTokenGatewayAddr": null }, "L2": { diff --git a/scripts/deterministic/GenerateConfigs.s.sol b/scripts/deterministic/GenerateConfigs.s.sol index 3f44aee..c926fe6 100644 --- a/scripts/deterministic/GenerateConfigs.s.sol +++ b/scripts/deterministic/GenerateConfigs.s.sol @@ -128,7 +128,7 @@ 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.gas_token_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"); From ee696f8ca76de5d39bad82364593cf54bed6ae65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Garamv=C3=B6lgyi?= Date: Thu, 15 Aug 2024 15:56:06 +0200 Subject: [PATCH 18/24] feat: lock tokens in bridge after deployment --- scripts/deterministic/DeployScroll.s.sol | 100 ++++++++++++++++++++++- 1 file changed, 98 insertions(+), 2 deletions(-) diff --git a/scripts/deterministic/DeployScroll.s.sol b/scripts/deterministic/DeployScroll.s.sol index 076e29d..034f856 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"; @@ -247,17 +248,80 @@ contract DeployScroll is DeterminsticDeployment { 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) + if (broadcastLayer == Layer.L1 && ALTERNATIVE_GAS_TOKEN_ENABLED) { + 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 { @@ -360,6 +424,11 @@ contract DeployScroll is DeterminsticDeployment { // 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(); } @@ -1292,6 +1361,33 @@ contract DeployScroll is DeterminsticDeployment { } } + 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).send(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); From b8d6ec94e6fe19b108a372214b5ba97a0b25652e Mon Sep 17 00:00:00 2001 From: Morty Date: Fri, 16 Aug 2024 05:06:06 +0800 Subject: [PATCH 19/24] fix: restore chain monitor config confirm block --- docker/templates/chain-monitor-config.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/templates/chain-monitor-config.json b/docker/templates/chain-monitor-config.json index 428b3d9..1bc7fba 100644 --- a/docker/templates/chain-monitor-config.json +++ b/docker/templates/chain-monitor-config.json @@ -1,7 +1,7 @@ { "l1_config": { "l1_url": null, - "confirm": "0x0", + "confirm": "0x20", "start_number": null, "l1_contracts": { "l1_gateways": { @@ -26,7 +26,7 @@ }, "l2_config": { "l2_url": null, - "confirm": "0x0", + "confirm": "0x80", "l2_contracts": { "l2_gateways": { "eth_gateway": null, From 2f3e91edc1cb7d98622822d0a4873f289f65ff18 Mon Sep 17 00:00:00 2001 From: Morty Date: Sat, 17 Aug 2024 00:48:56 +0800 Subject: [PATCH 20/24] fix: use transfer when lock tokens --- scripts/deterministic/DeployScroll.s.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/deterministic/DeployScroll.s.sol b/scripts/deterministic/DeployScroll.s.sol index 034f856..1836de3 100644 --- a/scripts/deterministic/DeployScroll.s.sol +++ b/scripts/deterministic/DeployScroll.s.sol @@ -1368,7 +1368,7 @@ contract DeployScroll is DeterminsticDeployment { if (l1MessengerBalance < amountToLock) { uint256 amountToSend = amountToLock - l1MessengerBalance; - payable(L1_SCROLL_MESSENGER_PROXY_ADDR).send(amountToSend); + payable(L1_SCROLL_MESSENGER_PROXY_ADDR).transfer(amountToSend); } } else { uint256 l1GasTokenGatewayBalance = IERC20Metadata(L1_GAS_TOKEN_ADDR).balanceOf( From 923ead9a8256296b7cd85f80e8607e38027126c5 Mon Sep 17 00:00:00 2001 From: Morty Date: Mon, 19 Aug 2024 00:06:19 +0800 Subject: [PATCH 21/24] fix: skip check funds if use test gas token --- docker/config-example.toml | 2 +- scripts/deterministic/DeployScroll.s.sol | 6 ++++-- scripts/deterministic/GenerateConfigs.s.sol | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docker/config-example.toml b/docker/config-example.toml index 7676c5c..09df81d 100644 --- a/docker/config-example.toml +++ b/docker/config-example.toml @@ -67,7 +67,7 @@ L1_PLONK_VERIFIER_ADDR = "0x0000000000000000000000000000000000000001" [contracts.overrides] # L1_WETH = "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14" -# L1_GAS_TOKEN = "0x0000000000000000000000000000000000000000" +# L1_GAS_TOKEN = "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" L2_MESSAGE_QUEUE = "0x5300000000000000000000000000000000000000" L1_GAS_PRICE_ORACLE = "0x5300000000000000000000000000000000000002" diff --git a/scripts/deterministic/DeployScroll.s.sol b/scripts/deterministic/DeployScroll.s.sol index 1836de3..fac74df 100644 --- a/scripts/deterministic/DeployScroll.s.sol +++ b/scripts/deterministic/DeployScroll.s.sol @@ -242,7 +242,7 @@ contract DeployScroll is DeterminsticDeployment { } } - function checkDeployerBalance() private view { + function checkDeployerBalance() private { // ignore balance during simulation if (broadcastLayer == Layer.None) { return; @@ -289,7 +289,9 @@ contract DeployScroll is DeterminsticDeployment { } // check funds for initial deposit (L1, alternative gas token) - if (broadcastLayer == Layer.L1 && ALTERNATIVE_GAS_TOKEN_ENABLED) { + // 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 ); diff --git a/scripts/deterministic/GenerateConfigs.s.sol b/scripts/deterministic/GenerateConfigs.s.sol index c926fe6..68d9f4b 100644 --- a/scripts/deterministic/GenerateConfigs.s.sol +++ b/scripts/deterministic/GenerateConfigs.s.sol @@ -133,6 +133,7 @@ contract GenerateChainMonitorConfig is DeployScroll { 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 vm.writeJson(L2_RPC_ENDPOINT, CHAIN_MONITOR_CONFIG_PATH, ".l2_config.l2_url"); From 18a483e8550832f5be891a669ab5977235dbd6c2 Mon Sep 17 00:00:00 2001 From: Morty Date: Mon, 19 Aug 2024 00:17:06 +0800 Subject: [PATCH 22/24] fix: remove redundant code from gen-configs.sh --- docker/scripts/deploy.sh | 5 ----- 1 file changed, 5 deletions(-) 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 From 446bd8f03e29d980173444dd2bade8fad06c439c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Garamv=C3=B6lgyi?= Date: Sun, 18 Aug 2024 23:17:30 +0200 Subject: [PATCH 23/24] feat: lock tokens in bridge after deployment (#24) Co-authored-by: Morty --- docker/config-example.toml | 2 +- docker/scripts/deploy.sh | 5 - scripts/deterministic/DeployScroll.s.sol | 104 +++++++++++++++++++- scripts/deterministic/GenerateConfigs.s.sol | 1 + 4 files changed, 103 insertions(+), 9 deletions(-) diff --git a/docker/config-example.toml b/docker/config-example.toml index 7676c5c..09df81d 100644 --- a/docker/config-example.toml +++ b/docker/config-example.toml @@ -67,7 +67,7 @@ L1_PLONK_VERIFIER_ADDR = "0x0000000000000000000000000000000000000001" [contracts.overrides] # L1_WETH = "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14" -# L1_GAS_TOKEN = "0x0000000000000000000000000000000000000000" +# 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/scripts/deterministic/DeployScroll.s.sol b/scripts/deterministic/DeployScroll.s.sol index 076e29d..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"; @@ -241,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 { @@ -360,6 +426,11 @@ contract DeployScroll is DeterminsticDeployment { // 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(); } @@ -1292,6 +1363,33 @@ contract DeployScroll is DeterminsticDeployment { } } + 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); diff --git a/scripts/deterministic/GenerateConfigs.s.sol b/scripts/deterministic/GenerateConfigs.s.sol index c926fe6..68d9f4b 100644 --- a/scripts/deterministic/GenerateConfigs.s.sol +++ b/scripts/deterministic/GenerateConfigs.s.sol @@ -133,6 +133,7 @@ contract GenerateChainMonitorConfig is DeployScroll { 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 vm.writeJson(L2_RPC_ENDPOINT, CHAIN_MONITOR_CONFIG_PATH, ".l2_config.l2_url"); From 91aee29210fed0d6f40caadf30df901c921f786b Mon Sep 17 00:00:00 2001 From: Morty Date: Mon, 19 Aug 2024 05:18:58 +0800 Subject: [PATCH 24/24] fix: predict L1GasTokenGateway address issue --- docker/scripts/gen-configs.sh | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) 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"