From eedc79aa06578b82f182eb86020c8362dec1683a Mon Sep 17 00:00:00 2001 From: Xi Lin Date: Wed, 25 Sep 2024 23:14:53 +0800 Subject: [PATCH] feat: gateway for l2 native erc20 (#40) --- foundry.toml | 6 +- src/L1/gateways/L1CustomERC20Gateway.sol | 8 +- .../gateways/L1ReverseCustomERC20Gateway.sol | 82 ++++ src/L2/gateways/L2CustomERC20Gateway.sol | 15 +- .../gateways/L2ReverseCustomERC20Gateway.sol | 120 +++++ src/test/L1CustomERC20Gateway.t.sol | 58 ++- src/test/L1ReverseCustomERC20Gateway.t.sol | 413 ++++++++++++++++ src/test/L2CustomERC20Gateway.t.sol | 38 +- src/test/L2ReverseCustomERC20Gateway.t.sol | 450 ++++++++++++++++++ .../batch-bridge/L1BatchBridgeGateway.t.sol | 18 +- 10 files changed, 1153 insertions(+), 55 deletions(-) create mode 100644 src/L1/gateways/L1ReverseCustomERC20Gateway.sol create mode 100644 src/L2/gateways/L2ReverseCustomERC20Gateway.sol create mode 100644 src/test/L1ReverseCustomERC20Gateway.t.sol create mode 100644 src/test/L2ReverseCustomERC20Gateway.t.sol diff --git a/foundry.toml b/foundry.toml index 741b5bb..d2d6dd4 100644 --- a/foundry.toml +++ b/foundry.toml @@ -7,9 +7,9 @@ libs = ["lib"] remappings = [] # a list of remappings libraries = [] # a list of deployed libraries to link against cache = true # whether to cache builds or not -force = true # whether to ignore the cache (clean build) -# evm_version = 'london' # the evm version (by hardfork name) -solc_version = '0.8.24' # override for the solc version (setting this ignores `auto_detect_solc`) +# force = true # whether to ignore the cache (clean build) +evm_version = 'cancun' # the evm version (by hardfork name) +solc_version = '0.8.24' # override for the solc version (setting this ignores `auto_detect_solc`) optimizer = true # enable or disable the solc optimizer optimizer_runs = 200 # the number of optimizer runs verbosity = 2 # the verbosity of tests diff --git a/src/L1/gateways/L1CustomERC20Gateway.sol b/src/L1/gateways/L1CustomERC20Gateway.sol index e3af654..327c930 100644 --- a/src/L1/gateways/L1CustomERC20Gateway.sol +++ b/src/L1/gateways/L1CustomERC20Gateway.sol @@ -43,7 +43,7 @@ contract L1CustomERC20Gateway is L1ERC20Gateway { /// @notice Constructor for `L1CustomERC20Gateway` implementation contract. /// - /// @param _counterpart The address of `L2USDCGateway` contract in L2. + /// @param _counterpart The address of `L2CustomERC20Gateway` contract in L2. /// @param _router The address of `L1GatewayRouter` contract in L1. /// @param _messenger The address of `L1ScrollMessenger` contract L1. constructor( @@ -86,13 +86,17 @@ contract L1CustomERC20Gateway is L1ERC20Gateway { /// @notice Update layer 1 to layer 2 token mapping. /// @param _l1Token The address of ERC20 token on layer 1. /// @param _l2Token The address of corresponding ERC20 token on layer 2. - function updateTokenMapping(address _l1Token, address _l2Token) external onlyOwner { + function updateTokenMapping(address _l1Token, address _l2Token) external payable onlyOwner { require(_l2Token != address(0), "token address cannot be 0"); address _oldL2Token = tokenMapping[_l1Token]; tokenMapping[_l1Token] = _l2Token; emit UpdateTokenMapping(_l1Token, _oldL2Token, _l2Token); + + // update corresponding mapping in L2, 1000000 gas limit should be enough + bytes memory _message = abi.encodeCall(L1CustomERC20Gateway.updateTokenMapping, (_l2Token, _l1Token)); + IL1ScrollMessenger(messenger).sendMessage{value: msg.value}(counterpart, 0, _message, 1000000, _msgSender()); } /********************** diff --git a/src/L1/gateways/L1ReverseCustomERC20Gateway.sol b/src/L1/gateways/L1ReverseCustomERC20Gateway.sol new file mode 100644 index 0000000..49bb178 --- /dev/null +++ b/src/L1/gateways/L1ReverseCustomERC20Gateway.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: MIT + +pragma solidity =0.8.24; + +import {IL2ERC20Gateway} from "../../L2/gateways/IL2ERC20Gateway.sol"; +import {IScrollERC20Upgradeable} from "../../libraries/token/IScrollERC20Upgradeable.sol"; +import {IL1ScrollMessenger} from "../IL1ScrollMessenger.sol"; +import {L1CustomERC20Gateway} from "./L1CustomERC20Gateway.sol"; +import {L1ERC20Gateway} from "./L1ERC20Gateway.sol"; + +/// @title L1ReverseCustomERC20Gateway +/// @notice The `L1ReverseCustomERC20Gateway` is used to deposit layer 2 native ERC20 tokens on layer 1 and +/// finalize withdraw the tokens from layer 2. +/// @dev The deposited tokens are transferred to this gateway and then burned. On finalizing withdraw, the corresponding +/// tokens will be minted and transfer to the recipient. +contract L1ReverseCustomERC20Gateway is L1CustomERC20Gateway { + /********** + * Errors * + **********/ + + /// @dev Thrown when no l2 token exists. + error ErrorNoCorrespondingL2Token(); + + /*************** + * Constructor * + ***************/ + + /// @notice Constructor for `L1ReverseCustomERC20Gateway` implementation contract. + /// + /// @param _counterpart The address of `L2ReverseCustomERC20Gateway` contract in L2. + /// @param _router The address of `L1GatewayRouter` contract in L1. + /// @param _messenger The address of `L1ScrollMessenger` contract L1. + constructor( + address _counterpart, + address _router, + address _messenger + ) L1CustomERC20Gateway(_counterpart, _router, _messenger) { + _disableInitializers(); + } + + /********************** + * Internal Functions * + **********************/ + + /// @inheritdoc L1ERC20Gateway + function _beforeFinalizeWithdrawERC20( + address _l1Token, + address _l2Token, + address _from, + address _to, + uint256 _amount, + bytes calldata _data + ) internal virtual override { + super._beforeFinalizeWithdrawERC20(_l1Token, _l2Token, _from, _to, _amount, _data); + + IScrollERC20Upgradeable(_l1Token).mint(address(this), _amount); + } + + /// @inheritdoc L1ERC20Gateway + function _beforeDropMessage( + address _token, + address _receiver, + uint256 _amount + ) internal virtual override { + super._beforeDropMessage(_token, _receiver, _amount); + + IScrollERC20Upgradeable(_token).mint(address(this), _amount); + } + + /// @inheritdoc L1ERC20Gateway + function _deposit( + address _token, + address _to, + uint256 _amount, + bytes memory _data, + uint256 _gasLimit + ) internal virtual override { + super._deposit(_token, _to, _amount, _data, _gasLimit); + + IScrollERC20Upgradeable(_token).burn(address(this), _amount); + } +} diff --git a/src/L2/gateways/L2CustomERC20Gateway.sol b/src/L2/gateways/L2CustomERC20Gateway.sol index 32eb7de..bf9d1d3 100644 --- a/src/L2/gateways/L2CustomERC20Gateway.sol +++ b/src/L2/gateways/L2CustomERC20Gateway.sol @@ -8,8 +8,8 @@ import {IL1ERC20Gateway} from "../../L1/gateways/IL1ERC20Gateway.sol"; import {ScrollGatewayBase} from "../../libraries/gateway/ScrollGatewayBase.sol"; import {IScrollERC20Upgradeable} from "../../libraries/token/IScrollERC20Upgradeable.sol"; -/// @title L2ERC20Gateway -/// @notice The `L2ERC20Gateway` is used to withdraw custom ERC20 compatible tokens on layer 2 and +/// @title L2CustomERC20Gateway +/// @notice The `L2CustomERC20Gateway` is used to withdraw custom ERC20 compatible tokens on layer 2 and /// finalize deposit the tokens from layer 1. /// @dev The withdrawn tokens will be burned directly. On finalizing deposit, the corresponding /// tokens will be minted and transferred to the recipient. @@ -92,7 +92,7 @@ contract L2CustomERC20Gateway is L2ERC20Gateway { address _to, uint256 _amount, bytes calldata _data - ) external payable override onlyCallByCounterpart nonReentrant { + ) external payable virtual override onlyCallByCounterpart nonReentrant { require(msg.value == 0, "nonzero msg.value"); require(_l1Token != address(0), "token address cannot be 0"); require(_l1Token == tokenMapping[_l2Token], "l1 token mismatch"); @@ -109,11 +109,12 @@ contract L2CustomERC20Gateway is L2ERC20Gateway { ************************/ /// @notice Update layer 2 to layer 1 token mapping. + /// + /// @dev To make the token mapping consistent with L1, this should be called from L1. + /// /// @param _l2Token The address of corresponding ERC20 token on layer 2. /// @param _l1Token The address of ERC20 token on layer 1. - function updateTokenMapping(address _l2Token, address _l1Token) external onlyOwner { - require(_l1Token != address(0), "token address cannot be 0"); - + function updateTokenMapping(address _l2Token, address _l1Token) external onlyCallByCounterpart { address _oldL1Token = tokenMapping[_l2Token]; tokenMapping[_l2Token] = _l1Token; @@ -146,7 +147,7 @@ contract L2CustomERC20Gateway is L2ERC20Gateway { // 2. Burn token. IScrollERC20Upgradeable(_token).burn(_from, _amount); - // 3. Generate message passed to L1StandardERC20Gateway. + // 3. Generate message passed to L1CustomERC20Gateway. bytes memory _message = abi.encodeCall( IL1ERC20Gateway.finalizeWithdrawERC20, (_l1Token, _token, _from, _to, _amount, _data) diff --git a/src/L2/gateways/L2ReverseCustomERC20Gateway.sol b/src/L2/gateways/L2ReverseCustomERC20Gateway.sol new file mode 100644 index 0000000..676d038 --- /dev/null +++ b/src/L2/gateways/L2ReverseCustomERC20Gateway.sol @@ -0,0 +1,120 @@ +// 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 {AddressUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol"; + +import {IL1ERC20Gateway} from "../../L1/gateways/IL1ERC20Gateway.sol"; +import {IL2ScrollMessenger} from "../IL2ScrollMessenger.sol"; +import {IL2ERC20Gateway, L2ERC20Gateway} from "./L2ERC20Gateway.sol"; +import {L2CustomERC20Gateway} from "./L2CustomERC20Gateway.sol"; + +/// @title L2ReverseCustomERC20Gateway +/// @notice The `L2ReverseCustomERC20Gateway` is used to withdraw native ERC20 tokens on layer 2 and +/// finalize deposit the tokens from layer 1. +/// @dev The withdrawn ERC20 tokens are holed in this contract. On finalizing deposit, the corresponding +/// token will be transferred to the recipient. +contract L2ReverseCustomERC20Gateway is L2CustomERC20Gateway { + using SafeERC20Upgradeable for IERC20Upgradeable; + + /********** + * Errors * + **********/ + + /// @dev Thrown when the message value is not zero. + error ErrorNonzeroMsgValue(); + + /// @dev Thrown when the given l1 token address is zero. + error ErrorL1TokenAddressIsZero(); + + /// @dev Thrown when the given l1 token address not match stored one. + error ErrorL1TokenAddressMismatch(); + + /// @dev Thrown when no l1 token exists. + error ErrorNoCorrespondingL1Token(); + + /// @dev Thrown when withdraw zero amount token. + error ErrorWithdrawZeroAmount(); + + /*************** + * Constructor * + ***************/ + + /// @notice Constructor for `L2ReverseCustomERC20Gateway` implementation contract. + /// + /// @param _counterpart The address of `L1ReverseCustomERC20Gateway` contract in L1. + /// @param _router The address of `L2GatewayRouter` contract in L2. + /// @param _messenger The address of `L2ScrollMessenger` contract in L2. + constructor( + address _counterpart, + address _router, + address _messenger + ) L2CustomERC20Gateway(_counterpart, _router, _messenger) { + _disableInitializers(); + } + + /***************************** + * Public Mutating Functions * + *****************************/ + + /// @inheritdoc IL2ERC20Gateway + function finalizeDepositERC20( + address _l1Token, + address _l2Token, + address _from, + address _to, + uint256 _amount, + bytes calldata _data + ) external payable override onlyCallByCounterpart nonReentrant { + if (msg.value != 0) revert ErrorNonzeroMsgValue(); + if (_l1Token == address(0)) revert ErrorL1TokenAddressIsZero(); + if (_l1Token != tokenMapping[_l2Token]) revert ErrorL1TokenAddressMismatch(); + + IERC20Upgradeable(_l2Token).safeTransfer(_to, _amount); + + _doCallback(_to, _data); + + emit FinalizeDepositERC20(_l1Token, _l2Token, _from, _to, _amount, _data); + } + + /********************** + * Internal Functions * + **********************/ + + /// @inheritdoc L2ERC20Gateway + function _withdraw( + address _token, + address _to, + uint256 _amount, + bytes memory _data, + uint256 _gasLimit + ) internal virtual override nonReentrant { + address _l1Token = tokenMapping[_token]; + if (_l1Token == address(0)) revert ErrorNoCorrespondingL1Token(); + if (_amount == 0) revert ErrorWithdrawZeroAmount(); + + // 1. Extract real sender if this call is from L2GatewayRouter. + address _from = _msgSender(); + if (router == _from) { + (_from, _data) = abi.decode(_data, (address, bytes)); + } + + // 2. transfer token to this contract + uint256 balance = IERC20Upgradeable(_token).balanceOf(address(this)); + IERC20Upgradeable(_token).safeTransferFrom(_from, address(this), _amount); + _amount = IERC20Upgradeable(_token).balanceOf(address(this)) - balance; + + // 3. Generate message passed to L1ReverseCustomERC20Gateway. + bytes memory _message = abi.encodeCall( + IL1ERC20Gateway.finalizeWithdrawERC20, + (_l1Token, _token, _from, _to, _amount, _data) + ); + + // 4. send message to L2ScrollMessenger + IL2ScrollMessenger(messenger).sendMessage{value: msg.value}(counterpart, 0, _message, _gasLimit); + + emit WithdrawERC20(_l1Token, _token, _from, _to, _amount, _data); + } +} diff --git a/src/test/L1CustomERC20Gateway.t.sol b/src/test/L1CustomERC20Gateway.t.sol index ecc0b2c..879fcb6 100644 --- a/src/test/L1CustomERC20Gateway.t.sol +++ b/src/test/L1CustomERC20Gateway.t.sol @@ -99,7 +99,7 @@ contract L1CustomERC20GatewayTest is L1GatewayTestBase { hevm.assume(token1 != address(l1Token)); assertEq(gateway.getL2ERC20Address(token1), address(0)); - gateway.updateTokenMapping(token1, token2); + gateway.updateTokenMapping{value: 1 ether}(token1, token2); assertEq(gateway.getL2ERC20Address(token1), token2); } @@ -178,7 +178,16 @@ contract L1CustomERC20GatewayTest is L1GatewayTestBase { address recipient, bytes memory dataToCall ) public { - gateway.updateTokenMapping(address(l1Token), address(l2Token)); + // message 0 is append here + gateway.updateTokenMapping{value: 1 ether}(address(l1Token), address(l2Token)); + + // finalize message 0 + hevm.startPrank(address(rollup)); + messageQueue.popCrossDomainMessage(0, 1, 0); + messageQueue.finalizePoppedCrossDomainMessage(1); + hevm.stopPrank(); + assertEq(messageQueue.pendingQueueIndex(), 1); + assertEq(messageQueue.nextUnfinalizedQueueIndex(), 1); amount = bound(amount, 1, l1Token.balanceOf(address(this))); bytes memory message = abi.encodeWithSelector( @@ -192,20 +201,20 @@ contract L1CustomERC20GatewayTest is L1GatewayTestBase { ); gateway.depositERC20AndCall(address(l1Token), recipient, amount, dataToCall, defaultGasLimit); - // skip message 0 + // skip message 1 hevm.startPrank(address(rollup)); - messageQueue.popCrossDomainMessage(0, 1, 0x1); - messageQueue.finalizePoppedCrossDomainMessage(1); - assertEq(messageQueue.pendingQueueIndex(), 1); - assertEq(messageQueue.nextUnfinalizedQueueIndex(), 1); + messageQueue.popCrossDomainMessage(1, 1, 0x1); + messageQueue.finalizePoppedCrossDomainMessage(2); + assertEq(messageQueue.pendingQueueIndex(), 2); + assertEq(messageQueue.nextUnfinalizedQueueIndex(), 2); hevm.stopPrank(); - // drop message 0 + // drop message 1 hevm.expectEmit(true, true, false, true); emit RefundERC20(address(l1Token), address(this), amount); uint256 balance = l1Token.balanceOf(address(this)); - l1Messenger.dropMessage(address(gateway), address(counterpartGateway), 0, 0, message); + l1Messenger.dropMessage(address(gateway), address(counterpartGateway), 0, 1, message); assertEq(balance + amount, l1Token.balanceOf(address(this))); } @@ -283,7 +292,7 @@ contract L1CustomERC20GatewayTest is L1GatewayTestBase { hevm.assume(recipient != address(0)); hevm.assume(recipient != address(gateway)); - gateway.updateTokenMapping(address(l1Token), address(l2Token)); + gateway.updateTokenMapping{value: 1 ether}(address(l1Token), address(l2Token)); amount = bound(amount, 1, l1Token.balanceOf(address(this))); @@ -342,7 +351,7 @@ contract L1CustomERC20GatewayTest is L1GatewayTestBase { ) public { MockGatewayRecipient recipient = new MockGatewayRecipient(); - gateway.updateTokenMapping(address(l1Token), address(l2Token)); + gateway.updateTokenMapping{value: 1 ether}(address(l1Token), address(l2Token)); amount = bound(amount, 1, l1Token.balanceOf(address(this))); @@ -407,6 +416,7 @@ contract L1CustomERC20GatewayTest is L1GatewayTestBase { uint256 gasLimit, uint256 feePerGas ) private { + uint64 nonce = 1; amount = bound(amount, 0, l1Token.balanceOf(address(this))); gasLimit = bound(gasLimit, defaultGasLimit / 2, defaultGasLimit); feePerGas = bound(feePerGas, 0, 1000); @@ -428,7 +438,7 @@ contract L1CustomERC20GatewayTest is L1GatewayTestBase { address(gateway), address(counterpartGateway), 0, - 0, + nonce, message ); @@ -439,7 +449,7 @@ contract L1CustomERC20GatewayTest is L1GatewayTestBase { gateway.depositERC20{value: feeToPay + extraValue}(address(l1Token), amount, gasLimit); } - gateway.updateTokenMapping(address(l1Token), address(l2Token)); + gateway.updateTokenMapping{value: 1 ether}(address(l1Token), address(l2Token)); if (amount == 0) { hevm.expectRevert("deposit zero amount"); if (useRouter) { @@ -452,13 +462,13 @@ contract L1CustomERC20GatewayTest is L1GatewayTestBase { { hevm.expectEmit(true, true, false, true); address sender = AddressAliasHelper.applyL1ToL2Alias(address(l1Messenger)); - emit QueueTransaction(sender, address(l2Messenger), 0, 0, gasLimit, xDomainCalldata); + emit QueueTransaction(sender, address(l2Messenger), 0, nonce, gasLimit, xDomainCalldata); } // emit SentMessage from L1ScrollMessenger { hevm.expectEmit(true, true, false, true); - emit SentMessage(address(gateway), address(counterpartGateway), 0, 0, gasLimit, message); + emit SentMessage(address(gateway), address(counterpartGateway), 0, nonce, gasLimit, message); } // emit DepositERC20 from L1CustomERC20Gateway @@ -486,6 +496,7 @@ contract L1CustomERC20GatewayTest is L1GatewayTestBase { uint256 gasLimit, uint256 feePerGas ) private { + uint64 nonce = 1; amount = bound(amount, 0, l1Token.balanceOf(address(this))); gasLimit = bound(gasLimit, defaultGasLimit / 2, defaultGasLimit); feePerGas = bound(feePerGas, 0, 1000); @@ -507,7 +518,7 @@ contract L1CustomERC20GatewayTest is L1GatewayTestBase { address(gateway), address(counterpartGateway), 0, - 0, + nonce, message ); @@ -518,7 +529,7 @@ contract L1CustomERC20GatewayTest is L1GatewayTestBase { gateway.depositERC20{value: feeToPay + extraValue}(address(l1Token), amount, gasLimit); } - gateway.updateTokenMapping(address(l1Token), address(l2Token)); + gateway.updateTokenMapping{value: 1 ether}(address(l1Token), address(l2Token)); if (amount == 0) { hevm.expectRevert("deposit zero amount"); if (useRouter) { @@ -531,13 +542,13 @@ contract L1CustomERC20GatewayTest is L1GatewayTestBase { { hevm.expectEmit(true, true, false, true); address sender = AddressAliasHelper.applyL1ToL2Alias(address(l1Messenger)); - emit QueueTransaction(sender, address(l2Messenger), 0, 0, gasLimit, xDomainCalldata); + emit QueueTransaction(sender, address(l2Messenger), 0, nonce, gasLimit, xDomainCalldata); } // emit SentMessage from L1ScrollMessenger { hevm.expectEmit(true, true, false, true); - emit SentMessage(address(gateway), address(counterpartGateway), 0, 0, gasLimit, message); + emit SentMessage(address(gateway), address(counterpartGateway), 0, nonce, gasLimit, message); } // emit DepositERC20 from L1CustomERC20Gateway @@ -566,6 +577,7 @@ contract L1CustomERC20GatewayTest is L1GatewayTestBase { uint256 gasLimit, uint256 feePerGas ) private { + uint64 nonce = 1; amount = bound(amount, 0, l1Token.balanceOf(address(this))); gasLimit = bound(gasLimit, defaultGasLimit / 2, defaultGasLimit); feePerGas = bound(feePerGas, 0, 1000); @@ -587,7 +599,7 @@ contract L1CustomERC20GatewayTest is L1GatewayTestBase { address(gateway), address(counterpartGateway), 0, - 0, + nonce, message ); @@ -598,7 +610,7 @@ contract L1CustomERC20GatewayTest is L1GatewayTestBase { gateway.depositERC20{value: feeToPay + extraValue}(address(l1Token), amount, gasLimit); } - gateway.updateTokenMapping(address(l1Token), address(l2Token)); + gateway.updateTokenMapping{value: 1 ether}(address(l1Token), address(l2Token)); if (amount == 0) { hevm.expectRevert("deposit zero amount"); if (useRouter) { @@ -623,13 +635,13 @@ contract L1CustomERC20GatewayTest is L1GatewayTestBase { { hevm.expectEmit(true, true, false, true); address sender = AddressAliasHelper.applyL1ToL2Alias(address(l1Messenger)); - emit QueueTransaction(sender, address(l2Messenger), 0, 0, gasLimit, xDomainCalldata); + emit QueueTransaction(sender, address(l2Messenger), 0, nonce, gasLimit, xDomainCalldata); } // emit SentMessage from L1ScrollMessenger { hevm.expectEmit(true, true, false, true); - emit SentMessage(address(gateway), address(counterpartGateway), 0, 0, gasLimit, message); + emit SentMessage(address(gateway), address(counterpartGateway), 0, nonce, gasLimit, message); } // emit DepositERC20 from L1CustomERC20Gateway diff --git a/src/test/L1ReverseCustomERC20Gateway.t.sol b/src/test/L1ReverseCustomERC20Gateway.t.sol new file mode 100644 index 0000000..81913ec --- /dev/null +++ b/src/test/L1ReverseCustomERC20Gateway.t.sol @@ -0,0 +1,413 @@ +// 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 {IL1ERC20Gateway} from "../L1/gateways/IL1ERC20Gateway.sol"; +import {L1ReverseCustomERC20Gateway} from "../L1/gateways/L1ReverseCustomERC20Gateway.sol"; +import {L1GatewayRouter} from "../L1/gateways/L1GatewayRouter.sol"; +import {IL1ScrollMessenger} from "../L1/IL1ScrollMessenger.sol"; +import {L1ScrollMessenger} from "../L1/L1ScrollMessenger.sol"; +import {IL2ERC20Gateway} from "../L2/gateways/IL2ERC20Gateway.sol"; +import {L2ReverseCustomERC20Gateway} from "../L2/gateways/L2ReverseCustomERC20Gateway.sol"; +import {AddressAliasHelper} from "../libraries/common/AddressAliasHelper.sol"; +import {ScrollConstants} from "../libraries/constants/ScrollConstants.sol"; + +import {L1GatewayTestBase} from "./L1GatewayTestBase.t.sol"; +import {MockScrollMessenger} from "./mocks/MockScrollMessenger.sol"; +import {MockGatewayRecipient} from "./mocks/MockGatewayRecipient.sol"; + +contract MockL1ReverseCustomERC20Gateway is L1ReverseCustomERC20Gateway { + constructor( + address _counterpart, + address _router, + address _messenger + ) L1ReverseCustomERC20Gateway(_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) + } + } + } +} + +contract L1ReverseCustomERC20GatewayTest is L1GatewayTestBase { + // from L1ReverseCustomERC20Gateway + event FinalizeWithdrawERC20( + address indexed _l1Token, + address indexed _l2Token, + address indexed _from, + address _to, + uint256 _amount, + bytes _data + ); + event DepositERC20( + address indexed _l1Token, + address indexed _l2Token, + address indexed _from, + address _to, + uint256 _amount, + bytes _data + ); + event RefundERC20(address indexed token, address indexed recipient, uint256 amount); + + MockL1ReverseCustomERC20Gateway private gateway; + L1GatewayRouter private router; + + L2ReverseCustomERC20Gateway private counterpartGateway; + + MockERC20 private l1Token; + MockERC20 private l2Token; + + function setUp() public { + __L1GatewayTestBase_setUp(); + + // Deploy tokens + l1Token = new MockERC20("Mock L1", "ML1", 18); + l2Token = new MockERC20("Mock L2", "ML2", 18); + + // Deploy L2 contracts + counterpartGateway = new L2ReverseCustomERC20Gateway(address(1), address(1), address(1)); + + // Deploy L1 contracts + router = L1GatewayRouter(_deployProxy(address(new L1GatewayRouter()))); + gateway = _deployGateway(address(l1Messenger)); + + // Initialize L1 contracts + gateway.initialize(address(counterpartGateway), address(router), address(l1Messenger)); + router.initialize(address(0), address(gateway)); + + // Prepare token balances + l1Token.mint(address(this), type(uint128).max); + l1Token.approve(address(gateway), type(uint256).max); + l1Token.approve(address(router), type(uint256).max); + } + + function testDepositERC20( + uint256 amount, + uint256 gasLimit, + uint256 feePerGas + ) external { + _depositERC20(false, 0, amount, address(this), new bytes(0), gasLimit, feePerGas); + } + + function testDepositERC20WithRecipient( + uint256 amount, + address recipient, + uint256 gasLimit, + uint256 feePerGas + ) external { + _depositERC20(false, 1, amount, recipient, new bytes(0), gasLimit, feePerGas); + } + + function testDepositERC20WithRecipientAndCalldata( + uint256 amount, + address recipient, + bytes memory dataToCall, + uint256 gasLimit, + uint256 feePerGas + ) external { + _depositERC20(false, 2, amount, recipient, dataToCall, gasLimit, feePerGas); + } + + function testDepositERC20ByRouter( + uint256 amount, + uint256 gasLimit, + uint256 feePerGas + ) external { + _depositERC20(true, 0, amount, address(this), new bytes(0), gasLimit, feePerGas); + } + + function testDepositERC20WithRecipientByRouter( + uint256 amount, + address recipient, + uint256 gasLimit, + uint256 feePerGas + ) external { + _depositERC20(true, 1, amount, recipient, new bytes(0), gasLimit, feePerGas); + } + + function testDepositERC20WithRecipientAndCalldataByRouter( + uint256 amount, + address recipient, + bytes memory dataToCall, + uint256 gasLimit, + uint256 feePerGas + ) external { + _depositERC20(true, 2, amount, recipient, dataToCall, gasLimit, feePerGas); + } + + function testDropMessage( + uint256 amount, + address recipient, + bytes memory dataToCall + ) public { + // message 0 is append here + gateway.updateTokenMapping{value: 1 ether}(address(l1Token), address(l2Token)); + + // finalize message 0 + hevm.startPrank(address(rollup)); + messageQueue.popCrossDomainMessage(0, 1, 0); + messageQueue.finalizePoppedCrossDomainMessage(1); + hevm.stopPrank(); + assertEq(messageQueue.pendingQueueIndex(), 1); + assertEq(messageQueue.nextUnfinalizedQueueIndex(), 1); + + amount = bound(amount, 1, l1Token.balanceOf(address(this))); + bytes memory message = abi.encodeWithSelector( + IL2ERC20Gateway.finalizeDepositERC20.selector, + address(l1Token), + address(l2Token), + address(this), + recipient, + amount, + dataToCall + ); + gateway.depositERC20AndCall(address(l1Token), recipient, amount, dataToCall, defaultGasLimit); + + // skip message 1 + hevm.startPrank(address(rollup)); + messageQueue.popCrossDomainMessage(1, 1, 0x1); + messageQueue.finalizePoppedCrossDomainMessage(2); + assertEq(messageQueue.pendingQueueIndex(), 2); + assertEq(messageQueue.nextUnfinalizedQueueIndex(), 2); + hevm.stopPrank(); + + // drop message 1 + hevm.expectEmit(true, true, false, true); + emit RefundERC20(address(l1Token), address(this), amount); + + uint256 balance = l1Token.balanceOf(address(this)); + uint256 gatewayBalance = l1Token.balanceOf(address(gateway)); + l1Messenger.dropMessage(address(gateway), address(counterpartGateway), 0, 1, message); + assertEq(balance + amount, l1Token.balanceOf(address(this))); + assertEq(gatewayBalance, l1Token.balanceOf(address(gateway))); + } + + function testFinalizeWithdrawERC20( + address sender, + uint256 amount, + bytes memory dataToCall + ) public { + MockGatewayRecipient recipient = new MockGatewayRecipient(); + + gateway.updateTokenMapping{value: 1 ether}(address(l1Token), address(l2Token)); + + amount = bound(amount, 1, l1Token.balanceOf(address(this))); + + // deposit some token to L1StandardERC20Gateway + gateway.depositERC20(address(l1Token), amount, defaultGasLimit); + + // do finalize withdraw token + bytes memory message = abi.encodeWithSelector( + IL1ERC20Gateway.finalizeWithdrawERC20.selector, + address(l1Token), + address(l2Token), + sender, + address(recipient), + amount, + dataToCall + ); + bytes memory xDomainCalldata = abi.encodeWithSignature( + "relayMessage(address,address,uint256,uint256,bytes)", + address(counterpartGateway), + address(gateway), + 0, + 0, + message + ); + + prepareL2MessageRoot(keccak256(xDomainCalldata)); + + IL1ScrollMessenger.L2MessageProof memory proof; + proof.batchIndex = rollup.lastFinalizedBatchIndex(); + + // emit FinalizeWithdrawERC20 from L1StandardERC20Gateway + { + hevm.expectEmit(true, true, true, true); + emit FinalizeWithdrawERC20( + address(l1Token), + address(l2Token), + sender, + address(recipient), + amount, + dataToCall + ); + } + + // emit RelayedMessage from L1ScrollMessenger + { + hevm.expectEmit(true, false, false, true); + emit RelayedMessage(keccak256(xDomainCalldata)); + } + + uint256 gatewayBalance = l1Token.balanceOf(address(gateway)); + uint256 recipientBalance = l1Token.balanceOf(address(recipient)); + assertBoolEq(false, l1Messenger.isL2MessageExecuted(keccak256(xDomainCalldata))); + l1Messenger.relayMessageWithProof(address(counterpartGateway), address(gateway), 0, 0, message, proof); + assertEq(gatewayBalance, l1Token.balanceOf(address(gateway))); + assertEq(recipientBalance + amount, l1Token.balanceOf(address(recipient))); + assertBoolEq(true, l1Messenger.isL2MessageExecuted(keccak256(xDomainCalldata))); + } + + function _depositERC20( + bool useRouter, + uint256 methodType, + uint256 amount, + address recipient, + bytes memory dataToCall, + uint256 gasLimit, + uint256 feePerGas + ) private { + hevm.assume(recipient != address(0)); + amount = bound(amount, 1, l1Token.balanceOf(address(this))); + gasLimit = bound(gasLimit, defaultGasLimit / 2, defaultGasLimit); + feePerGas = bound(feePerGas, 0, 1000); + messageQueue.setL2BaseFee(feePerGas); + feePerGas = feePerGas * gasLimit; + + // revert when reentrant + hevm.expectRevert("ReentrancyGuard: reentrant call"); + { + bytes memory reentrantData; + if (methodType == 0) { + reentrantData = abi.encodeWithSignature( + "depositERC20(address,uint256,uint256)", + address(l1Token), + amount, + gasLimit + ); + } else if (methodType == 1) { + reentrantData = abi.encodeWithSignature( + "depositERC20(address,address,uint256,uint256)", + address(l1Token), + recipient, + amount, + gasLimit + ); + } else if (methodType == 2) { + reentrantData = abi.encodeCall( + IL1ERC20Gateway.depositERC20AndCall, + (address(l1Token), recipient, amount, dataToCall, gasLimit) + ); + } + gateway.reentrantCall(useRouter ? address(router) : address(gateway), reentrantData); + } + + // revert when l1 token not support + hevm.expectRevert("no corresponding l2 token"); + _invokeDepositERC20Call( + useRouter, + methodType, + address(l2Token), + amount, + recipient, + dataToCall, + gasLimit, + feePerGas + ); + + gateway.updateTokenMapping{value: 1 ether}(address(l1Token), address(l2Token)); + uint64 nonce = uint64(messageQueue.nextCrossDomainMessageIndex()); + + // revert when deposit zero amount + hevm.expectRevert("deposit zero amount"); + _invokeDepositERC20Call(useRouter, methodType, address(l1Token), 0, recipient, dataToCall, gasLimit, feePerGas); + + // succeed to deposit + bytes memory message = abi.encodeCall( + IL2ERC20Gateway.finalizeDepositERC20, + (address(l1Token), address(l2Token), address(this), recipient, amount, dataToCall) + ); + bytes memory xDomainCalldata = abi.encodeCall( + l2Messenger.relayMessage, + (address(gateway), address(counterpartGateway), 0, nonce, message) + ); + // should emit QueueTransaction from L1MessageQueue + { + hevm.expectEmit(true, true, false, true); + address sender = AddressAliasHelper.applyL1ToL2Alias(address(l1Messenger)); + emit QueueTransaction(sender, address(l2Messenger), 0, nonce, gasLimit, xDomainCalldata); + } + // should emit SentMessage from L1ScrollMessenger + { + hevm.expectEmit(true, true, false, true); + emit SentMessage(address(gateway), address(counterpartGateway), 0, nonce, gasLimit, message); + } + // should emit DepositERC20 from L1CustomERC20Gateway + { + hevm.expectEmit(true, true, true, true); + emit DepositERC20(address(l1Token), address(l2Token), address(this), recipient, amount, dataToCall); + } + + uint256 gatewayBalance = l1Token.balanceOf(address(gateway)); + uint256 feeVaultBalance = address(feeVault).balance; + uint256 thisBalance = l1Token.balanceOf(address(this)); + assertEq(l1Messenger.messageSendTimestamp(keccak256(xDomainCalldata)), 0); + uint256 balance = address(this).balance; + _invokeDepositERC20Call( + useRouter, + methodType, + address(l1Token), + amount, + recipient, + dataToCall, + gasLimit, + feePerGas + ); + assertEq(balance - feePerGas, address(this).balance); // extra value is transferred back + assertGt(l1Messenger.messageSendTimestamp(keccak256(xDomainCalldata)), 0); + assertEq(thisBalance - amount, l1Token.balanceOf(address(this))); + assertEq(feeVaultBalance + feePerGas, address(feeVault).balance); + assertEq(gatewayBalance, l1Token.balanceOf(address(gateway))); + } + + function _invokeDepositERC20Call( + bool useRouter, + uint256 methodType, + address token, + uint256 amount, + address recipient, + bytes memory dataToCall, + uint256 gasLimit, + uint256 feeToPay + ) private { + uint256 value = feeToPay + extraValue; + if (useRouter) { + if (methodType == 0) { + router.depositERC20{value: value}(token, amount, gasLimit); + } else if (methodType == 1) { + router.depositERC20{value: value}(token, recipient, amount, gasLimit); + } else if (methodType == 2) { + router.depositERC20AndCall{value: value}(token, recipient, amount, dataToCall, gasLimit); + } + } else { + if (methodType == 0) { + gateway.depositERC20{value: value}(token, amount, gasLimit); + } else if (methodType == 1) { + gateway.depositERC20{value: value}(token, recipient, amount, gasLimit); + } else if (methodType == 2) { + gateway.depositERC20AndCall{value: value}(token, recipient, amount, dataToCall, gasLimit); + } + } + } + + function _deployGateway(address messenger) internal returns (MockL1ReverseCustomERC20Gateway _gateway) { + _gateway = MockL1ReverseCustomERC20Gateway(_deployProxy(address(0))); + + admin.upgrade( + ITransparentUpgradeableProxy(address(_gateway)), + address(new MockL1ReverseCustomERC20Gateway(address(counterpartGateway), address(router), messenger)) + ); + } +} diff --git a/src/test/L2CustomERC20Gateway.t.sol b/src/test/L2CustomERC20Gateway.t.sol index 5a0838f..52b0f3f 100644 --- a/src/test/L2CustomERC20Gateway.t.sol +++ b/src/test/L2CustomERC20Gateway.t.sol @@ -81,23 +81,27 @@ contract L2CustomERC20GatewayTest is L2GatewayTestBase { } function testUpdateTokenMappingFailed(address token2) public { - // call by non-owner, should revert + // revert ErrorCallerIsNotMessenger hevm.startPrank(address(1)); - hevm.expectRevert("Ownable: caller is not the owner"); + hevm.expectRevert(ErrorCallerIsNotMessenger.selector); gateway.updateTokenMapping(token2, token2); hevm.stopPrank(); - // l1 token is zero, should revert - hevm.expectRevert("token address cannot be 0"); - gateway.updateTokenMapping(token2, address(0)); + // revert ErrorCallerIsNotCounterpartGateway + hevm.startPrank(address(l2Messenger)); + hevm.expectRevert(ErrorCallerIsNotCounterpartGateway.selector); + gateway.updateTokenMapping(token2, token2); + hevm.stopPrank(); } function testUpdateTokenMappingSuccess(address token1, address token2) public { hevm.assume(token1 != address(0)); assertEq(gateway.getL1ERC20Address(token2), address(0)); - gateway.updateTokenMapping(token2, token1); + _updateTokenMapping(token1, token2); assertEq(gateway.getL1ERC20Address(token2), token1); + _updateTokenMapping(address(0), token2); + assertEq(gateway.getL1ERC20Address(token2), address(0)); } function testWithdrawERC20( @@ -227,7 +231,7 @@ contract L2CustomERC20GatewayTest is L2GatewayTestBase { // blacklist some addresses hevm.assume(recipient != address(0)); - gateway.updateTokenMapping(address(l2Token), address(l1Token)); + _updateTokenMapping(address(l1Token), address(l2Token)); amount = bound(amount, 1, l2Token.balanceOf(address(this))); @@ -273,7 +277,7 @@ contract L2CustomERC20GatewayTest is L2GatewayTestBase { ) public { MockGatewayRecipient recipient = new MockGatewayRecipient(); - gateway.updateTokenMapping(address(l2Token), address(l1Token)); + _updateTokenMapping(address(l1Token), address(l2Token)); amount = bound(amount, 1, l2Token.balanceOf(address(this))); @@ -364,7 +368,7 @@ contract L2CustomERC20GatewayTest is L2GatewayTestBase { gateway.withdrawERC20{value: feeToPay}(address(l2Token), amount, gasLimit); } - gateway.updateTokenMapping(address(l2Token), address(l1Token)); + _updateTokenMapping(address(l1Token), address(l2Token)); if (amount == 0) { hevm.expectRevert("withdraw zero amount"); if (useRouter) { @@ -442,7 +446,7 @@ contract L2CustomERC20GatewayTest is L2GatewayTestBase { gateway.withdrawERC20{value: feeToPay}(address(l2Token), amount, gasLimit); } - gateway.updateTokenMapping(address(l2Token), address(l1Token)); + _updateTokenMapping(address(l1Token), address(l2Token)); if (amount == 0) { hevm.expectRevert("withdraw zero amount"); if (useRouter) { @@ -521,7 +525,7 @@ contract L2CustomERC20GatewayTest is L2GatewayTestBase { gateway.withdrawERC20{value: feeToPay}(address(l2Token), amount, gasLimit); } - gateway.updateTokenMapping(address(l2Token), address(l1Token)); + _updateTokenMapping(address(l1Token), address(l2Token)); if (amount == 0) { hevm.expectRevert("withdraw zero amount"); if (useRouter) { @@ -580,4 +584,16 @@ contract L2CustomERC20GatewayTest is L2GatewayTestBase { address(new L2CustomERC20Gateway(address(counterpartGateway), address(router), address(messenger))) ); } + + function _updateTokenMapping(address _l1Token, address _l2Token) internal { + hevm.startPrank(AddressAliasHelper.applyL1ToL2Alias(address(l1Messenger))); + l2Messenger.relayMessage( + address(counterpartGateway), + address(gateway), + 0, + 0, + abi.encodeCall(gateway.updateTokenMapping, (_l2Token, _l1Token)) + ); + hevm.stopPrank(); + } } diff --git a/src/test/L2ReverseCustomERC20Gateway.t.sol b/src/test/L2ReverseCustomERC20Gateway.t.sol new file mode 100644 index 0000000..b0c6d84 --- /dev/null +++ b/src/test/L2ReverseCustomERC20Gateway.t.sol @@ -0,0 +1,450 @@ +// 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 {IL1ERC20Gateway} from "../L1/gateways/IL1ERC20Gateway.sol"; +import {L1ReverseCustomERC20Gateway} from "../L1/gateways/L1ReverseCustomERC20Gateway.sol"; +import {IL2ERC20Gateway} from "../L2/gateways/IL2ERC20Gateway.sol"; +import {L2ReverseCustomERC20Gateway} from "../L2/gateways/L2ReverseCustomERC20Gateway.sol"; +import {L2GatewayRouter} from "../L2/gateways/L2GatewayRouter.sol"; + +import {AddressAliasHelper} from "../libraries/common/AddressAliasHelper.sol"; + +import {L2GatewayTestBase} from "./L2GatewayTestBase.t.sol"; +import {MockScrollMessenger} from "./mocks/MockScrollMessenger.sol"; +import {MockGatewayRecipient} from "./mocks/MockGatewayRecipient.sol"; + +contract MockL2ReverseCustomERC20Gateway is L2ReverseCustomERC20Gateway { + constructor( + address _counterpart, + address _router, + address _messenger + ) L2ReverseCustomERC20Gateway(_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) + } + } + } +} + +contract L2ReverseCustomERC20GatewayTest is L2GatewayTestBase { + // from L2ReverseCustomERC20Gateway + event WithdrawERC20( + address indexed _l1Token, + address indexed _l2Token, + address indexed _from, + address _to, + uint256 _amount, + bytes _data + ); + event FinalizeDepositERC20( + address indexed _l1Token, + address indexed _l2Token, + address indexed _from, + address _to, + uint256 _amount, + bytes _data + ); + + MockL2ReverseCustomERC20Gateway private gateway; + L2GatewayRouter private router; + + L1ReverseCustomERC20Gateway private counterpartGateway; + + MockERC20 private l1Token; + MockERC20 private l2Token; + + function setUp() public { + setUpBase(); + // Deploy tokens + l1Token = new MockERC20("Mock L1", "ML1", 18); + l2Token = new MockERC20("Mock L2", "ML2", 18); + + // Deploy L1 contracts + counterpartGateway = new L1ReverseCustomERC20Gateway(address(1), address(1), address(1)); + + // Deploy L2 contracts + router = L2GatewayRouter(_deployProxy(address(new L2GatewayRouter()))); + gateway = _deployGateway(address(l2Messenger)); + + // Initialize L2 contracts + gateway.initialize(address(counterpartGateway), address(router), address(l2Messenger)); + router.initialize(address(0), address(gateway)); + + // Prepare token balances + l2Token.mint(address(this), type(uint128).max); + l2Token.approve(address(gateway), type(uint256).max); + } + + function testWithdrawERC20(uint256 amount, uint256 gasLimit) external { + _withdrawERC20(false, 0, amount, address(this), new bytes(0), gasLimit); + } + + function testWithdrawERC20WithRecipient( + uint256 amount, + address recipient, + uint256 gasLimit + ) external { + _withdrawERC20(false, 1, amount, recipient, new bytes(0), gasLimit); + } + + function testWithdrawERC20WithRecipientAndCalldata( + uint256 amount, + address recipient, + bytes memory dataToCall, + uint256 gasLimit + ) external { + _withdrawERC20(false, 2, amount, recipient, dataToCall, gasLimit); + } + + function testWithdrawERC20ByRouter(uint256 amount, uint256 gasLimit) external { + _withdrawERC20(true, 0, amount, address(this), new bytes(0), gasLimit); + } + + function testWithdrawERC20WithRecipientByRouter( + uint256 amount, + address recipient, + uint256 gasLimit + ) external { + _withdrawERC20(true, 1, amount, recipient, new bytes(0), gasLimit); + } + + function testWithdrawERC20WithRecipientAndCalldataByRouter( + uint256 amount, + address recipient, + bytes memory dataToCall, + uint256 gasLimit + ) external { + _withdrawERC20(true, 2, amount, recipient, dataToCall, gasLimit); + } + + function testFinalizeDepositERC20FailedMocking( + address sender, + address recipient, + uint256 amount, + bytes memory dataToCall + ) public { + amount = bound(amount, 1, 100000); + + // revert when caller is not messenger + hevm.expectRevert(ErrorCallerIsNotMessenger.selector); + gateway.finalizeDepositERC20(address(l1Token), address(l2Token), sender, recipient, amount, dataToCall); + + MockScrollMessenger mockMessenger = new MockScrollMessenger(); + gateway = _deployGateway(address(mockMessenger)); + gateway.initialize(address(counterpartGateway), address(router), address(mockMessenger)); + + // only call by counterpart + hevm.expectRevert(ErrorCallerIsNotCounterpartGateway.selector); + mockMessenger.callTarget( + address(gateway), + abi.encodeWithSelector( + gateway.finalizeDepositERC20.selector, + address(l1Token), + address(l2Token), + sender, + recipient, + amount, + dataToCall + ) + ); + + mockMessenger.setXDomainMessageSender(address(counterpartGateway)); + + // msg.value mismatch + hevm.expectRevert(L2ReverseCustomERC20Gateway.ErrorNonzeroMsgValue.selector); + mockMessenger.callTarget{value: 1}( + address(gateway), + abi.encodeWithSelector( + gateway.finalizeDepositERC20.selector, + address(l1Token), + address(l2Token), + sender, + recipient, + amount, + dataToCall + ) + ); + + // l1 token iszero + hevm.expectRevert(L2ReverseCustomERC20Gateway.ErrorL1TokenAddressIsZero.selector); + mockMessenger.callTarget( + address(gateway), + abi.encodeWithSelector( + gateway.finalizeDepositERC20.selector, + address(0), + address(l2Token), + sender, + recipient, + amount, + dataToCall + ) + ); + + // l1 token mismatch + hevm.expectRevert(L2ReverseCustomERC20Gateway.ErrorL1TokenAddressMismatch.selector); + mockMessenger.callTarget( + address(gateway), + abi.encodeWithSelector( + gateway.finalizeDepositERC20.selector, + address(l1Token), + address(l2Token), + sender, + recipient, + amount, + dataToCall + ) + ); + } + + function testFinalizeDepositERC20Failed( + address sender, + address recipient, + uint256 amount, + bytes memory dataToCall + ) public { + // blacklist some addresses + hevm.assume(recipient != address(0)); + + _updateTokenMapping(address(l1Token), address(l2Token)); + + amount = bound(amount, 1, l2Token.balanceOf(address(this))); + + // do finalize deposit token + bytes memory message = abi.encodeWithSelector( + IL2ERC20Gateway.finalizeDepositERC20.selector, + address(l1Token), + address(l2Token), + sender, + recipient, + amount, + dataToCall + ); + bytes memory xDomainCalldata = abi.encodeWithSignature( + "relayMessage(address,address,uint256,uint256,bytes)", + address(uint160(address(counterpartGateway)) + 1), + address(gateway), + 0, + 0, + message + ); + + // counterpart is not L1ReverseCustomERC20Gateway + // emit FailedRelayedMessage from L2ScrollMessenger + hevm.expectEmit(true, false, false, true); + emit FailedRelayedMessage(keccak256(xDomainCalldata)); + + uint256 gatewayBalance = l2Token.balanceOf(address(gateway)); + uint256 recipientBalance = l2Token.balanceOf(recipient); + assertBoolEq(false, l2Messenger.isL1MessageExecuted(keccak256(xDomainCalldata))); + hevm.startPrank(AddressAliasHelper.applyL1ToL2Alias(address(l1Messenger))); + l2Messenger.relayMessage(address(uint160(address(counterpartGateway)) + 1), address(gateway), 0, 0, message); + hevm.stopPrank(); + assertEq(gatewayBalance, l2Token.balanceOf(address(gateway))); + assertEq(recipientBalance, l2Token.balanceOf(recipient)); + assertBoolEq(false, l2Messenger.isL1MessageExecuted(keccak256(xDomainCalldata))); + } + + function testFinalizeDepositERC20( + address sender, + uint256 amount, + bytes memory dataToCall + ) public { + MockGatewayRecipient recipient = new MockGatewayRecipient(); + + _updateTokenMapping(address(l1Token), address(l2Token)); + + amount = bound(amount, 1, l2Token.balanceOf(address(this))); + + // deposit some token to L2ReverseCustomERC20Gateway + gateway.withdrawERC20(address(l2Token), amount, 0); + + // do finalize deposit token + bytes memory message = abi.encodeWithSelector( + IL2ERC20Gateway.finalizeDepositERC20.selector, + address(l1Token), + address(l2Token), + sender, + address(recipient), + amount, + dataToCall + ); + bytes memory xDomainCalldata = abi.encodeWithSignature( + "relayMessage(address,address,uint256,uint256,bytes)", + address(counterpartGateway), + address(gateway), + 0, + 0, + message + ); + + // emit FinalizeDepositERC20 from L2ReverseCustomERC20Gateway + { + hevm.expectEmit(true, true, true, true); + emit FinalizeDepositERC20( + address(l1Token), + address(l2Token), + sender, + address(recipient), + amount, + dataToCall + ); + } + + // emit RelayedMessage from L2ScrollMessenger + { + hevm.expectEmit(true, false, false, true); + emit RelayedMessage(keccak256(xDomainCalldata)); + } + + uint256 gatewayBalance = l2Token.balanceOf(address(gateway)); + uint256 recipientBalance = l2Token.balanceOf(address(recipient)); + assertBoolEq(false, l2Messenger.isL1MessageExecuted(keccak256(xDomainCalldata))); + hevm.startPrank(AddressAliasHelper.applyL1ToL2Alias(address(l1Messenger))); + l2Messenger.relayMessage(address(counterpartGateway), address(gateway), 0, 0, message); + hevm.stopPrank(); + assertEq(gatewayBalance - amount, l2Token.balanceOf(address(gateway))); + assertEq(recipientBalance + amount, l2Token.balanceOf(address(recipient))); + assertBoolEq(true, l2Messenger.isL1MessageExecuted(keccak256(xDomainCalldata))); + } + + function _withdrawERC20( + bool useRouter, + uint256 methodType, + uint256 amount, + address recipient, + bytes memory dataToCall, + uint256 gasLimit + ) private { + hevm.assume(recipient != address(0)); + amount = bound(amount, 1, l2Token.balanceOf(address(this))); + + // revert when reentrant + hevm.expectRevert("ReentrancyGuard: reentrant call"); + bytes memory reentrantData; + if (methodType == 0) { + reentrantData = abi.encodeWithSignature( + "withdrawERC20(address,uint256,uint256)", + address(l2Token), + amount, + gasLimit + ); + } else if (methodType == 1) { + reentrantData = abi.encodeWithSignature( + "withdrawERC20(address,address,uint256,uint256)", + address(l2Token), + recipient, + amount, + gasLimit + ); + } else if (methodType == 2) { + reentrantData = abi.encodeCall( + IL2ERC20Gateway.withdrawERC20AndCall, + (address(l2Token), recipient, amount, dataToCall, gasLimit) + ); + } + gateway.reentrantCall(useRouter ? address(router) : address(gateway), reentrantData); + + // revert when l2 token not support + hevm.expectRevert(L2ReverseCustomERC20Gateway.ErrorNoCorrespondingL1Token.selector); + _invokeWithdrawERC20Call(useRouter, methodType, address(l1Token), amount, recipient, dataToCall, gasLimit); + + _updateTokenMapping(address(l1Token), address(l2Token)); + + // revert when withdraw zero amount + hevm.expectRevert(L2ReverseCustomERC20Gateway.ErrorWithdrawZeroAmount.selector); + _invokeWithdrawERC20Call(useRouter, methodType, address(l2Token), 0, recipient, dataToCall, gasLimit); + + // succeed to withdraw + bytes memory message = abi.encodeCall( + IL1ERC20Gateway.finalizeWithdrawERC20, + (address(l1Token), address(l2Token), address(this), recipient, amount, dataToCall) + ); + bytes memory xDomainCalldata = abi.encodeCall( + l2Messenger.relayMessage, + (address(gateway), address(counterpartGateway), 0, 0, message) + ); + // should emit AppendMessage from L2MessageQueue + hevm.expectEmit(false, false, false, true); + emit AppendMessage(0, keccak256(xDomainCalldata)); + + // should emit SentMessage from L2ScrollMessenger + hevm.expectEmit(true, true, false, true); + emit SentMessage(address(gateway), address(counterpartGateway), 0, 0, gasLimit, message); + + // should emit WithdrawERC20 from L2LidoGateway + hevm.expectEmit(true, true, true, true); + emit WithdrawERC20(address(l1Token), address(l2Token), address(this), recipient, amount, dataToCall); + + uint256 gatewayBalance = l2Token.balanceOf(address(gateway)); + uint256 thisBalance = l2Token.balanceOf(address(this)); + assertEq(l2Messenger.messageSendTimestamp(keccak256(xDomainCalldata)), 0); + _invokeWithdrawERC20Call(useRouter, methodType, address(l2Token), amount, recipient, dataToCall, gasLimit); + assertGt(l2Messenger.messageSendTimestamp(keccak256(xDomainCalldata)), 0); + assertEq(thisBalance - amount, l2Token.balanceOf(address(this))); + assertEq(gatewayBalance + amount, l2Token.balanceOf(address(gateway))); + } + + function _invokeWithdrawERC20Call( + bool useRouter, + uint256 methodType, + address token, + uint256 amount, + address recipient, + bytes memory dataToCall, + uint256 gasLimit + ) private { + if (useRouter) { + if (methodType == 0) { + router.withdrawERC20(token, amount, gasLimit); + } else if (methodType == 1) { + router.withdrawERC20(token, recipient, amount, gasLimit); + } else if (methodType == 2) { + router.withdrawERC20AndCall(token, recipient, amount, dataToCall, gasLimit); + } + } else { + if (methodType == 0) { + gateway.withdrawERC20(token, amount, gasLimit); + } else if (methodType == 1) { + gateway.withdrawERC20(token, recipient, amount, gasLimit); + } else if (methodType == 2) { + gateway.withdrawERC20AndCall(token, recipient, amount, dataToCall, gasLimit); + } + } + } + + function _deployGateway(address messenger) internal returns (MockL2ReverseCustomERC20Gateway _gateway) { + _gateway = MockL2ReverseCustomERC20Gateway(_deployProxy(address(0))); + + admin.upgrade( + ITransparentUpgradeableProxy(address(_gateway)), + address( + new MockL2ReverseCustomERC20Gateway(address(counterpartGateway), address(router), address(messenger)) + ) + ); + } + + function _updateTokenMapping(address _l1Token, address _l2Token) internal { + hevm.startPrank(AddressAliasHelper.applyL1ToL2Alias(address(l1Messenger))); + l2Messenger.relayMessage( + address(counterpartGateway), + address(gateway), + 0, + 0, + abi.encodeCall(gateway.updateTokenMapping, (_l2Token, _l1Token)) + ); + hevm.stopPrank(); + } +} diff --git a/src/test/batch-bridge/L1BatchBridgeGateway.t.sol b/src/test/batch-bridge/L1BatchBridgeGateway.t.sol index 0f26de4..a71f195 100644 --- a/src/test/batch-bridge/L1BatchBridgeGateway.t.sol +++ b/src/test/batch-bridge/L1BatchBridgeGateway.t.sol @@ -95,7 +95,7 @@ contract L1BatchBridgeGatewayTest is L1GatewayTestBase { // Prepare token balances l1Token.mint(address(this), type(uint128).max); - gateway.updateTokenMapping(address(l1Token), address(l2Token)); + gateway.updateTokenMapping{value: 1 ether}(address(l1Token), address(l2Token)); hevm.warp(1000000); } @@ -442,7 +442,7 @@ contract L1BatchBridgeGatewayTest is L1GatewayTestBase { // emit SentMessage by deposit ETH hevm.expectEmit(true, true, false, true); - emit SentMessage(address(batch), address(counterpartBatch), 1000, 0, ETH_DEPOSIT_SAFE_GAS_LIMIT, ""); + emit SentMessage(address(batch), address(counterpartBatch), 1000, 1, ETH_DEPOSIT_SAFE_GAS_LIMIT, ""); // emit SentMessage by batchBridge hevm.expectEmit(true, true, false, true); @@ -450,7 +450,7 @@ contract L1BatchBridgeGatewayTest is L1GatewayTestBase { address(batch), address(counterpartBatch), 0, - 1, + 2, SAFE_BATCH_BRIDGE_GAS_LIMIT, abi.encodeCall( L2BatchBridgeGateway.finalizeBatchDeposit, @@ -485,7 +485,7 @@ contract L1BatchBridgeGatewayTest is L1GatewayTestBase { // emit SentMessage by deposit ETH hevm.expectEmit(true, true, false, true); - emit SentMessage(address(batch), address(counterpartBatch), 900, 2, ETH_DEPOSIT_SAFE_GAS_LIMIT, ""); + emit SentMessage(address(batch), address(counterpartBatch), 900, 3, ETH_DEPOSIT_SAFE_GAS_LIMIT, ""); // emit SentMessage by batchBridge hevm.expectEmit(true, true, false, true); @@ -493,7 +493,7 @@ contract L1BatchBridgeGatewayTest is L1GatewayTestBase { address(batch), address(counterpartBatch), 0, - 3, + 4, SAFE_BATCH_BRIDGE_GAS_LIMIT, abi.encodeCall( L2BatchBridgeGateway.finalizeBatchDeposit, @@ -544,14 +544,14 @@ contract L1BatchBridgeGatewayTest is L1GatewayTestBase { ); // emit SentMessage by deposit ERC20 hevm.expectEmit(true, true, false, true); - emit SentMessage(address(gateway), address(counterpartGateway), 0, 0, ERC20_DEPOSIT_SAFE_GAS_LIMIT, message); + emit SentMessage(address(gateway), address(counterpartGateway), 0, 1, ERC20_DEPOSIT_SAFE_GAS_LIMIT, message); // emit SentMessage by batchBridge hevm.expectEmit(true, true, false, true); emit SentMessage( address(batch), address(counterpartBatch), 0, - 1, + 2, SAFE_BATCH_BRIDGE_GAS_LIMIT, abi.encodeCall( L2BatchBridgeGateway.finalizeBatchDeposit, @@ -598,14 +598,14 @@ contract L1BatchBridgeGatewayTest is L1GatewayTestBase { ); // emit SentMessage by deposit ERC20 hevm.expectEmit(true, true, false, true); - emit SentMessage(address(gateway), address(counterpartGateway), 0, 2, ERC20_DEPOSIT_SAFE_GAS_LIMIT, message); + emit SentMessage(address(gateway), address(counterpartGateway), 0, 3, ERC20_DEPOSIT_SAFE_GAS_LIMIT, message); // emit SentMessage by batchBridge hevm.expectEmit(true, true, false, true); emit SentMessage( address(batch), address(counterpartBatch), 0, - 3, + 4, SAFE_BATCH_BRIDGE_GAS_LIMIT, abi.encodeCall( L2BatchBridgeGateway.finalizeBatchDeposit,