From c762a3cf083c97ac87ade6752b4e777fd77156a1 Mon Sep 17 00:00:00 2001 From: Goran Vladika Date: Fri, 26 Jan 2024 13:11:30 +0100 Subject: [PATCH] Add L1ForceOnlyReverseCustomGatewayTest tests --- .../L1ForceOnlyReverseCustomGateway.t.sol | 313 ++++++++++++++++++ test-foundry/L1ReverseCustomGateway.t.sol | 23 +- 2 files changed, 322 insertions(+), 14 deletions(-) create mode 100644 test-foundry/L1ForceOnlyReverseCustomGateway.t.sol diff --git a/test-foundry/L1ForceOnlyReverseCustomGateway.t.sol b/test-foundry/L1ForceOnlyReverseCustomGateway.t.sol new file mode 100644 index 000000000..1cbf40c2a --- /dev/null +++ b/test-foundry/L1ForceOnlyReverseCustomGateway.t.sol @@ -0,0 +1,313 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.0; + +import "./L1ReverseCustomGateway.t.sol"; +import {L1ForceOnlyReverseCustomGateway} from + "contracts/tokenbridge/ethereum/gateway/L1ForceOnlyReverseCustomGateway.sol"; +import { + MintableTestCustomTokenL1, + ReverseTestCustomTokenL1 +} from "contracts/tokenbridge/test/TestCustomTokenL1.sol"; +import {ERC20PresetMinterPauser} from + "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; + +contract L1ForceOnlyReverseCustomGatewayTest is L1ReverseCustomGatewayTest { + function setUp() public virtual override { + inbox = address(new InboxMock()); + + l1Gateway = new L1ForceOnlyReverseCustomGateway(); + L1ForceOnlyReverseCustomGateway(address(l1Gateway)).initialize( + l2Gateway, router, inbox, owner + ); + + token = IERC20(address(new TestERC20())); + + maxSubmissionCost = 20; + retryableCost = maxSubmissionCost + gasPriceBid * maxGas; + + // fund user and router + vm.prank(user); + TestERC20(address(token)).mint(); + vm.deal(router, 100 ether); + vm.deal(address(token), 100 ether); + vm.deal(owner, 100 ether); + } + + /* solhint-disable func-name-mixedcase */ + function test_calculateL2TokenAddress(address l1Token, address l2Token) + public + virtual + override + { + vm.assume(l1Token != FOUNDRY_CHEATCODE_ADDRESS && l2Token != FOUNDRY_CHEATCODE_ADDRESS); + vm.deal(l1Token, 100 ether); + + // register token to gateway + // register token to gateway + address[] memory l1Tokens = new address[](1); + l1Tokens[0] = l1Token; + address[] memory l2Tokens = new address[](1); + l2Tokens[0] = l2Token; + + vm.prank(owner); + uint256 seqNum0 = L1CustomGateway(address(l1Gateway)).forceRegisterTokenToL2{ + value: retryableCost + }(l1Tokens, l2Tokens, maxGas, gasPriceBid, maxSubmissionCost); + + assertEq(l1Gateway.calculateL2TokenAddress(l1Token), l2Token, "Invalid L2 token address"); + } + + function test_outboundTransfer() public override { + // fund user with tokens + MintableTestCustomTokenL1 bridgedToken = + new ReverseTestCustomTokenL1(address(l1Gateway), router); + vm.prank(address(user)); + bridgedToken.mint(); + + // snapshot state before + uint256 userBalanceBefore = bridgedToken.balanceOf(user); + + uint256 amount = 300; + bytes memory callHookData = ""; + bytes memory routerEncodedData = buildRouterEncodedData(callHookData); + + // register token to gateway + address[] memory l1Tokens = new address[](1); + l1Tokens[0] = address(bridgedToken); + address[] memory l2Tokens = new address[](1); + l2Tokens[0] = makeAddr("tokenL2Address"); + + vm.prank(owner); + uint256 seqNum0 = L1CustomGateway(address(l1Gateway)).forceRegisterTokenToL2{ + value: retryableCost + }(l1Tokens, l2Tokens, maxGas, gasPriceBid, maxSubmissionCost); + + // approve token + vm.prank(user); + bridgedToken.approve(address(l1Gateway), amount); + + // event checkers + vm.expectEmit(true, true, true, true); + emit TicketData(maxSubmissionCost); + + vm.expectEmit(true, true, true, true); + emit RefundAddresses(user, user); + + vm.expectEmit(true, true, true, true); + emit InboxRetryableTicket( + address(l1Gateway), + l2Gateway, + 0, + maxGas, + l1Gateway.getOutboundCalldata(address(bridgedToken), user, user, amount, callHookData) + ); + + vm.expectEmit(true, true, true, true); + emit DepositInitiated(address(bridgedToken), user, user, 1, amount); + + // trigger transfer + vm.prank(router); + bytes memory seqNum1 = l1Gateway.outboundTransfer{value: retryableCost}( + address(bridgedToken), user, amount, maxGas, gasPriceBid, routerEncodedData + ); + + // check tokens are burned + uint256 userBalanceAfter = bridgedToken.balanceOf(user); + assertEq(userBalanceBefore - userBalanceAfter, amount, "Wrong user balance"); + + assertEq(seqNum0, 0, "Invalid seqNum0"); + assertEq(seqNum1, abi.encode(1), "Invalid seqNum1"); + } + + function test_outboundTransferCustomRefund() public override { + // fund user with tokens + MintableTestCustomTokenL1 bridgedToken = + new ReverseTestCustomTokenL1(address(l1Gateway), router); + vm.prank(address(user)); + bridgedToken.mint(); + + // snapshot state before + uint256 userBalanceBefore = bridgedToken.balanceOf(user); + + uint256 amount = 450; + bytes memory callHookData = ""; + bytes memory routerEncodedData = buildRouterEncodedData(callHookData); + + // register token to gateway + address[] memory l1Tokens = new address[](1); + l1Tokens[0] = address(bridgedToken); + address[] memory l2Tokens = new address[](1); + l2Tokens[0] = makeAddr("tokenL2Address"); + + vm.prank(owner); + uint256 seqNum0 = L1CustomGateway(address(l1Gateway)).forceRegisterTokenToL2{ + value: retryableCost + }(l1Tokens, l2Tokens, maxGas, gasPriceBid, maxSubmissionCost); + + // approve token + vm.prank(user); + bridgedToken.approve(address(l1Gateway), amount); + + // event checkers + vm.expectEmit(true, true, true, true); + emit TicketData(maxSubmissionCost); + + vm.expectEmit(true, true, true, true); + emit RefundAddresses(creditBackAddress, user); + + vm.expectEmit(true, true, true, true); + emit InboxRetryableTicket( + address(l1Gateway), + l2Gateway, + 0, + maxGas, + l1Gateway.getOutboundCalldata(address(bridgedToken), user, user, amount, callHookData) + ); + + vm.expectEmit(true, true, true, true); + emit DepositInitiated(address(bridgedToken), user, user, 1, amount); + + // trigger deposit + vm.prank(router); + bytes memory seqNum1 = l1Gateway.outboundTransferCustomRefund{value: retryableCost}( + address(bridgedToken), + creditBackAddress, + user, + amount, + maxGas, + gasPriceBid, + routerEncodedData + ); + + // check tokens are escrowed + uint256 userBalanceAfter = bridgedToken.balanceOf(user); + assertEq(userBalanceBefore - userBalanceAfter, amount, "Wrong user balance"); + + assertEq(seqNum0, 0, "Invalid seqNum0"); + assertEq(seqNum1, abi.encode(1), "Invalid seqNum1"); + } + + function test_outboundTransferCustomRefund_revert_InsufficientAllowance() public override { + // fund user with tokens + MintableTestCustomTokenL1 bridgedToken = + new ReverseTestCustomTokenL1(address(l1Gateway), router); + vm.prank(address(user)); + bridgedToken.mint(); + + uint256 tooManyTokens = 500 ether; + + // register token to gateway + address[] memory l1Tokens = new address[](1); + l1Tokens[0] = address(bridgedToken); + address[] memory l2Tokens = new address[](1); + l2Tokens[0] = makeAddr("tokenL2Address"); + + vm.prank(owner); + uint256 seqNum0 = L1CustomGateway(address(l1Gateway)).forceRegisterTokenToL2{ + value: retryableCost + }(l1Tokens, l2Tokens, maxGas, gasPriceBid, maxSubmissionCost); + + vm.prank(router); + vm.expectRevert("ERC20: burn amount exceeds balance"); + l1Gateway.outboundTransferCustomRefund{value: 1 ether}( + address(bridgedToken), + user, + user, + tooManyTokens, + 0.1 ether, + 0.01 ether, + buildRouterEncodedData("") + ); + } + + function test_outboundTransferCustomRefund_revert_NoL2TokenSet() public virtual override { + uint256 tooManyTokens = 500 ether; + + // register token to gateway + address[] memory l1Tokens = new address[](1); + l1Tokens[0] = address(token); + address[] memory l2Tokens = new address[](1); + l2Tokens[0] = address(0); + + vm.prank(owner); + L1CustomGateway(address(l1Gateway)).forceRegisterTokenToL2{value: retryableCost}( + l1Tokens, l2Tokens, maxGas, gasPriceBid, maxSubmissionCost + ); + + vm.prank(router); + vm.expectRevert("NO_L2_TOKEN_SET"); + l1Gateway.outboundTransferCustomRefund{value: 1 ether}( + address(token), + user, + user, + tooManyTokens, + 0.1 ether, + 0.01 ether, + buildRouterEncodedData("") + ); + } + + function test_outboundTransferCustomRefund_revert_Reentrancy() public override { + // fund user with tokens + MintableTestCustomTokenL1 bridgedToken = + new ReverseTestCustomTokenL1(address(l1Gateway), router); + vm.prank(address(user)); + bridgedToken.mint(); + + // register token to gateway + address[] memory l1Tokens = new address[](1); + l1Tokens[0] = address(bridgedToken); + address[] memory l2Tokens = new address[](1); + l2Tokens[0] = makeAddr("tokenL2Address"); + + vm.prank(owner); + L1CustomGateway(address(l1Gateway)).forceRegisterTokenToL2{value: retryableCost}( + l1Tokens, l2Tokens, maxGas, gasPriceBid, maxSubmissionCost + ); + + // approve token + uint256 amount = 450; + vm.prank(user); + bridgedToken.approve(address(l1Gateway), amount); + + // trigger re-entrancy + MockReentrantERC20 mockReentrantERC20 = new MockReentrantERC20(); + vm.etch(address(bridgedToken), address(mockReentrantERC20).code); + + vm.expectRevert("ReentrancyGuard: reentrant call"); + vm.prank(router); + l1Gateway.outboundTransferCustomRefund{value: retryableCost}( + address(bridgedToken), + creditBackAddress, + user, + amount, + maxGas, + gasPriceBid, + buildRouterEncodedData("") + ); + } + + function test_registerTokenToL2(address, address l2Token) public virtual override { + vm.expectRevert("REGISTER_TOKEN_ON_L2_DISABLED"); + L1CustomGateway(address(l1Gateway)).registerTokenToL2{value: retryableCost}( + l2Token, maxGas, gasPriceBid, maxSubmissionCost + ); + } + + function test_registerTokenToL2_CustomRefund(address, address) public virtual override { + 0; // N/A + } + + function test_registerTokenToL2_UpdateToSameAddress(address, address) public virtual override { + 0; // N/A + } + + function test_registerTokenToL2_revert_NoUpdateToDifferentAddress() public virtual override { + 0; // N/A + } + + function test_registerTokenToL2_revert_NotArbEnabled() public virtual override { + 0; // N/A + } +} diff --git a/test-foundry/L1ReverseCustomGateway.t.sol b/test-foundry/L1ReverseCustomGateway.t.sol index bd5ea117d..311bac590 100644 --- a/test-foundry/L1ReverseCustomGateway.t.sol +++ b/test-foundry/L1ReverseCustomGateway.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.0; -import {L1CustomGatewayTest, InboxMock, IERC20, IInbox, TestERC20} from "./L1CustomGateway.t.sol"; +import "./L1CustomGateway.t.sol"; import {L1ReverseCustomGateway} from "contracts/tokenbridge/ethereum/gateway/L1ReverseCustomGateway.sol"; import { @@ -33,7 +33,7 @@ contract L1ReverseCustomGatewayTest is L1CustomGatewayTest { } /* solhint-disable func-name-mixedcase */ - function test_finalizeInboundTransfer() public override { + function test_finalizeInboundTransfer() public virtual override { // fund gateway with bridged tokens MintableTestCustomTokenL1 bridgedToken = new MintableTestCustomTokenL1(address(l1Gateway), router); @@ -61,7 +61,7 @@ contract L1ReverseCustomGatewayTest is L1CustomGatewayTest { assertEq(userBalanceAfter - userBalanceBefore, amount, "Wrong user balance"); } - function test_outboundTransfer() public override { + function test_outboundTransfer() public virtual override { // fund user with tokens MintableTestCustomTokenL1 bridgedToken = new ReverseTestCustomTokenL1(address(l1Gateway), router); @@ -124,7 +124,7 @@ contract L1ReverseCustomGatewayTest is L1CustomGatewayTest { assertEq(seqNum1, abi.encode(1), "Invalid seqNum1"); } - function test_outboundTransferCustomRefund() public override { + function test_outboundTransferCustomRefund() public virtual override { // fund user with tokens MintableTestCustomTokenL1 bridgedToken = new ReverseTestCustomTokenL1(address(l1Gateway), router); @@ -193,7 +193,11 @@ contract L1ReverseCustomGatewayTest is L1CustomGatewayTest { assertEq(seqNum1, abi.encode(1), "Invalid seqNum1"); } - function test_outboundTransferCustomRefund_revert_InsufficientAllowance() public override { + function test_outboundTransferCustomRefund_revert_InsufficientAllowance() + public + virtual + override + { // fund user with tokens MintableTestCustomTokenL1 bridgedToken = new ReverseTestCustomTokenL1(address(l1Gateway), router); @@ -272,12 +276,3 @@ contract L1ReverseCustomGatewayTest is L1CustomGatewayTest { ); } } - -contract MockReentrantERC20 { - function bridgeBurn(address, uint256) external { - // re-enter - L1ReverseCustomGateway(msg.sender).outboundTransferCustomRefund( - address(100), address(100), address(100), 2, 2, 3, bytes("") - ); - } -}